Skip to content

Commit fdafa1e

Browse files
dmealingclaude
andcommitted
test(persistence-conformance): R6 — Measurement entity exercises REAL/DOUBLE round-trip
Adds the cross-port end-to-end proof for R6 float fidelity: a Measurement entity (tempC field.float → REAL, massKg field.double → DOUBLE PRECISION) to the shared persistence corpus, plus a float round-trip query scenario. All five ports (TS/C#/Java/Python/Kotlin) create the table and round-trip the values byte-identically against Testcontainers Postgres. - canonical/meta.fitness.json: + Measurement (id, tempC float, massKg double). - queries/measurement-floats.yaml: get + list assert REAL/DOUBLE values round-trip as canonical plain-decimal strings. Seed values (1.5, 0.125, -3.25, 1234.5) are dyadic rationals exact in BOTH float4 and float8, so normalization is byte-identical regardless of a driver's native float width. - migrations/bootstrap-canonical-from-empty.yaml: + measurements table, + base-table row, AND column-type assertions ("tempC" REAL / "massKg" DOUBLE PRECISION). The type assertions are load-bearing: because the seed values are exact in both precisions, the value round-trip alone cannot catch a port that wrongly emits DOUBLE for field.float — the DDL-type check is what gates the REAL vs DOUBLE distinction across the SQL ports. Per-port entity binding (OO ports need a native registration; TS/Java/Python auto-discover from metadata): - C#: Generated/Measurement.g.cs (EF entity, tempC→float/massKg→double) + DbSet. - Kotlin: tables/MeasurementTable.kt (Exposed float()/double()); registered in the query + migration runners; KotlinCodegenMatchesReferenceTest gains a Measurement expectation that verifies the generator emits the float()/double() column families (Kotlin's REAL/ DOUBLE type gate, since it uses Exposed create rather than emitted SQL). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 163ef5a commit fdafa1e

9 files changed

Lines changed: 100 additions & 2 deletions

File tree

fixtures/persistence-conformance/canonical/meta.fitness.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@
2727
{ "identity.reference": { "name": "fkProgram", "@fields": "programId", "@references": "Program" } }
2828
]
2929
}},
30+
{ "object.entity": {
31+
"name": "Measurement",
32+
"children": [
33+
{ "source.rdb": { "@table": "measurements" } },
34+
{ "field.long": { "name": "id" } },
35+
{ "field.float": { "name": "tempC", "@required": true } },
36+
{ "field.double": { "name": "massKg", "@required": true } },
37+
{ "identity.primary": { "@fields": "id", "@generation": "increment" } }
38+
]
39+
}},
3040
{ "object.entity": {
3141
"name": "ProgramView",
3242
"children": [

fixtures/persistence-conformance/migrations/bootstrap-canonical-from-empty.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ expect:
1515
- 'CONSTRAINT "programs_pkey" PRIMARY KEY ("id")'
1616
- 'CREATE TABLE "weeks"'
1717
- 'CONSTRAINT "weeks_pkey" PRIMARY KEY ("id")'
18+
- 'CREATE TABLE "measurements"'
19+
# R6 float fidelity: field.float → REAL (float4), field.double → DOUBLE PRECISION
20+
# (float8). Asserting the column types here (not just the round-trip, which is
21+
# value-identical for in-band dyadic rationals) is what makes this corpus catch a
22+
# REAL→DOUBLE regression.
23+
- '"tempC" REAL'
24+
- '"massKg" DOUBLE PRECISION'
1825
- 'CREATE UNIQUE INDEX "byTitle" ON "programs" ("title");'
1926
- 'ALTER TABLE "weeks" ADD CONSTRAINT "weeks_programId_fk"'
2027
# Match both TS (`CREATE VIEW`) and C# (`CREATE OR REPLACE VIEW`, idempotent)
@@ -29,5 +36,6 @@ expect:
2936
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
3037
ORDER BY table_name
3138
rows:
39+
- { table_name: "measurements" }
3240
- { table_name: "programs" }
3341
- { table_name: "weeks" }
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: measurement-floats
2+
description: |
3+
REAL (field.float, float4) and DOUBLE PRECISION (field.double, float8) round-trip
4+
as canonical plain-decimal strings — no trailing zeros, no exponential notation.
5+
Seed values are in-band dyadic rationals (exactly representable in float4/float8),
6+
so every port must surface byte-identical normalized rows regardless of its driver's
7+
native float type. This is the end-to-end proof of the R6 float-fidelity contract.
8+
seed-data: |
9+
INSERT INTO "measurements" ("id", "tempC", "massKg") VALUES
10+
(1, 1.5, 0.125),
11+
(2, -3.25, 1234.5);
12+
queries:
13+
- name: get-measurement-1
14+
op: get
15+
entity: Measurement
16+
by: { id: 1 }
17+
expect: { id: "1", tempC: "1.5", massKg: "0.125" }
18+
19+
- name: list-measurements-by-id-asc
20+
op: list
21+
entity: Measurement
22+
sort: [{ field: id, dir: asc }]
23+
expect:
24+
- { id: "1", tempC: "1.5", massKg: "0.125" }
25+
- { id: "2", tempC: "-3.25", massKg: "1234.5" }

server/csharp/MetaObjects.IntegrationTests/Generated/AppDbContext.g.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class AppDbContext : DbContext
99
{
1010
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
1111

12+
public DbSet<Measurement> Measurements { get; set; } = default!;
1213
public DbSet<Program> Programs { get; set; } = default!;
1314
public DbSet<ProgramStat> ProgramStats { get; set; } = default!;
1415
public DbSet<ProgramView> ProgramViews { get; set; } = default!;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// <auto-generated/>
2+
// Generated by MetaObjects entity-generator. Do not edit by hand.
3+
#nullable enable
4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel.DataAnnotations;
7+
using System.ComponentModel.DataAnnotations.Schema;
8+
9+
namespace MetaObjects.IntegrationTests.Generated;
10+
11+
[Table("measurements")]
12+
public class Measurement
13+
{
14+
[Key]
15+
[Column("id")]
16+
public long Id { get; set; }
17+
[Column("tempC")]
18+
public float TempC { get; set; }
19+
[Column("massKg")]
20+
public double MassKg { get; set; }
21+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ internal class KotlinCodegenMatchesReferenceTest {
4848
ExpectedFk(columnName = "programId", targetTable = "ProgramTable"),
4949
),
5050
),
51+
// R6 float fidelity: field.float → Exposed `float(...)` (REAL),
52+
// field.double → `double(...)` (DOUBLE PRECISION). Verifies the generator
53+
// emits the distinct single/double-precision column families.
54+
"Measurement" to EntityExpectation(
55+
columns = listOf(
56+
ExpectedColumn("id", families = setOf("long")),
57+
ExpectedColumn("tempC", families = setOf("float")),
58+
ExpectedColumn("massKg", families = setOf("double")),
59+
),
60+
),
5161
"ProgramView" to EntityExpectation(
5262
columns = listOf(
5363
ExpectedColumn("id", families = setOf("long")),

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.metaobjects.integration.kotlin
22

3+
import com.metaobjects.integration.kotlin.tables.MeasurementTable
34
import com.metaobjects.integration.kotlin.tables.ProgramTable
45
import com.metaobjects.integration.kotlin.tables.ProgramV1Table
56
import com.metaobjects.integration.kotlin.tables.ProgramV2Table
@@ -56,7 +57,7 @@ internal class MigrationScenarioConformanceTest {
5657
// 1. Apply the canonical schema via Exposed's full-CREATE path —
5758
// Exposed's analogue of the sibling SchemaMigrationEngine.emit() flow.
5859
transaction(db) {
59-
SchemaUtils.create(ProgramTable, WeekTable)
60+
SchemaUtils.create(ProgramTable, WeekTable, MeasurementTable)
6061
}
6162

6263
// 2. Run the scenario's apply-up-then-query post-condition: it queries

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.metaobjects.integration.kotlin
22

33
import com.metaobjects.integration.kotlin.Scenarios.QueryScenario
44
import com.metaobjects.integration.kotlin.Scenarios.QuerySpec
5+
import com.metaobjects.integration.kotlin.tables.MeasurementTable
56
import com.metaobjects.integration.kotlin.tables.ProgramStatView
67
import com.metaobjects.integration.kotlin.tables.ProgramTable
78
import com.metaobjects.integration.kotlin.tables.WeekTable
@@ -49,7 +50,7 @@ object QueryScenarioRunner {
4950
val db = Database.connect(pg.jdbcUrl, user = pg.username, password = pg.password)
5051

5152
transaction(db) {
52-
SchemaUtils.create(ProgramTable, WeekTable)
53+
SchemaUtils.create(ProgramTable, WeekTable, MeasurementTable)
5354
}
5455

5556
// 2. Seed via the YAML's raw SQL — runs outside Exposed transactions, on a
@@ -109,6 +110,7 @@ object QueryScenarioRunner {
109110
private fun tableFor(entity: String): Table = when (entity) {
110111
"Program" -> ProgramTable
111112
"Week" -> WeekTable
113+
"Measurement" -> MeasurementTable
112114
"ProgramStat" -> ProgramStatView
113115
else -> error("No Exposed Table registered for entity '$entity' — extend QueryScenarioRunner.tableFor")
114116
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.metaobjects.integration.kotlin.tables
2+
3+
import org.jetbrains.exposed.sql.Table
4+
5+
/**
6+
* Hand-written reference Exposed Table mirroring `Measurement` from
7+
* `fixtures/persistence-conformance/canonical/meta.fitness.json`.
8+
*
9+
* - `field.float` → `float` (REAL / float4, single precision)
10+
* - `field.double` → `double` (DOUBLE PRECISION / float8)
11+
*
12+
* The two floating columns are the R6 float-fidelity round-trip subject.
13+
*/
14+
object MeasurementTable : Table("measurements") {
15+
val id = long("id").autoIncrement()
16+
val tempC = float("tempC")
17+
val massKg = double("massKg")
18+
19+
override val primaryKey = PrimaryKey(id)
20+
}

0 commit comments

Comments
 (0)