Conversation
zu holds a date as a count of days, a time and a timestamp as counts of
nanoseconds and a duration as a count of months or nanoseconds, and the
four classes this client registers hand those counts over as they are.
That is exact and portable and no help at all to a program that wants
to know what day of the week it was.
A connection opened with `{ temporal: true }` gets `Temporal` values
instead, and `toTemporal()` on each of the four classes converts one
value on any connection. A time carrying an offset is the exception in
both: `Temporal` has no type for one, since a `PlainTime` is local and
a `ZonedDateTime` carries a date nobody wrote, so it stays a `ZuTime`
and everything else in the row still arrives converted.
The option is settled where the connection is opened and cannot be
named per statement, unlike `bigIntMode`. Both spellings are exact and
neither loses anything, so which one a program wants is a property of
the program rather than of the query, and a result whose classes change
halfway through a codebase is a result nobody can write a function
against.
Going the other way needs no opt-in. A `Temporal` value passed as a
parameter binds as the zu value it is on every connection, recognized
by `Symbol.toStringTag`, which is one property read for all eight of
its classes where `instanceof` would be one call each. A
`PlainYearMonth`, a `PlainMonthDay` or a value on a calendar that is
not `iso8601` is refused by name, and so is a `Duration` counting both
months and days, because no number of days is a month and a value
holding both would have to invent an answer for one month after
31 January.
`Temporal` reached Stage 4 in March 2026 and is unflagged in Node 26
and the current browsers, while Node 24 is still the active LTS and has
it behind `--harmony-temporal`, which is the whole reason this is an
opt-in rather than the default. Asking a runtime without it for
`{ temporal: true }` is refused on the way in and before the database
file is opened, so a program that asked for what its runtime cannot do
fails at the line that asked and leaves nothing behind.
The TypeScript types ask `globalThis` whether the compiler has heard of
`Temporal` rather than importing it, because which `lib` declares it is
different in every version of TypeScript that has shipped since Stage 4.
A program that has it is checked against the real types, one that does
not gets `unknown` for a return and nothing at all for a union member,
so the unions it already had are unchanged.
On 50k rows here a DATE column costs about 560ns a row as a `ZuDate`
and about 650ns as a `Temporal.PlainDate`, against 240ns for the INT64
column beside it. Nothing is cached between rows, because a `Temporal`
value is a JavaScript object and the constructors are properties of
one, and neither can be held by a struct that crosses to the threadpool
thread the statement runs on.
33 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
zu holds a date as a count of days from 1970-01-01, a time as nanoseconds from midnight, a timestamp as nanoseconds from the epoch and a duration as a count of months or nanoseconds. The four classes this client registers hand those counts over as they are, which is exact, portable and no help at all to a program that wants to know what day of the week it was.
Temporalis the standard answer to that, so a connection can ask for it:A DATE becomes a
PlainDate, a local TIME aPlainTime, a local TIMESTAMP aPlainDateTime, a zoned TIMESTAMP aZonedDateTimeat the offset it was written at, and a DURATION aDurationcounting months or seconds and nanoseconds. A time carrying an offset is the one value zu holds thatTemporalhas no type for, since aPlainTimeis local and would drop the offset and aZonedDateTimecarries a date nobody wrote, so it stays aZuTimeand everything else in the same row still arrives converted.The option is settled where the connection is opened and cannot be named per statement, unlike
bigIntMode. Both spellings are exact and neither loses anything, so which one a program wants is a property of the program rather than of the query, and a result whose classes changed halfway through a codebase is a result nobody can write a function against. A program that wants one value converted rather than all of them callstoTemporal(), which is on all four classes and needs no option anywhere.Going the other way needs no opt-in at all. A
Temporalvalue passed as a parameter binds as the zu value it is on every connection, whether or not that connection asked forTemporalon the way out, because recognizing one costs a property read and refusing one would be a rule nobody could guess. They are recognized bySymbol.toStringTag, which everyTemporalclass sets to its own name: one property read covers all eight of them whereinstanceofis one call each, and it is the same answerObject.prototype.toStringgives, so a value that prints as aPlainDatebinds as one. APlainYearMonth, aPlainMonthDayor a value on a calendar that is notiso8601is refused by name, since zu holds neither, and so is aDurationcounting both months and days, because no number of days is a month and a value holding both would have to invent an answer for one month after 31 January. A value outside the nanoseconds an i64 holds is refused with the range it left, 1677-09-21 to 2262-04-11.Temporalreached Stage 4 in March 2026 and is unflagged in Node 26 and the current browsers, while Node 24 is still the active LTS and has it behind--harmony-temporal. That is the whole reason this is an opt-in rather than the default: a client that returnedTemporalvalues everywhere would be a client half its users cannot load. Asking a runtime without it for{ temporal: true }is refused on the way in and before the database file is opened, so a program that asked for what its runtime cannot do fails at the line that asked and leaves nothing behind. The check is read on the runtime's own thread and carried into the task, because only that thread may touch a JavaScript value at all.The TypeScript types ask
globalThiswhether the compiler has heard ofTemporalrather than importing it, since whichlibdeclares it is different in every version of TypeScript that has shipped since Stage 4. A program compiling against alibthat has it is checked against the real types. One compiling against alibthat does not getsunknownfor a return type and nothing at all for a union member, soZuValueandZuParamare exactly what they were before this existed and still narrow the way they did.Nothing is cached between rows. A
Temporalvalue is a JavaScript object and the constructors are properties of one, and neither can be held by a struct that crosses to the threadpool thread a statement runs on, so each conversion looks the constructor up through a global, a namespace and a class. On 50k rows here a DATE column costs about 560ns a row as aZuDateand about 650ns as aTemporal.PlainDate, against 240ns for the INT64 column beside it, so the conversion is worth about a sixth of a value that was already the most expensive kind to build. The day count is turned into a date with Hinnant's civil calendar algorithms, which are branch free and have no table behind them, and they round trip over two million days in the unit tests.Eleven tests, which adapt themselves to the runtime rather than being skipped by hand: they check the mode on every kind of value, the zoned time that keeps its class,
toTemporal()without the option, every inbound class including the pre-epoch and offset cases, the refusals by name and by range, that a value goes out the way it came in, that query and stream agree, and that the mode is off unless it was asked for. Both directions run on Node 24 withnpm run test:temporal, which CI now runs beside the plain suite on 24, while 26 hasTemporalunflagged and covers it in the ordinary run.npm testis 90 tests green both ways, andcargo clippy --all-features,cargo fmt --check,npm run check:typesandnpm run check:packageare green.Part of DX3, tamnd/zu#169.