11package com.metaobjects.generator.kotlin
22
3- import com.metaobjects.field.MetaField
3+ import com.metaobjects.database.CoreDBMetaDataProvider
44import com.metaobjects.field.ObjectField
55import com.metaobjects.generator.GeneratorIOWriter
66import com.metaobjects.generator.direct.MultiFileDirectGeneratorBase
77import com.metaobjects.identity.MetaIdentity
88import com.metaobjects.loader.MetaDataLoader
99import com.metaobjects.`object`.MetaObject
10+ import com.metaobjects.relationship.CompositionRelationship
1011import com.metaobjects.relationship.MetaRelationship
11- import com.metaobjects.source.MetaSource
1212import com.metaobjects.source.RdbSource
1313import java.io.OutputStream
1414import java.io.PrintWriter
@@ -39,20 +39,22 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
3939 parseArgs()
4040 val outRoot = Paths .get(outDir.absolutePath)
4141 for (entity in loader.metaObjects) {
42- if (entity.subType != " entity " ) continue
43- val sourceRdb = findRdbSource( entity) ? : continue
42+ if (entity.subType != MetaObject . SUBTYPE_ENTITY ) continue
43+ val sourceRdb = entity.children.filterIsInstance< RdbSource >().firstOrNull( ) ? : continue
4444 emit(entity, sourceRdb, outRoot, loader)
4545 }
4646 }
4747
48- private fun emit (entity : MetaObject , sourceRdb : MetaSource , outRoot : Path , loader : MetaDataLoader ) {
48+ private fun emit (entity : MetaObject , sourceRdb : RdbSource , outRoot : Path , loader : MetaDataLoader ) {
4949 val (pkg, shortName) = PackageMapping .splitFqn(entity.name)
5050 val tableObjectName = shortName + " Table"
5151 val tableName = sourceRdb.tableName ? : (shortName.lowercase() + " s" )
5252
53- val primary = findPrimaryIdentity(entity)
53+ val primary = entity.children
54+ .filterIsInstance<MetaIdentity >()
55+ .firstOrNull { it.isPrimary }
5456 val primaryFieldName = primary?.fields?.firstOrNull()
55- val incrementPk = primary?.generation == MetaIdentity . GENERATION_INCREMENT
57+ val incrementPk = primary?.isIncrement == true
5658
5759 val fkColumns = buildFkColumns(entity, loader)
5860 val objectColumns = buildObjectColumns(entity, primaryFieldName, loader)
@@ -74,13 +76,11 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
7476 append(" /** GENERATED — do not hand-edit. Regenerated from metadata. */\n " )
7577 append(" object $tableObjectName : Table(\" $tableName \" ) {\n " )
7678 for (field in entity.metaFields) {
77- if (field is ObjectField ) {
78- // ObjectField columns are produced by buildObjectColumns() so we can
79- // emit @storage flattened (N columns) or jsonb (1 column) uniformly.
80- continue
81- }
79+ // ObjectField columns are produced by buildObjectColumns() so we can emit
80+ // @storage flattened (N columns) or jsonb (1 column) uniformly.
81+ if (field is ObjectField ) continue
8282 val isPk = field.name == primaryFieldName
83- val nullable = ! isPk && ! isRequired (field)
83+ val nullable = ! isPk && ! KotlinGenUtil .isRequiredField (field)
8484 val baseSpec = KotlinTypeMapper .exposedColumnSpec(field)
8585 val withAuto = if (isPk && incrementPk) " $baseSpec .autoIncrement()" else baseSpec
8686 val full = if (nullable) " $withAuto .nullable()" else withAuto
@@ -138,25 +138,23 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
138138 for (field in entity.metaFields) {
139139 if (field !is ObjectField ) continue
140140 val parentName = field.name
141+ val parentNullable = parentName != primaryFieldName && ! KotlinGenUtil .isRequiredField(field)
141142 val storage = readStorage(field) // null → default to jsonb
142143 if (storage == STORAGE_FLATTENED ) {
143144 val ref = readObjectRef(field) ? : continue
144- val target = resolveObjectByShortOrFqn(loader, ref) ? : continue
145- val parentNullable = parentName != primaryFieldName && ! isRequired(field)
145+ val target = KotlinGenUtil .resolveObjectByShortOrFqn(loader, ref) ? : continue
146146 for (subField in target.metaFields) {
147147 val propertyName = parentName + subField.name.replaceFirstChar { it.uppercase() }
148148 val colName = parentName + " _" + subField.name
149149 val baseSpec = KotlinTypeMapper .exposedColumnSpec(subField, colName)
150150 // Sub-column is nullable iff the parent is nullable OR the sub-field itself is.
151- val nullable = parentNullable || ! isRequired (subField)
151+ val nullable = parentNullable || ! KotlinGenUtil .isRequiredField (subField)
152152 val full = if (nullable) " $baseSpec .nullable()" else baseSpec
153153 result.add(ObjectColumnSpec (propertyName, full, ObjectColumnKind .FLATTENED ))
154154 }
155155 } else {
156156 // jsonb (explicit) OR absent (default per CLAUDE.md back-compat rule).
157- val colName = parentName
158- val expr = " jsonb(\" $colName \" , { Json.encodeToString(it) }, { Json.decodeFromString(it) })"
159- val parentNullable = parentName != primaryFieldName && ! isRequired(field)
157+ val expr = " jsonb(\" $parentName \" , { Json.encodeToString(it) }, { Json.decodeFromString(it) })"
160158 val full = if (parentNullable) " $expr .nullable()" else expr
161159 result.add(ObjectColumnSpec (parentName, full, ObjectColumnKind .JSONB ))
162160 }
@@ -177,15 +175,6 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
177175 .getOrNull()
178176 }
179177
180- /* * Resolve a MetaObject (entity OR value) by FQN match or short-name match. */
181- private fun resolveObjectByShortOrFqn (loader : MetaDataLoader , ref : String ): MetaObject ? {
182- for (child in loader.metaObjects) {
183- val short = child.name.substringAfterLast(" ::" )
184- if (child.name == ref || short == ref) return child
185- }
186- return null
187- }
188-
189178 private companion object {
190179 /* * Cross-language @storage attr on field.object — values: flattened | jsonb (default). */
191180 const val ATTR_STORAGE = " storage"
@@ -223,53 +212,39 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
223212 val result = mutableListOf<FkColumnSpec >()
224213 for (child in entity.children) {
225214 if (child !is MetaRelationship ) continue
226- if (child.subType != " composition " ) continue
215+ if (child.subType != CompositionRelationship . SUBTYPE_COMPOSITION ) continue
227216 // Skip the "many" side — FK lives on the other entity.
228217 if (child.cardinality == MetaRelationship .CARDINALITY_MANY ) continue
229218
230219 val objectRef = child.objectRef ? : continue
231- val targetShortName = resolveTargetShortName(objectRef, loader ) ? : continue
232- val targetTable = targetShortName + " Table"
220+ val target = KotlinGenUtil .resolveObjectByShortOrFqn(loader, objectRef ) ? : continue
221+ val targetTable = PackageMapping .splitFqn(target.name).second + " Table"
233222
234- // Use shortName: relationship.name is fully-qualified after loading
235- // (e.g., "acme::demo::author"), which would produce an illegal Kotlin
236- // identifier with "::" embedded. shortName is the leaf authored token.
223+ // Use shortName: relationship.name is fully-qualified after loading (e.g.
224+ // "acme::demo::author"), which would produce an illegal Kotlin identifier with
225+ // "::" embedded. shortName is the leaf authored token.
237226 val relShortName = child.shortName ? : child.name
238- val propertyName = if (child.hasMetaAttr(com.metaobjects.database.CoreDBMetaDataProvider .COLUMN , true )) {
239- runCatching { child.getMetaAttr(com.metaobjects.database.CoreDBMetaDataProvider .COLUMN , true ).valueAsString }
240- .getOrNull() ? : (relShortName + " Id" )
241- } else {
242- relShortName + " Id"
243- }
227+ val propertyName = readColumnAttr(child) ? : (relShortName + " Id" )
244228
245229 // Default Exposed FK is `long(...)`; refine later if other PK types appear.
246- val parts = StringBuilder (" long(\" $propertyName \" ).references($targetTable .id" )
247- var hasOption = false
230+ val refParts = mutableListOf<String >()
248231 val onDelete = child.onDeleteRaw?.let { mapReferentialAction(it) }
249232 val onUpdate = child.onUpdateRaw?.let { mapReferentialAction(it) }
250- if (onDelete != null ) {
251- parts.append(" , onDelete = ReferenceOption.$onDelete " )
252- hasOption = true
253- }
254- if (onUpdate != null ) {
255- parts.append(" , onUpdate = ReferenceOption.$onUpdate " )
256- hasOption = true
257- }
258- parts.append(' )' )
259- result.add(FkColumnSpec (propertyName, parts.toString(), hasOption))
233+ if (onDelete != null ) refParts + = " onDelete = ReferenceOption.$onDelete "
234+ if (onUpdate != null ) refParts + = " onUpdate = ReferenceOption.$onUpdate "
235+
236+ val refArgs = if (refParts.isEmpty()) " " else " , " + refParts.joinToString(" , " )
237+ val expr = " long(\" $propertyName \" ).references($targetTable .id$refArgs )"
238+ result.add(FkColumnSpec (propertyName, expr, hasReferenceOption = refParts.isNotEmpty()))
260239 }
261240 return result
262241 }
263242
264- /* * Resolve `@objectRef` (short name OR fully-qualified) to the target entity's short name. */
265- private fun resolveTargetShortName (objectRef : String , loader : MetaDataLoader ): String? {
266- // Try exact match first (FQN), then by short name across all entities.
267- val direct = loader.metaObjects.firstOrNull { it.name == objectRef }
268- if (direct != null ) return PackageMapping .splitFqn(direct.name).second
269- val byShortName = loader.metaObjects.firstOrNull {
270- PackageMapping .splitFqn(it.name).second == objectRef
271- }
272- return byShortName?.let { PackageMapping .splitFqn(it.name).second }
243+ /* * Read the `@column` attr on a relationship (inheritance allowed); null when absent. */
244+ private fun readColumnAttr (rel : MetaRelationship ): String? {
245+ if (! rel.hasMetaAttr(CoreDBMetaDataProvider .COLUMN , true )) return null
246+ return runCatching { rel.getMetaAttr(CoreDBMetaDataProvider .COLUMN , true ).valueAsString }
247+ .getOrNull()
273248 }
274249
275250 /* *
@@ -287,26 +262,6 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
287262 else -> null
288263 }
289264
290- private fun findRdbSource (entity : MetaObject ): MetaSource ? =
291- entity.children.firstOrNull { it is RdbSource } as ? MetaSource
292-
293- private fun findPrimaryIdentity (entity : MetaObject ): MetaIdentity ? {
294- for (child in entity.children) {
295- if (child is MetaIdentity && child.subType == " primary" ) return child
296- }
297- return null
298- }
299-
300- private fun isRequired (field : MetaField <* >): Boolean {
301- if (! field.hasMetaAttr(MetaField .ATTR_REQUIRED , true )) return false
302- val raw = runCatching { field.getMetaAttr(MetaField .ATTR_REQUIRED , true ).value }.getOrNull()
303- return when (raw) {
304- is Boolean -> raw
305- is String -> raw.equals(" true" , ignoreCase = true )
306- else -> false
307- }
308- }
309-
310265 // === MultiFileDirectGeneratorBase abstract-method stubs ====================
311266 override fun writeSingleFile (md : MetaObject , writer : GeneratorIOWriter <* >? ) { /* unused */ }
312267 override fun <T : GeneratorIOWriter <* >? > getSingleWriter (
0 commit comments