Skip to content

Commit 01d5eb5

Browse files
dmealingclaude
andcommitted
feat(csharp-migrate): R6 — field.float emits REAL; stringify REAL/DOUBLE on the wire
Add SqlType.Real4 (REAL / float4) distinct from SqlType.Real (DOUBLE PRECISION / float8). Route field.float → Real4 and field.double → Real in ExpectedSchema. Split PostgresIntrospect so float4/real → Real4 and float8/double precision → Real. PostgresEmit renders Real4 as REAL. Normalization.CanonicalFloat stringifies REAL and DOUBLE values as plain-decimal strings (throws on exponential notation), matching the normalization.md contract and the TS reference port. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9a53b69 commit 01d5eb5

8 files changed

Lines changed: 34 additions & 7 deletions

File tree

server/csharp/MetaObjects.Codegen.Tests/Migrate/PostgresEmitTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ public void PgType_renders_each_kind()
125125
Assert.Contains("VARCHAR(50)", T(new SqlType.Text(50)));
126126
Assert.Contains("INTEGER;", T(new SqlType.Integer(32)));
127127
Assert.Contains("DOUBLE PRECISION", T(new SqlType.Real()));
128+
Assert.Contains("REAL;", T(new SqlType.Real4()));
128129
Assert.Contains("NUMERIC(10,2)", T(new SqlType.Numeric(10, 2)));
129130
Assert.Contains("NUMERIC(8)", T(new SqlType.Numeric(8)));
130131
Assert.Contains("NUMERIC;", T(new SqlType.Numeric()));

server/csharp/MetaObjects.Codegen.Tests/Migrate/PostgresIntrospectTests.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,15 @@ public void PgTypeToSqlType_integers(string dataType, int bits)
6565
[Theory]
6666
[InlineData("real")]
6767
[InlineData("float4")]
68+
public void PgTypeToSqlType_float4_maps_to_real4(string dataType)
69+
{
70+
Assert.Equal(new SqlType.Real4(), PostgresIntrospect.PgTypeToSqlType(dataType));
71+
}
72+
73+
[Theory]
6874
[InlineData("double precision")]
6975
[InlineData("float8")]
70-
public void PgTypeToSqlType_real(string dataType)
76+
public void PgTypeToSqlType_float8_maps_to_real(string dataType)
7177
{
7278
Assert.Equal(new SqlType.Real(), PostgresIntrospect.PgTypeToSqlType(dataType));
7379
}

server/csharp/MetaObjects.Codegen.Tests/Migrate/SqlTypeTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public void Record_equality_is_structural()
1414
Assert.Equal(new SqlType.Integer(64), new SqlType.Integer(64));
1515
Assert.NotEqual<SqlType>(new SqlType.Integer(32), new SqlType.Integer(64));
1616
Assert.Equal<SqlType>(new SqlType.Real(), new SqlType.Real());
17+
Assert.Equal<SqlType>(new SqlType.Real4(), new SqlType.Real4());
18+
Assert.NotEqual<SqlType>(new SqlType.Real(), new SqlType.Real4()); // float8 ≠ float4
1719
Assert.NotEqual<SqlType>(new SqlType.Timestamp(true), new SqlType.Timestamp(false));
1820
Assert.NotEqual<SqlType>(new SqlType.Integer(32), new SqlType.Real());
1921
}

server/csharp/MetaObjects.Codegen/Migrate/ExpectedSchema.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ private static ColumnDescriptor BuildColumn(
140140
FIELD_SUBTYPE_STRING or FIELD_SUBTYPE_CLASS => new SqlType.Text(f.MaxLength),
141141
FIELD_SUBTYPE_INT or FIELD_SUBTYPE_SHORT or FIELD_SUBTYPE_BYTE => new SqlType.Integer(32),
142142
FIELD_SUBTYPE_LONG or FIELD_SUBTYPE_CURRENCY => new SqlType.Integer(64),
143-
FIELD_SUBTYPE_DOUBLE or FIELD_SUBTYPE_FLOAT => new SqlType.Real(),
143+
FIELD_SUBTYPE_DOUBLE => new SqlType.Real(),
144+
FIELD_SUBTYPE_FLOAT => new SqlType.Real4(),
144145
FIELD_SUBTYPE_DECIMAL => new SqlType.Numeric(f.Precision, f.Scale),
145146
// enum is string-backed → SqlType.Text (PostgresEmit renders as TEXT).
146147
// The DB-side CHECK constraint enforcing membership is tail-appended by

server/csharp/MetaObjects.Codegen/Migrate/PostgresEmit.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ private static string RenderColumn(ColumnDescriptor c)
115115
SqlType.Text x => x.MaxLength is { } n ? $"VARCHAR({n})" : "TEXT",
116116
SqlType.Integer x => x.Bits == 64 ? "BIGINT" : "INTEGER",
117117
SqlType.Real => "DOUBLE PRECISION",
118+
SqlType.Real4 => "REAL",
118119
SqlType.Numeric x => x switch
119120
{
120121
{ Precision: { } p, Scale: { } s } => $"NUMERIC({p},{s})",

server/csharp/MetaObjects.Codegen/Migrate/PostgresIntrospect.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ public static SqlType PgTypeToSqlType(string dataType, long? maxLength = null)
9090
or "int2" or "smallint" or "smallserial")
9191
return new SqlType.Integer(32);
9292

93-
if (dt is "float4" or "real" or "float8" or "double precision") return new SqlType.Real();
93+
if (dt is "float4" or "real") return new SqlType.Real4();
94+
if (dt is "float8" or "double precision") return new SqlType.Real();
9495

9596
var num = NumericInline.Match(dt);
9697
if (num.Success)

server/csharp/MetaObjects.Codegen/Migrate/SqlType.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public abstract record SqlType
1919
{
2020
public sealed record Text(long? MaxLength = null) : SqlType;
2121
public sealed record Integer(int Bits) : SqlType; // 32 | 64
22-
public sealed record Real : SqlType;
22+
public sealed record Real : SqlType; // DOUBLE PRECISION (float8) — field.double
23+
public sealed record Real4 : SqlType; // REAL (float4) — field.float
2324
public sealed record Numeric(long? Precision = null, long? Scale = null) : SqlType;
2425
public sealed record Boolean : SqlType;
2526
public sealed record Timestamp(bool WithTimezone) : SqlType;

server/csharp/MetaObjects.IntegrationTests/Runner/Normalization.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ public static class Normalization
3636
long l => l.ToString(CultureInfo.InvariantCulture),
3737
int i => i,
3838
short s => (int)s,
39-
// Floating
40-
float f => (double)f,
41-
double d => d,
39+
// REAL/DOUBLE → canonical plain-decimal string (format from the native type).
40+
float f => CanonicalFloat((double)f),
41+
double d => CanonicalFloat(d),
4242
// NUMERIC / DECIMAL — canonical decimal string, no trailing zeros.
4343
decimal dec => CanonicalDecimal(dec),
4444
// Strings already canonical.
@@ -71,6 +71,20 @@ private static string CanonicalDecimal(decimal d)
7171
return s;
7272
}
7373

74+
private static string CanonicalFloat(double d)
75+
{
76+
var s = d.ToString(CultureInfo.InvariantCulture);
77+
if (s.Contains('E') || s.Contains('e'))
78+
throw new FormatException(
79+
$"CanonicalFloat: {d} is outside the plain-decimal band (exponential notation); " +
80+
"REAL/DOUBLE fixture values must be in-band dyadic rationals — " +
81+
"see fixtures/persistence-conformance/normalization.md");
82+
if (!s.Contains('.')) return s;
83+
s = s.TrimEnd('0');
84+
if (s.EndsWith('.')) s = s[..^1];
85+
return s;
86+
}
87+
7488
// TIMESTAMP (no TZ) → "YYYY-MM-DDTHH:MM:SS[.fff]" with no trailing zeros and no Z.
7589
// TIMESTAMPTZ comes via DateTimeOffset above and gets a Z; the bare DateTime
7690
// branch never appends one.

0 commit comments

Comments
 (0)