Skip to content

INT64 as a number, for the program that asked for it - #6

Merged
tamnd merged 1 commit into
mainfrom
bigint-mode
Aug 18, 2026
Merged

tamnd merged 1 commit into
mainfrom
bigint-mode

Conversation

@tamnd

@tamnd tamnd commented Aug 18, 2026

Copy link
Copy Markdown
Owner

bigIntMode says how INT64 is spelled on the way out. It goes on a statement, or on a connection for all of its statements, and a statement on a connection that named one may still name the other, which is the direction that matters: the one query that counts something large should not need a connection of its own.

const conn = await connect('social.zu1', { bigIntMode: 'number' })
const rows = await conn.query<{ id: number }>('MATCH (p:Person) RETURN p.id AS id')
JSON.stringify(rows) // works, which it does not with bigints in it

const exact = await conn.query('MATCH (p:Person) RETURN count(*) AS n', null, { bigIntMode: 'bigint' })

The default does not move. It is bigint everywhere, because zu's integers go to 2^63 and a JavaScript number stops telling one integer from the next at 2^53, and a client that returns a number there is a client whose users file "the id came back wrong" a year later.

Two things are usually behind the ask. JSON.stringify throws on a bigint, so a row holding one cannot be handed straight to a response, and arithmetic on a bigint will not mix with a number, so every + in the reporting code needs a conversion first. Numbers are also a little cheaper to make, since a double is not an allocation: on 50k rows here one INT64 column costs about 190ns a row as numbers against about 220ns as bigints.

What is traded for that is the reason this is never the default, and it is written down in the type, in the README and in the doc comment rather than left for somebody to find. Which integers a database holds is a property of the data and not of the program, so a query that returned numbers for every row of a test database is a query that can meet a larger one in production. This client refuses that row rather than rounding it: an integer past 2^53 raises a ZuUsageError naming the column and the value, which turns a wrong answer into a failure that says exactly which column to widen. It is still a failure that arrives at read time on a machine that is not yours, and that is the honest description of the mode.

The mode reaches the INT64 columns of a result and nothing else. A node's offset, an edge's src, dst and ord, and the nanosecond counts on the temporal classes stay bigint in both modes, because they are properties of classes the addon registers once rather than values a statement can respell. That is a documented edge and there is a test that pins it.

Inside, the table names a result carries and the spelling of its integers are now one Shape, settled once where the connection is held and read by every value of every row, so the streamed path and the whole-result path get the mode from the same place. A mode nobody can spell is read before the database is opened, so a typo in the connect options does not leave a database behind that nobody asked for, and there is a test for that too.

Ten new tests, seventy-nine in the suite, tsc and attw clean on all four resolutions, and the install round trip runs a statement out of the packed tarballs in both formats.

Milestone DX3, tamnd/zu#169.

`bigIntMode` says how INT64 is spelled on the way out, on a statement
or on a connection, and a statement on a connection that named one may
name the other. The default does not move: it is `bigint`, everywhere,
because zu's integers go to 2^63 and a JavaScript number stops telling
one integer from the next at 2^53.

An integer past that is refused rather than rounded. The refusal is a
ZuUsageError naming the column and the value, so a program that guessed
wrong about its own data gets a failure it can act on instead of an
answer that is quietly off by one. That is the whole hazard, stated in
the types, in the README and in the doc comment: which integers a
database holds is a property of the data and not of the program, so the
failure arrives at read time on somebody else's machine.

The mode reaches the INT64 columns of a result and nothing else. A
node's offset, an edge's src, dst and ord, and the nanosecond counts on
the temporal classes stay `bigint`, since they are properties of
classes the addon registers once rather than values a statement can
respell.

Ten tests, covering both places the mode is named, the override in
either direction, the edge of the exact range, the refusal inside a
list and inside a record, a stream that spells its rows the same way,
and a mode nobody can spell, which is refused at connect time before a
database is created. On 50k rows one INT64 column costs about 190ns a
row as numbers against about 220ns as bigints.
@tamnd
tamnd merged commit 16a4ade into main Aug 18, 2026
9 checks passed
@tamnd
tamnd deleted the bigint-mode branch August 18, 2026 14:04
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.

1 participant