Skip to content

Commit 163ef5a

Browse files
dmealingclaude
andcommitted
feat(codegen-kotlin): R6 — field.float maps to REAL; stringify REAL/DOUBLE on the wire
Adds a FloatField arm to KotlinTypeMapper so field.float (float4, single precision) maps to Kotlin Float / Exposed float() -> Postgres REAL, distinct from field.double (float8) -> Double / double() -> DOUBLE PRECISION. Previously field.float threw "unsupported". Mirrors the committed TS/C#/Python/Java R6 ports. - KotlinTypeMapper: FloatField + kotlinpoet FLOAT imports; `is FloatField -> FLOAT` in kotlinTypeName and `is FloatField -> float("col")` in exposedColumnSpec. - integration-tests-kotlin Normalization: canonicalFloat stringifies REAL/DOUBLE on the wire and fails loudly on out-of-band (exponential) values — a near-exact mirror of the Java Normalization.canonicalFloatStr (the plan's bare stripZeros predates that cross-port hardening and would silently corrupt exponential strings). - Tests: KotlinTypeMapper FloatField -> FLOAT / float-column arms; canonicalFloat in-band/strip/throw cases mirroring the Java + TS suites. Note: KotlinStoredProcGenerator still lacks a FloatField arm (float as a stored-proc param/result throws) — out of R6 scope, not a regression, and matches the cross-port reference; left as a documented follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4e4a725 commit 163ef5a

4 files changed

Lines changed: 66 additions & 2 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.metaobjects.field.CurrencyField
55
import com.metaobjects.field.DateField
66
import com.metaobjects.field.DoubleField
77
import com.metaobjects.field.EnumField
8+
import com.metaobjects.field.FloatField
89
import com.metaobjects.field.IntegerField
910
import com.metaobjects.field.LongField
1011
import com.metaobjects.field.MetaField
@@ -14,6 +15,7 @@ import com.metaobjects.`object`.MetaObject
1415
import com.squareup.kotlinpoet.BOOLEAN
1516
import com.squareup.kotlinpoet.ClassName
1617
import com.squareup.kotlinpoet.DOUBLE
18+
import com.squareup.kotlinpoet.FLOAT
1719
import com.squareup.kotlinpoet.INT
1820
import com.squareup.kotlinpoet.LONG
1921
import com.squareup.kotlinpoet.STRING
@@ -129,6 +131,9 @@ object KotlinTypeMapper {
129131
is IntegerField -> INT
130132
is LongField -> LONG
131133
is DoubleField -> DOUBLE
134+
// REAL (float4) — distinct single-precision arm so field.float round-trips as
135+
// Kotlin Float / Exposed REAL, separate from field.double (float8). See R6.
136+
is FloatField -> FLOAT
132137
is BooleanField -> BOOLEAN
133138
is DateField -> ClassName("java.time", "LocalDate")
134139
is TimestampField -> ClassName("java.time", "Instant")
@@ -226,6 +231,9 @@ object KotlinTypeMapper {
226231
is IntegerField -> "integer(\"$colName\")"
227232
is LongField -> "long(\"$colName\")"
228233
is DoubleField -> "double(\"$colName\")"
234+
// Exposed `float(name)` maps to Postgres REAL (float4); `double` maps to
235+
// DOUBLE PRECISION (float8). Keeps field.float distinct on the wire. See R6.
236+
is FloatField -> "float(\"$colName\")"
229237
is BooleanField -> "bool(\"$colName\")"
230238
is DateField -> "date(\"$colName\")"
231239
// Default for field.timestamp is plain `timestamp(...)` — Postgres `timestamp

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.metaobjects.field.CurrencyField
88
import com.metaobjects.field.DateField
99
import com.metaobjects.field.DoubleField
1010
import com.metaobjects.field.EnumField
11+
import com.metaobjects.field.FloatField
1112
import com.metaobjects.field.IntegerField
1213
import com.metaobjects.field.LongField
1314
import com.metaobjects.field.PrimitiveField
@@ -17,6 +18,7 @@ import com.metaobjects.metadata.ktx.loadString
1718
import com.squareup.kotlinpoet.BOOLEAN
1819
import com.squareup.kotlinpoet.ClassName
1920
import com.squareup.kotlinpoet.DOUBLE
21+
import com.squareup.kotlinpoet.FLOAT
2022
import com.squareup.kotlinpoet.INT
2123
import com.squareup.kotlinpoet.LONG
2224
import com.squareup.kotlinpoet.STRING
@@ -54,6 +56,11 @@ class KotlinTypeMapperTest {
5456
assertEquals(DOUBLE, KotlinTypeMapper.kotlinTypeName(f))
5557
}
5658

59+
@Test fun `float field maps to Float`() {
60+
val f = FloatField("weight")
61+
assertEquals(FLOAT, KotlinTypeMapper.kotlinTypeName(f))
62+
}
63+
5764
@Test fun `boolean field maps to Boolean`() {
5865
val f = BooleanField("active")
5966
assertEquals(BOOLEAN, KotlinTypeMapper.kotlinTypeName(f))
@@ -101,6 +108,11 @@ class KotlinTypeMapperTest {
101108
assertEquals("double(\"ratio\")", KotlinTypeMapper.exposedColumnSpec(f))
102109
}
103110

111+
@Test fun `float field maps to float exposed column`() {
112+
val f = FloatField("weight")
113+
assertEquals("float(\"weight\")", KotlinTypeMapper.exposedColumnSpec(f))
114+
}
115+
104116
@Test fun `date field maps to date exposed column`() {
105117
val f = DateField("birthday")
106118
assertEquals("date(\"birthday\")", KotlinTypeMapper.exposedColumnSpec(f))

server/java/integration-tests-kotlin/src/test/kotlin/com/metaobjects/integration/kotlin/Normalization.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ object Normalization {
3939
is Int -> v
4040
is Short -> v.toInt()
4141
is Byte -> v.toInt()
42-
is Float -> v.toDouble()
43-
is Double -> v
42+
is Float -> canonicalFloat(v)
43+
is Double -> canonicalFloat(v)
4444
is BigDecimal -> canonicalDecimal(v)
4545
is UUID -> v.toString().lowercase()
4646
is ByteArray -> Base64.getEncoder().encodeToString(v)
@@ -64,6 +64,22 @@ object Normalization {
6464
return if (s.contains(".") || !s.contains("E")) s else BigDecimal(s).toPlainString()
6565
}
6666

67+
/** REAL → canonical plain-decimal string, from the single (no widening tail). */
68+
private fun canonicalFloat(f: Float): String = canonicalFloatStr(f.toString(), f)
69+
/** DOUBLE → canonical plain-decimal string. */
70+
private fun canonicalFloat(d: Double): String = canonicalFloatStr(d.toString(), d)
71+
private fun canonicalFloatStr(s: String, v: Any): String {
72+
require(!s.contains('E') && !s.contains('e')) {
73+
"canonicalFloat: $v is outside the plain-decimal band (exponential notation); " +
74+
"REAL/DOUBLE fixture values must be in-band dyadic rationals — " +
75+
"see fixtures/persistence-conformance/normalization.md"
76+
}
77+
if (!s.contains('.')) return s
78+
var out = s.trimEnd('0')
79+
if (out.endsWith(".")) out = out.dropLast(1)
80+
return out
81+
}
82+
6783
fun canonicalRowsJson(rows: List<Map<String, Any?>>): String {
6884
val normalized = rows.map { normalizeRow(it) }
6985
return String(mapper.writeValueAsBytes(normalized), StandardCharsets.UTF_8)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.metaobjects.integration.kotlin
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFailsWith
6+
7+
/**
8+
* R6: REAL/DOUBLE wire normalization (mirror of the TS `normalization.test.ts` and Java
9+
* `NormalizationFloatTest` cases). [Normalization.normalizeValue] routes Float/Double through
10+
* the canonical-float stringifier: in-band values become plain-decimal strings (trailing zeros
11+
* + bare decimal point stripped); out-of-band (exponential) values fail loudly so a bad fixture
12+
* surfaces immediately instead of silently corrupting the cross-port byte-equality compare.
13+
*/
14+
class NormalizationFloatTest {
15+
@Test fun `in-band float is plain decimal string`() {
16+
assertEquals("1.5", Normalization.normalizeValue(1.5f)) // REAL (float4)
17+
assertEquals("0.25", Normalization.normalizeValue(0.25)) // DOUBLE (float8)
18+
}
19+
@Test fun `integer-valued double strips trailing zero and point`() {
20+
assertEquals("2", Normalization.normalizeValue(2.0)) // "2.0" -> "2." -> "2"
21+
}
22+
@Test fun `out-of-band double throws`() {
23+
assertFailsWith<IllegalArgumentException> { Normalization.normalizeValue(1.0e-10) } // "1.0E-10"
24+
}
25+
@Test fun `out-of-band float throws`() {
26+
assertFailsWith<IllegalArgumentException> { Normalization.normalizeValue(1.0e-10f) } // "1.0E-10"
27+
}
28+
}

0 commit comments

Comments
 (0)