Skip to content

Commit 8434f1a

Browse files
dmealingclaude
andcommitted
fix(java,kotlin,python): derive the PK type from metadata instead of hardcoding it
The same bug in three ports: the generated API code hardcoded the primary-key type, so an entity declaring `identity.primary @generation: uuid` got broken output while its own DTO/model correctly used UUID. Not a metamodel gap — a missed reuse: every port already had the type mapper and was already using it a few lines away. The Kotlin generator even documented the assumption: "for v1 we just default to \"id\" : Long for the route signatures since the canonical Author/BaseEntity convention is a single Long PK." It resolved the identity correctly, then threw the type away. Every entity in every conformance corpus is a `field.long` increment PK, so no gate ever contradicted it. - Java: `@PathVariable Long id` and `findById(Long id)` → Spring rejected a uuid path var with 400, and the generated repository interface was un-implementable against a UUID-keyed entity. New `SpringTypeMapper.primaryKeyJavaType()` (ADR-0039 resolving accessors) threaded through the controller (get/update/delete/M:N/TPH polymorphic + per-subtype) and the repository (findById/update/delete/M:N finder/TPH variants). - Kotlin: WORSE — the generated code did not COMPILE. `@PathVariable id: Long` then `pkField eq id` against Exposed's `Column<UUID>` is a type error, and FK columns hardcoded `long("col").references(...)` so `Column<Long>.references(Column<UUID>)` broke the build for ANY relationship pointing at a uuid-PK entity. PK/FK types are now derived from the target's declared identity. Also found: the FR-009 filter coercer had no uuid arm, so `filter[id][eq]=<uuid>` would have ClassCastException'd at runtime; and TPH `writableFields` hardcoded the literal "id" when excluding the PK, wrong for any PK not named `id`. - Python: the generated FastAPI router typed every path id `int`, so a real uuid was rejected with 422 by Pydantic and never reached the handler — the endpoint was simply unusable, and the cross-port contract (404 for a missing row) was broken. Now derives via the existing `type_map`, threaded through the repository Protocol, get/update/delete, M:N traversal and TPH routes. The generated router is booted over HTTP in a new test that proves 422 -> 404/200. C# needed no change: it already derived the PK scalar and threaded it through every route. It is the reference the other three were fixed against. Gates: java-slow CI (full reactor + Java and Kotlin integration suites incl. Testcontainers PG) green — Kotlin integration 98/98; codegen-spring 164/164; codegen-kotlin 279/279 with no byte-drift on existing snapshots; Python 1422 pass. A `field.long` increment PK still generates byte-identical output. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hd5NWZ1rKau63vJzrBSpLb
1 parent 61e5d8e commit 8434f1a

15 files changed

Lines changed: 1240 additions & 76 deletions

File tree

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinExposedTableGenerator.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,7 @@ open class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject
10761076
val refSuffix = referentialActionSuffix(rel.onDeleteRaw, rel.onUpdateRaw)
10771077
return FkColumnSpec(
10781078
propertyName = propertyName,
1079-
columnExpr = "long(\"$colName\").references($targetTable.${primaryKeyProperty(target)}$refSuffix)",
1079+
columnExpr = "${fkColumnBuilder(target, colName)}.references($targetTable.${primaryKeyProperty(target)}$refSuffix)",
10801080
refSuffix = refSuffix,
10811081
declared = true,
10821082
targetTable = targetTable,
@@ -1105,7 +1105,7 @@ open class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject
11051105
val refSuffix = referentialActionSuffix(rel.onDeleteRaw, rel.onUpdateRaw)
11061106
return FkColumnSpec(
11071107
propertyName = propertyName,
1108-
columnExpr = "long(\"$colName\").references($ownerTable.${primaryKeyProperty(owner)}$refSuffix)",
1108+
columnExpr = "${fkColumnBuilder(owner, colName)}.references($ownerTable.${primaryKeyProperty(owner)}$refSuffix)",
11091109
refSuffix = refSuffix,
11101110
declared = false,
11111111
targetTable = ownerTable,
@@ -1231,6 +1231,28 @@ open class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject
12311231
return KotlinNaming.safeColumnProperty(pkField ?: "id")
12321232
}
12331233

1234+
/**
1235+
* The Exposed column builder for a FK column pointing at [target]'s primary key.
1236+
* The FK column's SQL type MUST match the referenced PK's — Exposed's typed
1237+
* `Column<S : T>.references(Column<T>)` rejects a mismatch at compile time, so a
1238+
* hard-coded `long("author_id")` referencing a uuid PK broke the build of ANY
1239+
* relationship aimed at a uuid-keyed entity. Derived from the target's single-field
1240+
* `identity.primary` via [KotlinTypeMapper.exposedColumnSpec] (uuid → `uuid("col")`,
1241+
* long → `long("col")`, int → `integer("col")`, string → text/varchar), with the FK's
1242+
* own physical column name. Falls back to `long("col")` when the PK field can't be
1243+
* resolved — the historical default (same lossy-tolerant policy as
1244+
* [primaryKeyProperty]).
1245+
*/
1246+
private fun fkColumnBuilder(target: MetaObject, colName: String): String {
1247+
// ADR-0039: resolving lookups — the identity and its field may be inherited.
1248+
val pkFieldName = target.getIdentities(true).firstOrNull { it.isPrimary }
1249+
?.fields?.firstOrNull() ?: return "long(\"$colName\")"
1250+
val pkField = target.metaFields.firstOrNull { it.name == pkFieldName }
1251+
?: return "long(\"$colName\")"
1252+
return runCatching { KotlinTypeMapper.exposedColumnSpec(pkField, colName) }
1253+
.getOrDefault("long(\"$colName\")")
1254+
}
1255+
12341256
/** Read the `@column` attr on a relationship (inheritance allowed); null when absent. */
12351257
private fun readColumnAttr(rel: MetaRelationship): String? {
12361258
if (!rel.hasMetaAttr(CoreDBMetaDataProvider.COLUMN, true)) return null

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinRelationsGenerator.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ open class KotlinRelationsGenerator : MultiFileDirectGeneratorBase<MetaObject>()
138138
}
139139
}
140140
append("import org.jetbrains.exposed.sql.selectAll\n")
141+
// java.util.UUID appears in helper signatures when the owner's PK (or a
142+
// reverse-FK value type) is uuid — the param types were already derived, but
143+
// without this import the generated file had an unresolved reference. Emitted
144+
// conditionally so uuid-free files stay byte-identical.
145+
if (pkParamSimpleName == "UUID" || reverseFks.any { it.valueType == "UUID" }) {
146+
append("import java.util.UUID\n")
147+
}
141148
append("\n")
142149
append("/** GENERATED — extension fns for `$ownerShort` to-many relationships. Do not hand-edit. */\n")
143150
var first = true
@@ -258,11 +265,12 @@ open class KotlinRelationsGenerator : MultiFileDirectGeneratorBase<MetaObject>()
258265
/**
259266
* Resolve the Kotlin type of [entity]'s primary key. Falls back to [LONG] when
260267
* the primary identity is missing, multi-field, or its field can't be resolved
261-
* — Long matches the dominant convention (entities almost always have a single
262-
* `field.long` PK; KotlinExposedTableGenerator's FK columns hard-code
263-
* `long("…").references(...)`). Same lossy-tolerant policy as the table
264-
* generator: defensive defaults rather than throwing on partially-formed
265-
* metadata.
268+
* — Long matches the dominant convention (a single `field.long` PK) and the
269+
* same fallback KotlinExposedTableGenerator's FK column builder and
270+
* KotlinSpringControllerGenerator's route signatures use, so the derived types
271+
* stay in lockstep across the three generators. Same lossy-tolerant policy as
272+
* the table generator: defensive defaults rather than throwing on
273+
* partially-formed metadata.
266274
*/
267275
protected open fun primaryKeyKotlinType(entity: MetaObject): TypeName {
268276
// ADR-0039: identities are inheritable — RESOLVE via getIdentities(true);

0 commit comments

Comments
 (0)