From 42a02553cb3957820a9a1df04524e7e314cf69bc Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Sun, 2 Aug 2026 22:13:05 -0700 Subject: [PATCH 1/2] fix(spec): narrow lastInsertId to string or null A `string | number` union invites servers to emit 64-bit rowids as JSON numbers, which silently lose precision in clients whose numbers are IEEE-754 doubles. Narrow the field to string-or-null, with integer ids encoded as decimal strings the way section 5 already encodes `bigint`. The D1 worker example stringifies; the conformance nice-to-have entry states the string form. Co-Authored-By: Claude Fable 5 --- SPEC.md | 2 +- conformance/README.md | 2 +- examples/cloudflare-worker-to-d1/src/index.ts | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/SPEC.md b/SPEC.md index e115dab..bddb30a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -117,7 +117,7 @@ HTTP status: `200`. - `columns` (REQUIRED, array of strings) — the column names of the result, in the order produced by the SQL engine. Empty array for statements that produce no result set (INSERT, UPDATE, DELETE, DDL). - `rows` (REQUIRED, array of arrays) — each inner array has the same length as `columns`, with values in column order. Values use the same JSON / tagged-value encoding as section 5. Empty array if no rows. - `rowsAffected` (REQUIRED, integer) — the number of rows changed by the statement. `0` for SELECT. -- `lastInsertId` (OPTIONAL, string, number, or null) — the identifier of the most recently inserted row when the server can determine it (typically the auto-increment id). `null` when not applicable or not available. +- `lastInsertId` (OPTIONAL, string or null) — the identifier of the most recently inserted row when the server can determine it (typically the auto-increment id). Always a string, never a JSON number: when the identifier is an integer it is encoded as a decimal string, consistent with the `bigint` encoding in section 5, so 64-bit ids survive clients whose numbers are IEEE-754 doubles. Text identifiers (a UUID primary key, say) are carried as-is. `null` when not applicable or not available. The arrays-of-arrays shape (not arrays-of-objects) is normative. It keeps payloads compact, makes column order explicit, and supports duplicate column names from joins. diff --git a/conformance/README.md b/conformance/README.md index d4f1811..417729e 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -71,7 +71,7 @@ Conformance is self-asserted. The community can call out failures via issues. ## Optional / "nice to have" - Vendor error codes carry the `vendor:` prefix. -- `lastInsertId` is populated for INSERT statements where the SQL engine reports it. +- `lastInsertId` is populated for INSERT statements where the SQL engine reports it, as a JSON string (integer ids as decimal strings) or `null` — never a JSON number. - Rate-limited responses return `error.code` = `rate_limited` and HTTP 429. - Server enforces a maximum result row count and returns `payload_too_large` past it. diff --git a/examples/cloudflare-worker-to-d1/src/index.ts b/examples/cloudflare-worker-to-d1/src/index.ts index 2918988..e6b22da 100644 --- a/examples/cloudflare-worker-to-d1/src/index.ts +++ b/examples/cloudflare-worker-to-d1/src/index.ts @@ -21,7 +21,7 @@ interface StatementResult { columns: string[]; rows: unknown[][]; rowsAffected: number; - lastInsertId?: string | number | null; + lastInsertId?: string | null; } const VERSION = "0.1"; @@ -107,11 +107,14 @@ async function runBatch(db: D1Database, batch: Statement[], atomic: boolean): Pr function projectD1Result(res: D1Result): StatementResult { const rows = (res.results ?? []) as Record[]; const columns = rows.length > 0 ? Object.keys(rows[0]) : []; + // SPEC 6.1: lastInsertId is a string or null, never a JSON number, so 64-bit + // rowids survive clients whose numbers are IEEE-754 doubles. + const lastRowId = res.meta?.last_row_id; return { columns, rows: rows.map((r) => columns.map((c) => encodeValue(r[c]))), rowsAffected: res.meta?.changes ?? 0, - lastInsertId: res.meta?.last_row_id ?? null, + lastInsertId: lastRowId === undefined || lastRowId === null ? null : String(lastRowId), }; } From f212cb306ec5bd82ae437b103dada3c18c19da95 Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Sun, 2 Aug 2026 22:26:38 -0700 Subject: [PATCH 2/2] chore: retrigger CI (spec-sanity landed after this branch)