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), }; }