Skip to content

The shared corpus, run through this client - #29

Merged
tamnd merged 2 commits into
mainfrom
corpus
Aug 24, 2026
Merged

The shared corpus, run through this client#29
tamnd merged 2 commits into
mainfrom
corpus

Conversation

@tamnd

@tamnd tamnd commented Aug 24, 2026

Copy link
Copy Markdown
Owner

The corpus is one set of hand written YAML files in the engine's repository, under conformance/cases. Every client reads the same files and runs the same statements, so a value that survives one binding and not another is a diff rather than an argument. This is the Node end of it: a reader for the YAML subset the cases are written in, a decoder for the value encoding, a reader for the Arrow schema an export carries, and a runner that reports what happened in the words the reference runner reports it in.

1412 cases, 1411 passed, 0 failed, 1 unsupported

It is a development tool and not part of the package. files in package.json lists what ships and conformance is not among them, so it is there when the repository is checked out and absent when zudb is installed.

The reader is a second opinion and not a consumer

A client that decodes a value with the same code it encodes with passes every case it writes, because the case and the answer both went through one decoder. So conformance/reader.mjs refuses what crates/zu-corpus/src/yaml.rs refuses, with the same words and the same line numbers, and the tests are that file's own tables ported case for case. There is no YAML dependency, which would read these files and a good deal more besides, and would hand back 9223372036854775807 as a float on the way.

conformance/temporal.mjs spells the four accepted temporal forms out rather than calling Date.parse, which reads 2024-1-1 and Jan 1 2024 and is allowed by its own specification to read anything else it likes.

The Arrow half is read out of the bytes

This client hands out Arrow IPC rather than a C Data Interface stream, so there is no ArrowSchema to walk and no format string to read off one. conformance/arrow.mjs parses the schema flatbuffer at the head of the buffer and turns each field back into the format string a case is written in.

A flatbuffer omits any field holding the schema's declared default, which is the trap and which cost five cases before it was found. Int.is_signed defaults to false, so an unsigned column is written as a width and nothing else, and a reader defaulting it the other way reads every UINT64 as an INT64 and says nothing about it. FloatingPoint.precision, Time.unit and Duration.unit were wrong for the same reason and are fixed with it. test/conformance-arrow.test.mjs now pins the unsigned case against bytes the addon really produced, rather than against a buffer written by hand, which would only prove the reader agrees with whoever wrote the buffer.

Two client fixes it found

A negative zero bound as an INT64. There is no INT64 that is negative zero, so binding it as one throws away the sign a caller went out of their way to write, and a statement comparing it against a FLOAT64 -0.0 then compares against 0 instead. It binds as a FLOAT64 now.

A temporal column that arrived without cells reported a type whose buffer was empty. The engine's columnar sink has no arm for days, nanoseconds or months, so a date, a time or a duration reaching a projection the wrong way comes over as the values themselves, and which of the two a statement gets is the plan's business rather than the caller's: the same property is cells in one statement and values in another, and adding an unrelated boolean column to the projection is enough to move it. type on a column is what arrived now rather than what the statement declared, so a column that came over as values reads value with its dates in items, and unit and zone are null because there are no cells for them to describe. A column saying date with nothing in values would be a switch that lands nowhere, and the whole point of type is that the switch is one string comparison. The engine side is tamnd/zu#690, and the test pins the invariant rather than the shape, so it still passes on the day the engine fills a buffer for every one.

What this client cannot answer

result/the-same-name-twice, which is RETURN 1 AS a, 2 AS a. Two columns of one name is a legal result and this client hands a row back as a plain object keyed by column name, where the second would silently replace the first. The runner reports it unsupported, by name and with the reason in the line, rather than passing it on a value invented for the occasion or failing it as though the engine had got something wrong. Unsupported is a third outcome and not a softer kind of failure: a case the corpus wrote on purpose to catch a mapping cost is a case a client should be able to say out loud it cannot answer.

The pin

526ac58, up from 0698a4ec. It brings BYTES as a real type, which is a Uint8Array both ways and a bytes column in a columnar read, and it brings ten new reserved words. The tests that used on, at, number, nothing, record, small, count, big, day or exact as an alias or a label are renamed.

CI

A corpus job that reads the revision out of Cargo.toml, checks the engine out at it, and runs both the report and the tests against the cases it finds. The revision is not written down a second time, so it cannot drift from the pin this client already builds against.

Checks

node --test "test/*.test.mjs" is 451 tests, 442 passing and 9 skipped, the skipped being the corpus tests that want ZU_CASES. cargo fmt --check, tsc --noEmit and api-extractor run are all clean. The corpus report is the line above.

tamnd added 2 commits August 24, 2026 21:15
The corpus is one set of hand written YAML files in the engine's
repository and every client runs the same cases against it, so a value
that survives one binding and not another is a diff rather than an
argument. This is the Node end of it: a reader for the YAML subset the
cases are written in, a decoder for the value encoding, a reader for
the Arrow schema an export carries, and a runner that reports what
happened in the words the reference runner reports it in.

The reader is another implementation of the format rather than a
consumer of one. A client that decodes a value with the same code it
encodes with passes every case it writes, so this refuses what
crates/zu-corpus/src/yaml.rs refuses, with the same words and the same
line numbers, and the tests are that file's own tables ported case for
case.

The Arrow half is read out of the bytes. This client hands out IPC
rather than a C Data Interface stream, so there is no format string to
read off an ArrowSchema and the schema flatbuffer at the head of the
buffer is parsed instead. A flatbuffer omits any field holding the
schema's declared default, which cost five cases before it was found:
Int.is_signed defaults to false, so an unsigned column is written as a
width and nothing else, and a reader defaulting it the other way reads
every UINT64 as an INT64 and says nothing about it. Three more defaults
were wrong for the same reason and are fixed with it.

Two client fixes came out of running it. A negative zero bound as an
INT64, which throws away the sign a caller went out of their way to
write, and now binds as a FLOAT64. And a temporal column that arrived
without cells reported a type whose buffer was empty: the engine's
columnar sink has no arm for days, nanoseconds or months, so a date or
a duration reaching a projection the wrong way comes over as the values
themselves, and which of the two a statement gets is the plan's
business rather than the caller's. `type` now says what arrived, so a
switch on it always lands on a field that holds something. The gap
itself is tamnd/zu#690.

RETURN 1 AS a, 2 AS a is the one case this client cannot answer. Two
columns of one name is a legal result and a row keyed by name has one
of them, so the runner reports it unsupported, by name and with the
reason, rather than passing it on a value invented for the occasion or
failing it as though the engine had got something wrong.

The pin moves to 526ac58, which brings BYTES as a real type and ten new
reserved words. BYTES is a Uint8Array both ways and a bytes column in a
columnar read, and the tests that used on, at, number, nothing, record,
small, count, big, day or exact as an alias or a label are renamed.

1412 cases, 1411 passed, 0 failed, 1 unsupported.
Four things went red on CI that are nothing to do with the corpus
itself, and all four are the same kind of mistake: a test or a lint that
held on the machine it was written on and nowhere else.

Clippy on the newer toolchain wants as_chunks where the code says
chunks_exact and then unwraps a try_into back into the array it already
had. The new form drops the expect entirely, since the array size is in
the type. Two call sites in frame.rs and one in load.rs.

The two tests that ask whether a big read leaves the event loop free
counted ticks against a fixed bar of one every ten milliseconds. That
bar reads as a property of the code and is really a property of the
platform. Windows fires a timer on its own clock tick of about sixteen
milliseconds, so a sixty millisecond read gets four ticks with the loop
completely idle and the test fails on a loop that was never blocked.
Both now measure what an idle loop on this machine manages first, in
test/helper.mjs, and ask for a fifth of it. That is still a hundred
times what a loop parked inside the addon would give, so the test
catches what it was written to catch, and it moves with the load on the
machine rather than flaking under it.

The watch test that spawns a copy of the runtime is skipped on Deno.
Deno needs --allow-run to spawn at all, and a Deno child inherits none
of its parent's permissions, so the copy fails to open the library
rather than failing to exit. That is not the question the test asks.
@tamnd
tamnd merged commit d1c6921 into main Aug 24, 2026
30 checks passed
@tamnd
tamnd deleted the corpus branch August 24, 2026 15:55
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