diff --git a/.changeset/fix-d1-export-infinity.md b/.changeset/fix-d1-export-infinity.md new file mode 100644 index 00000000000..27e84c2856e --- /dev/null +++ b/.changeset/fix-d1-export-infinity.md @@ -0,0 +1,5 @@ +--- +"miniflare": patch +--- + +Fix Infinity/-Infinity being emitted as an invalid bare identifier in D1 export diff --git a/packages/miniflare/src/workers/d1/dumpSql.ts b/packages/miniflare/src/workers/d1/dumpSql.ts index da163b3061a..b4420e56fd5 100644 --- a/packages/miniflare/src/workers/d1/dumpSql.ts +++ b/packages/miniflare/src/workers/d1/dumpSql.ts @@ -110,6 +110,9 @@ export function* dumpSql( if (cell === null) { return "NULL"; } else if (cellType === "number") { + if (!Number.isFinite(cell)) { + return (cell as number) > 0 ? "9e999" : "-9e999"; + } return cell; } else if (cellType === "string") { return outputQuotedEscapedString(cell); diff --git a/packages/miniflare/test/plugins/d1/suite.ts b/packages/miniflare/test/plugins/d1/suite.ts index bbd9e4e08fb..ad594c084dc 100644 --- a/packages/miniflare/test/plugins/d1/suite.ts +++ b/packages/miniflare/test/plugins/d1/suite.ts @@ -632,6 +632,73 @@ test("dumpSql exports and imports complete database structure and content correc await isDatabaseEqual(expect, originalDb, mirrorDb); }); +/** + * Test that dumpSql emits a valid SQL literal for non-finite REAL values + * (Infinity / -Infinity), instead of the raw JS Infinity/-Infinity string, + * which SQLite parses back as a bare identifier and fails to re-import. + */ +test("dumpSql exports Infinity and -Infinity as valid, re-importable SQL literals", async ({ + expect, +}) => { + const tmp1 = await useTmp(); + const baseConfig = opts.workers[0].config; + const originalMF = new Miniflare({ + resourcePersistencePath: tmp1, + workers: [ + { config: { ...baseConfig, env: { test: { type: "d1", id: "test" } } } }, + ], + }); + useDispose(originalMF); + const originalDb = await originalMF.getD1Database("test"); + + await originalDb.exec( + `CREATE TABLE inf_test (id INTEGER PRIMARY KEY, r REAL)` + ); + await originalDb.exec( + `INSERT INTO inf_test (id, r) VALUES (1, 9e999), (2, -9e999)` + ); + + const result = await originalDb + .prepare("PRAGMA miniflare_d1_export(?,?,?);") + .bind(0, 0) + .raw(); + const [dumpStatements] = result as [string[]]; + const dump = dumpStatements.join("\n"); + + // The dump must not contain the raw JS Infinity/-Infinity tokens as bare identifiers + expect(dump).not.toMatch(/VALUES\(1,Infinity\)/); + expect(dump).not.toMatch(/VALUES\(2,-Infinity\)/); + + // Re-importing the dump must succeed and preserve the values + const tmp2 = await useTmp(); + const mirrorMF = new Miniflare({ + resourcePersistencePath: tmp2, + workers: [ + { config: { ...baseConfig, env: { test: { type: "d1", id: "test" } } } }, + ], + }); + useDispose(mirrorMF); + const mirrorDb = await mirrorMF.getD1Database("test"); + + // exec(dump) must not throw, If the dump contained the bare `Infinity`/`-Infinity` + // identifiers, SQLite would reject them at parse time and exec would throw + await mirrorDb.exec(dump); + + // Assert the exported SQL actually contains the correctly converted value for each row + expect(dump).toMatch( + /INSERT INTO "inf_test" \("id","r"\) VALUES\(1,9e999\);/ + ); + expect(dump).toMatch( + /INSERT INTO "inf_test" \("id","r"\) VALUES\(2,-9e999\);/ + ); + + // And confirm the import actually succeeded (rows exist post-import) + const rows = await mirrorDb + .prepare(`SELECT id FROM inf_test ORDER BY id`) + .raw(); + expect(rows).toEqual([[1], [2]]); +}); + /** * Populates a D1 database with test data for schema export testing. * Creates tables with various schema features (foreign keys, special characters, etc.)