Skip to content

Commit beafb58

Browse files
committed
Merge branch 'feat/own-fix-jvm' into feat/own-fix-reference
2 parents 0c16c92 + 5a82f37 commit beafb58

20 files changed

Lines changed: 232 additions & 52 deletions

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,10 @@ open class KotlinEntityGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
371371
}
372372

373373
private fun <V : MetaValidator> validator(field: MetaField<*>, type: Class<V>): V? {
374-
for (child in field.children) {
374+
// ADR-0039: resolving member iteration — getChildren(type) walks the extends
375+
// super chain, so a concrete field inherits validators from an abstract base.
376+
// A raw field.children read would miss inherited validators.
377+
for (child in field.getChildren(MetaValidator::class.java, true)) {
375378
if (type.isInstance(child)) return type.cast(child)
376379
}
377380
return null

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ open class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject
8181
entity.subType != MetaObject.SUBTYPE_PROJECTION) continue
8282
// Abstract entities are inheritance scaffolding — never emit a persistence table.
8383
if (KotlinGenUtil.isAbstractEntity(entity)) continue
84-
val sourceRdb = entity.children.filterIsInstance<RdbSource>().firstOrNull() ?: continue
84+
// ADR-0039: resolving source lookup — an entity inheriting its source.rdb via
85+
// extends must still emit a table (own-only .children returned null → no table).
86+
val sourceRdb = KotlinGenUtil.firstRdbSource(entity) ?: continue
8587
val kind = sourceRdb.effectiveKind
8688
// table + view + materializedView → emit; view-like kinds are emitted read-only.
8789
// storedProc → skip; consumer should wire KotlinStoredProcGenerator for those entities.
@@ -927,7 +929,8 @@ open class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject
927929
*/
928930
private fun buildInverseFkSpec(owner: MetaObject, rel: MetaRelationship): FkColumnSpec? {
929931
// Skip when the owner has no rdb source — there would be no OwnerTable to reference.
930-
if (owner.children.filterIsInstance<RdbSource>().firstOrNull() == null) return null
932+
// ADR-0039: resolving (an inherited source.rdb still means the owner is persisted).
933+
if (!KotlinGenUtil.hasRdbSource(owner)) return null
931934
val ownerShort = PackageMapping.splitFqn(owner.name).second
932935
val ownerTable = ownerShort + "Table"
933936
val propertyName = ownerShort.replaceFirstChar { it.lowercaseChar() } + "Id"
@@ -1027,7 +1030,8 @@ open class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject
10271030
val targetEntityName = child.targetEntity ?: continue
10281031
val target = KotlinGenUtil.resolveObjectByShortOrFqn(loader, targetEntityName) ?: continue
10291032
// Skip when the target has no rdb source — Exposed cannot reference a non-table.
1030-
if (target.children.filterIsInstance<RdbSource>().firstOrNull() == null) continue
1033+
// ADR-0039: resolving (an inherited source.rdb still means the target is a table).
1034+
if (!KotlinGenUtil.hasRdbSource(target)) continue
10311035
// Soft references register a null-targetTable entry so dedup-vs-inferred works,
10321036
// but column emission will skip the `.references(...)` decoration.
10331037
val targetTable = if (child.isEnforced)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ internal object KotlinExtractSchemaEmitter {
143143
* Mirrors [KotlinTypeMapper.kotlinTypeName] but always nullable with `= null` defaults.
144144
*/
145145
private fun nullableTypeName(field: MetaField<*>): String = when {
146-
field.isArray -> "List<String>?"
146+
// ADR-0039: resolving array-ness (isArray is the own-only native flag).
147+
field.isArrayType() -> "List<String>?"
147148
field is EnumField -> "String?"
148149
field is StringField -> "String?"
149150
field is IntegerField -> "Int?"

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ open class KotlinFilterAllowlistGenerator : MultiFileDirectGeneratorBase<MetaObj
5555
val outRoot = Paths.get(outDir.absolutePath)
5656
for (entity in loader.metaObjects) {
5757
if (entity.subType != MetaObject.SUBTYPE_ENTITY) continue
58-
val sourceRdb = entity.children.filterIsInstance<RdbSource>().firstOrNull() ?: continue
58+
// ADR-0039: resolving source lookup (inherited source.rdb via extends).
59+
val sourceRdb = KotlinGenUtil.firstRdbSource(entity) ?: continue
5960
// Only writable tables get a filter allowlist (the controller is also
6061
// table-only). View / materializedView are read-only; storedProc has its
6162
// own dispatch; tableFunction has no controller surface today.

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.metaobjects.MetaData
44
import com.metaobjects.field.MetaField
55
import com.metaobjects.loader.MetaDataLoader
66
import com.metaobjects.`object`.MetaObject
7+
import com.metaobjects.source.RdbSource
78

89
/**
910
* Internal helpers shared by the codegen-kotlin generators. Extracted to keep
@@ -23,6 +24,23 @@ internal object KotlinGenUtil {
2324
return null
2425
}
2526

27+
/**
28+
* The first `source.rdb` child of [obj], RESOLVED through the `extends` super chain
29+
* (ADR-0039). `MetaObject.getSources(true)` walks the inheritance chain, so an entity
30+
* that inherits its `source.rdb` from a base entity still resolves a source — a raw
31+
* `obj.children.filterIsInstance<RdbSource>()` read returned null for such entities and
32+
* the generators emitted NOTHING for them (the high-blast-radius own-accessor bug).
33+
* Returns null when neither the object nor any ancestor declares an `source.rdb`.
34+
*/
35+
fun firstRdbSource(obj: MetaObject): RdbSource? =
36+
obj.getSources(true).filterIsInstance<RdbSource>().firstOrNull()
37+
38+
/**
39+
* True iff [obj] (or any ancestor via `extends`) declares a `source.rdb` — i.e. it is
40+
* persisted. Resolving (ADR-0039). See [firstRdbSource].
41+
*/
42+
fun hasRdbSource(obj: MetaObject): Boolean = firstRdbSource(obj) != null
43+
2644
/**
2745
* Split `"A.b"` into `("A","b")`; null if the ref isn't a single-dot ref
2846
* (no dot, leading dot, or trailing dot).

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ internal object KotlinOutputFormatSpecEmitter {
102102
private fun promptFieldLiteral(field: MetaField<*>): String {
103103
val name = field.name
104104
val required = KotlinExtractSchemaEmitter.isRequired(field)
105-
val array = field.isArray
105+
// ADR-0039: resolving array-ness (isArray is the own-only native flag).
106+
val array = field.isArrayType()
106107

107108
// Nested object OR open-keyed map — both structured JSON values; bounded
108109
// deferral (mirrors Java plan 3.1). FieldKind.OBJECT, nested prompt deferred.
@@ -138,9 +139,10 @@ internal object KotlinOutputFormatSpecEmitter {
138139
val valuesLit = "listOf($valuesList)"
139140

140141
// @enumDoc — optional; null when absent or empty.
142+
// ADR-0039: @enumDoc is an effective property — resolve (consistent with @values above).
141143
val enumDocLit: String = run {
142-
if (!field.hasMetaAttr(EnumField.ATTR_ENUM_DOC, false)) return@run "null"
143-
val v = field.getMetaAttr(EnumField.ATTR_ENUM_DOC, false).value
144+
if (!field.hasMetaAttr(EnumField.ATTR_ENUM_DOC)) return@run "null"
145+
val v = field.getMetaAttr(EnumField.ATTR_ENUM_DOC).value
144146
if (v is Properties && v.isNotEmpty()) KotlinExtractSchemaEmitter.buildMapOfLiteral(v) else "null"
145147
}
146148

@@ -160,8 +162,10 @@ internal object KotlinOutputFormatSpecEmitter {
160162
* example/instruction free-text containing quotes or newlines embeds safely in Kotlin source.
161163
*/
162164
private fun optStringAttr(field: MetaField<*>, attrName: String): String {
163-
if (!field.hasMetaAttr(attrName, false)) return "null"
164-
val v = field.getMetaAttr(attrName, false).valueAsString ?: return "null"
165+
// ADR-0039: @example/@instruction are effective properties — resolve (consistent
166+
// with @values/@required which resolve).
167+
if (!field.hasMetaAttr(attrName)) return "null"
168+
val v = field.getMetaAttr(attrName).valueAsString ?: return "null"
165169
return "\"${KotlinExtractSchemaEmitter.kotlinStringLiteral(v)}\""
166170
}
167171

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.metaobjects.loader.MetaDataLoader
88
import com.metaobjects.`object`.MetaObject
99
import com.metaobjects.relationship.CompositionRelationship
1010
import com.metaobjects.relationship.MetaRelationship
11-
import com.metaobjects.source.RdbSource
1211
import com.squareup.kotlinpoet.LONG
1312
import com.squareup.kotlinpoet.TypeName
1413
import java.io.OutputStream
@@ -68,7 +67,8 @@ open class KotlinRelationsGenerator : MultiFileDirectGeneratorBase<MetaObject>()
6867
// Abstract entities are inheritance scaffolding — never emit relation helpers.
6968
if (KotlinGenUtil.isAbstractEntity(entity)) continue
7069
// No source.rdb → no persistence layer → no FK column to query against.
71-
entity.children.filterIsInstance<RdbSource>().firstOrNull() ?: continue
70+
// ADR-0039: resolving (an inherited source.rdb still means the entity is persisted).
71+
if (!KotlinGenUtil.hasRdbSource(entity)) continue
7272

7373
val manyRels = entity.children
7474
.filterIsInstance<MetaRelationship>()

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ open class KotlinSpringControllerGenerator : MultiFileDirectGeneratorBase<MetaOb
9191
// FR-017 TPH: a subtype is folded into its base's single table + base controller (it
9292
// also carries no own source.rdb, so the guard below would skip it too).
9393
if (KotlinTphPlan.isTphSubtype(entity)) continue
94-
val sourceRdb = entity.children.filterIsInstance<RdbSource>().firstOrNull() ?: continue
94+
// ADR-0039: resolving source lookup (inherited source.rdb via extends).
95+
val sourceRdb = KotlinGenUtil.firstRdbSource(entity) ?: continue
9596
val kind = sourceRdb.effectiveKind
9697
// Only writable tables get a CRUD controller. View / materializedView are
9798
// read-only (would need a different controller shape — list + get only);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ open class KotlinStoredProcGenerator : MultiFileDirectGeneratorBase<MetaObject>(
7676
entity.subType != MetaObject.SUBTYPE_PROJECTION) continue
7777
// Abstract entities are inheritance scaffolding — never emit a stored-proc binding.
7878
if (KotlinGenUtil.isAbstractEntity(entity)) continue
79-
val sourceRdb = entity.children.filterIsInstance<RdbSource>().firstOrNull() ?: continue
79+
// ADR-0039: resolving source lookup (inherited source.rdb via extends).
80+
val sourceRdb = KotlinGenUtil.firstRdbSource(entity) ?: continue
8081
if (sourceRdb.effectiveKind != MetaSource.KIND_STORED_PROC) continue
8182
emit(entity, sourceRdb, outRoot)
8283
}

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,8 @@ object KotlinTypeMapper {
321321
* `@valueType` / `@objectRef` is set and that `@valueType` names a scalar subtype.
322322
*/
323323
fun mapValueScalarTypeName(field: MapField): TypeName? =
324-
when (stringAttr(field, MapField.ATTR_VALUE_TYPE)) {
324+
// ADR-0039: @valueType is an effective property — resolve through extends.
325+
when (stringAttr(field, MapField.ATTR_VALUE_TYPE, includeParent = true)) {
325326
StringField.SUBTYPE_STRING -> STRING
326327
IntegerField.SUBTYPE_INT -> INT
327328
LongField.SUBTYPE_LONG -> LONG
@@ -583,8 +584,10 @@ object KotlinTypeMapper {
583584
* [includeParent] = true also walks the `extends` super-field chain). Returns null when
584585
* the attribute is absent, throws during lookup, or isn't a [com.metaobjects.attr.MetaAttribute].
585586
* Used for non-typed dispatch keys (e.g. `@dbColumnType`, `@valueType`) read off a field.
587+
* ADR-0039: [includeParent] defaults to true (resolving is the default); pass false only
588+
* for the rare own-only case.
586589
*/
587-
private fun stringAttr(field: MetaField<*>, name: String, includeParent: Boolean = false): String? {
590+
private fun stringAttr(field: MetaField<*>, name: String, includeParent: Boolean = true): String? {
588591
if (!field.hasMetaAttr(name, includeParent)) return null
589592
val attr = runCatching {
590593
field.getMetaAttr(name, includeParent)
@@ -610,10 +613,15 @@ object KotlinTypeMapper {
610613
private fun decimalScale(field: DecimalField): Int =
611614
intAttr(field, DecimalField.ATTR_SCALE) ?: DECIMAL_DEFAULT_SCALE
612615

613-
/** Best-effort read of a named int-valued attribute (own-only) on [field]; null when absent/unparseable. */
616+
/**
617+
* Best-effort read of a named int-valued attribute on [field], resolved THROUGH the
618+
* `extends` super-field chain (own value wins); null when absent/unparseable. ADR-0039:
619+
* `@precision`/`@scale` are effective properties — a concrete field extending an abstract
620+
* decimal must inherit them (consistent with [stringMaxLengthOrNull] which resolves).
621+
*/
614622
private fun intAttr(field: MetaField<*>, name: String): Int? {
615-
if (!field.hasMetaAttr(name, false)) return null
616-
val raw = runCatching { field.getMetaAttr(name, false).value }.getOrNull()
623+
if (!field.hasMetaAttr(name, true)) return null
624+
val raw = runCatching { field.getMetaAttr(name, true).value }.getOrNull()
617625
return when (raw) {
618626
is Number -> raw.toInt()
619627
is String -> raw.toIntOrNull()

0 commit comments

Comments
 (0)