Skip to content

Commit 7d2a4fc

Browse files
dmealingclaude
andcommitted
fix(migrate-ts): R6 — collapse real4→real for SQLite (no phantom float diff)
SQLite has one float storage class; the real4/real (REAL vs DOUBLE PRECISION) distinction is Postgres-only. Normalize real4→real in the SQLite dialect pass so a field.float column round-trips without a spurious change-column-type diff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4a94f41 commit 7d2a4fc

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

server/typescript/packages/migrate-ts/src/expected-schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ function normalizeForSqlite(sqlType: SqlType): SqlType {
148148
// plain "INTEGER" regardless of source bit-width. Collapse 32 → 64 so the
149149
// expected snapshot matches what introspection sees.
150150
return { kind: "integer", bits: 64 };
151+
case "real4":
152+
// SQLite has a single float storage class ("REAL"); it cannot distinguish
153+
// single-precision (real4 / field.float) from double-precision (real /
154+
// field.double). Collapse real4 → real so the expected snapshot matches
155+
// what the SQLite introspector produces, preventing a phantom
156+
// change-column-type diff on every field.float column.
157+
return { kind: "real" };
151158
default:
152159
return sqlType;
153160
}

server/typescript/packages/migrate-ts/test/unit/expected-schema.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,77 @@ describe("buildExpectedSchema — indexes + FKs", () => {
134134
});
135135
});
136136

137+
describe("buildExpectedSchema — SQLite float normalization (R6)", () => {
138+
async function loadInline(children: unknown[]) {
139+
const json = JSON.stringify({ "metadata.root": { package: "test", children } });
140+
const result = await new MetaDataLoader().load([new InMemoryStringSource(json)]);
141+
if (result.errors.length > 0) {
142+
throw new Error(`Loader errors:\n${result.errors.map((e) => e.message).join("\n")}`);
143+
}
144+
return result.root;
145+
}
146+
147+
test("field.float → sqlType {kind:'real'} under sqlite (real4 collapsed, no phantom diff)", async () => {
148+
const root = await loadInline([
149+
{
150+
"object.entity": {
151+
name: "Measurement",
152+
children: [
153+
{ "source.rdb": { "@table": "measurements" } },
154+
{ "field.long": { name: "id" } },
155+
{ "field.float": { name: "score" } },
156+
{ "identity.primary": { "@fields": "id" } },
157+
],
158+
},
159+
},
160+
]);
161+
const snapshot = buildExpectedSchema(root, { dialect: "sqlite" });
162+
const scoreCol = snapshot.tables[0]?.columns.find((c) => c.name === "score");
163+
// SQLite has one float storage class; real4 must be collapsed to real so the
164+
// expected schema matches what the SQLite introspector produces ({kind:"real"}).
165+
expect(scoreCol?.sqlType).toEqual({ kind: "real" });
166+
});
167+
168+
test("field.double → sqlType {kind:'real'} under sqlite (already real, unaffected)", async () => {
169+
const root = await loadInline([
170+
{
171+
"object.entity": {
172+
name: "Measurement",
173+
children: [
174+
{ "source.rdb": { "@table": "measurements" } },
175+
{ "field.long": { name: "id" } },
176+
{ "field.double": { name: "value" } },
177+
{ "identity.primary": { "@fields": "id" } },
178+
],
179+
},
180+
},
181+
]);
182+
const snapshot = buildExpectedSchema(root, { dialect: "sqlite" });
183+
const valueCol = snapshot.tables[0]?.columns.find((c) => c.name === "value");
184+
expect(valueCol?.sqlType).toEqual({ kind: "real" });
185+
});
186+
187+
test("field.float → sqlType {kind:'real4'} under postgres (Postgres fidelity preserved)", async () => {
188+
const root = await loadInline([
189+
{
190+
"object.entity": {
191+
name: "Measurement",
192+
children: [
193+
{ "source.rdb": { "@table": "measurements" } },
194+
{ "field.long": { name: "id" } },
195+
{ "field.float": { name: "score" } },
196+
{ "identity.primary": { "@fields": "id" } },
197+
],
198+
},
199+
},
200+
]);
201+
const snapshot = buildExpectedSchema(root, { dialect: "postgres" });
202+
const scoreCol = snapshot.tables[0]?.columns.find((c) => c.name === "score");
203+
// Postgres can distinguish REAL (single) from DOUBLE PRECISION; real4 must not be collapsed.
204+
expect(scoreCol?.sqlType).toEqual({ kind: "real4" });
205+
});
206+
});
207+
137208
describe("buildExpectedSchema — identity.reference @enforce", () => {
138209
async function loadInline(children: unknown[]) {
139210
const json = JSON.stringify({ "metadata.root": { package: "test", children } });

0 commit comments

Comments
 (0)