Skip to content

Commit 340074a

Browse files
dmealingclaude
andcommitted
feat(codegen-kotlin): KotlinTypeMapper emits text() column for long strings (kind=text OR maxLength>4000)
StringField.exposedColumnSpec now dispatches to Exposed text(name) when either @kind=text is set or @maxlength exceeds the VARCHAR/TEXT cutoff (4000, the Postgres TOAST boundary). Otherwise behavior is unchanged: varchar(name, N) with N defaulting to 255. - new private constants: VARCHAR_TEXT_THRESHOLD=4000, KIND_TEXT, ATTR_KIND - new helper: stringAttr(field, name) reads an own-only string attribute for dispatch keys outside the registered StringField schema - three new tests on KotlinTypeMapperTest (kind=text -> text, large maxLength -> text, small maxLength -> varchar) 53 prior + 3 new = 56 tests green. Snapshot fixtures unaffected (no existing fixture uses maxLength > 4000). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a707984 commit 340074a

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ object KotlinTypeMapper {
4545
/** Default VARCHAR width for string-backed `field.enum` storage (v1). */
4646
const val ENUM_VARCHAR_LEN = 64
4747

48+
/**
49+
* Threshold beyond which a [StringField]'s `@maxLength` is treated as unbounded text and
50+
* emitted as Exposed `text(name)` rather than `varchar(name, N)`. Chosen at 4000 — the
51+
* customary Postgres inline-VARCHAR cutoff (TOAST boundary); larger values are better
52+
* served by `TEXT`, which Exposed maps to `text(name)`.
53+
*/
54+
private const val VARCHAR_TEXT_THRESHOLD = 4000
55+
56+
/** Sentinel `@kind` value on a [StringField] that forces `text(name)` emission. */
57+
private const val KIND_TEXT = "text"
58+
59+
/** Attribute name read off a [StringField] to dispatch to `text(name)` (kind=text). */
60+
private const val ATTR_KIND = "kind"
61+
4862
/**
4963
* Compute the generated Kotlin enum-class name for an [EnumField] hung off [entity].
5064
*
@@ -105,7 +119,16 @@ object KotlinTypeMapper {
105119
* for nested object.value fields without mutating the underlying MetaField.
106120
*/
107121
fun exposedColumnSpec(field: MetaField<*>, colName: String): String = when (field) {
108-
is StringField -> "varchar(\"$colName\", ${stringMaxLength(field)})"
122+
is StringField -> {
123+
// Dispatch to Exposed `text(name)` when the field is declared as unbounded text:
124+
// (1) explicit `@kind: "text"` opt-in, OR
125+
// (2) `@maxLength` exceeds the VARCHAR/TEXT cutoff (Postgres TOAST boundary).
126+
// Otherwise emit `varchar(name, N)` with N defaulting to 255.
127+
val kind = stringAttr(field, ATTR_KIND)
128+
val maxLen = stringMaxLength(field)
129+
if (kind == KIND_TEXT || maxLen > VARCHAR_TEXT_THRESHOLD) "text(\"$colName\")"
130+
else "varchar(\"$colName\", $maxLen)"
131+
}
109132
is IntegerField -> "integer(\"$colName\")"
110133
is LongField -> "long(\"$colName\")"
111134
is DoubleField -> "double(\"$colName\")"
@@ -127,6 +150,20 @@ object KotlinTypeMapper {
127150
)
128151
}
129152

153+
/**
154+
* Best-effort read of a named string attribute (own-only) on [field]. Returns null when
155+
* the attribute is absent, throws during lookup, or isn't a [com.metaobjects.attr.MetaAttribute].
156+
* Used for non-typed dispatch keys (e.g. `@kind`) that aren't part of the registered
157+
* StringField attribute schema.
158+
*/
159+
private fun stringAttr(field: MetaField<*>, name: String): String? {
160+
if (!field.hasMetaAttr(name, false)) return null
161+
val attr = runCatching {
162+
field.getMetaAttr(name, false)
163+
}.getOrNull() as? com.metaobjects.attr.MetaAttribute<*> ?: return null
164+
return attr.valueAsString
165+
}
166+
130167
/** Resolve @maxLength on a StringField; default 255. */
131168
private fun stringMaxLength(field: StringField): Int {
132169
if (!field.hasMetaAttr(StringField.ATTR_MAX_LENGTH, false)) return 255

server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapperTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.metaobjects.generator.kotlin
22

33
import com.metaobjects.DataTypes
4+
import com.metaobjects.attr.IntAttribute
5+
import com.metaobjects.attr.StringAttribute
46
import com.metaobjects.field.BooleanField
57
import com.metaobjects.field.CurrencyField
68
import com.metaobjects.field.DateField
@@ -151,6 +153,27 @@ class KotlinTypeMapperTest {
151153
assertNull(KotlinTypeMapper.enumTypeName(idField, entity))
152154
}
153155

156+
// === Long-text dispatch (varchar vs text) ===
157+
158+
@Test fun `string with kind text maps to text column`() {
159+
val f = StringField("body")
160+
f.addMetaAttr(StringAttribute.create("kind", "text"))
161+
assertEquals("text(\"body\")", KotlinTypeMapper.exposedColumnSpec(f))
162+
}
163+
164+
@Test fun `string with maxLength over threshold maps to text column`() {
165+
val f = StringField("description")
166+
f.addMetaAttr(IntAttribute.create(StringField.ATTR_MAX_LENGTH, 10000))
167+
assertEquals("text(\"description\")", KotlinTypeMapper.exposedColumnSpec(f))
168+
}
169+
170+
@Test fun `string with no kind and small maxLength maps to varchar`() {
171+
val f = StringField("name")
172+
f.addMetaAttr(IntAttribute.create(StringField.ATTR_MAX_LENGTH, 100))
173+
val spec = KotlinTypeMapper.exposedColumnSpec(f)
174+
assertTrue(spec.contains("varchar") && spec.contains("100"), "got: $spec")
175+
}
176+
154177
@Test fun `uuid field (matched by subtype) maps to java util UUID and uuid exposed column`() {
155178
// `field.uuid` has no dedicated Java class today — mapper matches by subtype name.
156179
// Use a minimal anonymous PrimitiveField with subType="uuid" to drive the path.

0 commit comments

Comments
 (0)