Skip to content

fix(#6): narrow lastInsertId to string or null - #17

Merged
ssilvius merged 2 commits into
mainfrom
fix/6-lastinsertid-string
Aug 3, 2026
Merged

fix(#6): narrow lastInsertId to string or null#17
ssilvius merged 2 commits into
mainfrom
fix/6-lastinsertid-string

Conversation

@ssilvius

@ssilvius ssilvius commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

lastInsertId was typed as string | number | null, and the number arm is a precision trap: a 64-bit auto-increment rowid emitted as a JSON number silently loses fidelity in any client whose numbers are IEEE-754 doubles, which is most of them. This narrows the field to string-or-null across the three places it is specified or produced — the normative bullet in SPEC.md section 6.1, the D1 worker reference server, and the conformance suite's "nice to have" entry — with integer ids encoded as decimal strings exactly the way section 5 already encodes bigint. Nothing in the repo relied on the number arm, so the change is free today and would not be once third-party servers ship.

Acceptance criteria mapping

1. SPEC.md narrows lastInsertId to string-or-null

The section 6.1 field bullet now reads "OPTIONAL, string or null" and states explicitly that the value is never a JSON number, giving the reason (64-bit ids vs. IEEE-754 doubles) and pinning the integer encoding to a decimal string "consistent with the bigint encoding in section 5". The decimal rule is deliberately scoped to the integer case: the spec's own examples and the conformance fixture use a TEXT primary key, so a blanket "always a decimal integer" rule would have falsified them. Text ids are stated to pass through as-is. The surrounding JSON examples needed no edit — SPEC.md:113 already shows null and SPEC.md:131-132 already show "1" / "2".

Evidence: SPEC.md:120, the lastInsertId bullet under section 6.1; git grep -n lastInsertId SPEC.md returns no remaining occurrence of the number arm.

2. The D1 worker example stringifies

projectD1Result in the Cloudflare Worker-to-D1 reference server now converts D1's numeric meta.last_row_id to a string, and its local StatementResult interface narrows to string | null so the compiler enforces it. The conversion is written as an explicit null check before String(...) rather than String(x ?? null), which would have produced the literal string "null" whenever D1 reported no rowid.

Evidence: examples/cloudflare-worker-to-d1/src/index.ts:24 (interface field) and the lastInsertId: lastRowId === undefined || lastRowId === null ? null : String(lastRowId) line inside projectD1Result.

3. Conformance "nice to have" entry expects the string form

The optional-behaviour bullet for lastInsertId now states the wire form as well as the population rule: a JSON string (integer ids as decimal strings) or null, never a JSON number. It stays in the optional section rather than becoming a numbered mandatory case, since populating the field at all remains optional; only its encoding-when-present is now pinned.

Evidence: the lastInsertId bullet under the "## Optional / "nice to have"" heading in conformance/README.md.

Not done

Three things left out on purpose. First, D1 sets meta.last_row_id to 0 on SELECT/UPDATE/DELETE, not only on INSERT, so the worker now emits "0" where it previously emitted 0 — the same over-reporting, differently typed. Mapping that to null is a behaviour change beyond "stringifies" and would deserve its own issue and its own spec sentence about when the field is meaningful. Second, the remaining string | number | null declarations in examples/cloudflare-durable-object/src/index.ts:69, examples/reference-server.ts:16, and examples/reference-client.ts:11 are untouched: they are type declarations only, no runtime path in them emits a number (the Durable Object hardcodes lastInsertId: null, verified at line 118), and the issue scoped the change to the D1 worker. Third, no typecheck was run: the Worker example has a package.json with only dev/deploy scripts and no installed dependencies, so tsc would fail on missing @cloudflare/workers-types rather than on this diff; the two edited lines were reviewed by hand instead.

Closes #6

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 <noreply@anthropic.com>
@ssilvius
ssilvius merged commit aab02d5 into main Aug 3, 2026
1 check passed
@ssilvius
ssilvius deleted the fix/6-lastinsertid-string branch August 3, 2026 05:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P2: lastInsertId string|number union loses 64-bit ids -- narrow to string

1 participant