Repository navigation
Check new-writer files against each edition's minimum reader in vortex-compat - #10399
joseph-isaacs wants to merge 13 commits into
Conversation
Adds a forward-compatibility check: fixtures written by the current writer are read by a separately built binary pinned to a released vortex crate, which dumps each one as Arrow IPC. `vortex-compat check-reader` decodes the same fixtures with the current reader and compares values column by column. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Some Arrow IPC implementations drop the flag, and it has no effect on the physical layout or values. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Lets the fixture writer target an older frozen edition so the files can be checked against that edition's recorded minimum reader version. Fixtures whose encodings the edition forbids are skipped. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
…data wheel Run with `uv run --with vortex-data==<version> old_reader.py <fixtures> <out>` and feed the output to `vortex-compat check-reader`. Reads each file in a child process so a reader abort is recorded instead of ending the run. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Writes random canonical arrays through the flat layout and both compressor pipelines, plus random dict, constant and run-end arrays, one set per seed. Honours --edition and skips files the edition rejects or the writer panics on. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
One child process reads the whole directory; when it dies on a file, that file is recorded as aborted and a new child resumes after it. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Prints each file's dtype, layout IDs and array encoding IDs. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
…-path queries The sweep now also writes columns of random base arrays through random encoder stacks, canonicalized random slices, and compressed files with randomised row block size and data block target. Every written file gets a queries.json entry: comparison filters with literals taken from the data, column projections, sorted row-index reads and limits. old_reader.py runs the queries with the old wheel and check-reader compares each result with the current reader. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Merging this PR will degrade performance by 11.52%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | WallTime | bitpack_blocked_compress_avx2 |
6.7 µs | 7.6 µs | -11.52% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing ji/nifty-cerf-m02guh (0a2f7a5) with develop (881b867)
Footnotes
-
534 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
serde_json's default float parser is not exact, which made data-derived f64 literals miss on equality and collapse tiny exponents; enable float_roundtrip. Query field names use the raw name rather than the escaped Display form, and limit queries use scan(limit=) which every wheel has. Add a probe command that evaluates one comparison by pushed-down scan and by plain Rust. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV
Summary
The existing
vortex-compatsuite proves the current reader can read files written by older releases. Nothing checks the other direction, which is the promise a frozen edition makes: a file written with editionEmust be readable by every release at or aboveE's recordedmin_library_version. This PR adds that check, a seeded random sweep with read-path queries, and the results of running them for every frozencoreedition against a ladder of released readers.The survey shows the recorded minimum versions of the three pre-2026 core editions cannot be honored by today's writer. The wire changes responsible all shipped in 0.37.0 to 0.84.0, before the frozen records were written (0.86.0, #9660), so the records retroactively promise compatibility that was already gone. The first release that reads every file and answers every query identically for every edition is 0.85.0. The two 2026 editions hold up completely: zero differences across 1,600 files and 5,431 queries per reader.
Changes
vortex-compat generate --edition <id>writes the curated fixtures with a chosen frozen edition enabled. Fixtures the edition forbids are skipped.vortex-compat sweep --edition <id> --seeds Nwrites, per seed: a random canonical array through the flat layout; a canonicalized random slice of it; the array through the default and compact compressor pipelines with randomised row block size and data block target; columns of random base arrays (integers, floats, strings, booleans, timestamps with units and timezones, decimals, sequences) through random encoder stacks (FoR, ZigZag, ALP, ALPRD, Pco, Zstd, RLE, Sparse, Dict, RunEnd, FSST, DateTimeParts, DecimalByteParts); and random dict, constant and run-end arrays. Every eighth seed is eight times longer. Deterministic in the seed; writer panics and edition rejections are recorded insweep.json.Every written file gets a
queries.jsonentry: comparison filters with literals taken from the data (so zone maps are actually pruned), random column projections, sorted row-index reads, and limits. The query language is deliberately small enough for every released Python binding to express.vortex-compat check-reader --dir <files> --arrow-dir <dumps>decodes each file and runs each query with the current reader and compares against the old reader's Arrow IPC dumps column by column. Mapkeys_sortedis ignored. A current-reader panic is recorded as a failure.vortex-compat describeprints dtype, layout IDs and array IDs per file;vortex-compat probeevaluates one f64 comparison by pushed-down scan and by plain Rust. Both for triage.scripts/old_reader.pyis the old reader: a publishedvortex-datawheel reads every file and runs its queries, dumping results as Arrow IPC. Files are read in batches by a child process; an abort costs one process and is recorded.cargo run -p vortex-compat -- sweep --output /tmp/sw --seeds 200 --edition core2025.10.0 uv run --with pyarrow --with typing_extensions --with "substrait==0.23.0" --with vortex-data==0.54.0 \ vortex-test/compat-gen/scripts/old_reader.py /tmp/sw /tmp/dumps cargo run -p vortex-compat -- check-reader --dir /tmp/sw --arrow-dir /tmp/dumpsWheels before 0.84 need the
substrait==0.23.0pin. The 0.36.0 and 0.40.0 wheels do not compile invortex-pco, socore2025.06.0cannot be measured at 0.40.0 with a wheel. Wheels before 0.55 have nolimitparameter, so limit queries fail on them for that reason alone.Sanity check
An injected break (a version byte prepended to the
fastlanes.formetadata, stripped by the matching deserializer) passes the existing HEAD-onlycheck30/30 but is caught against 0.85.0 withfailed to decode Protobuf message: invalid tag value: 0. The break is not in this PR.Results: random sweep with queries (200 seeds per edition, up to 8 files per seed)
Under every pre-2026 edition, every compressed-pipeline file is rejected at write time because the zoned layout emits
vortex.max,vortex.bounded_maxandvortex.null_countaggregates no old edition permits, so those rows hold flat-layout files only and the default write strategy is unusable under an old edition as things stand. The curated fixtures (30 files) give the same picture: 26/29 on 0.54.0 for core2025.10.0, 16/23 on 0.36.0 for core2025.05.0, and 100% on 0.85.0.Wire changes under unchanged IDs
Each was traced to its commit and first release, and the old reader's error string was confirmed in that release's own crate source.
vortex.sparseCannot convert DType utf8? into PTypevortex.fsstExpected 2 buffers, got 3changelog/break)vortex.pcoformat version (4) exceeds max supported (3), then process abort. pco 1.x cannot write the old versionvortex.dictexpected type: unsigned int but instead got i32vortex.runendRunEnd array can only have Bool or Primitive valuesvortex.structlayoutStruct layout has N+1 children, but dtype has N fieldsvortex.constantListValueexpected List dtype for ListValuevortex.constantListValueexpected List dtype for ListValuevortex.constantUnknown DType variant/field DType must be valid, process abortThe Map row exposes a gap in the model: editions govern array, layout, extension-dtype and aggregate IDs, but not core DType variants, so a new logical type reaches old readers through any permitted encoding.
The query comparison found no divergence in filter pruning, projection, random access or limits against any reader from 0.84.0 on. Against older readers the query failures are the same wire changes above, reached through a different read path, plus reader-side bugs below.
Reader-side bugs the sweep also surfaced (not writer regressions)
Failed to downcast). 0.87.0 and HEAD read the file. Fixed in HEAD by Canonical dict execute handles edge cases in Decimal/Bool logic #10098, merged the day 0.88.0 was tagged, so the release carries it.ArrayStreamAdaptersees an inner struct read back as non-nullable while the file dtype says nullable. Release builds skip the assertion and produce the right Arrow schema, so this is a latent invariant violation ondevelop. Reproduce with the previous sweep generator at seed 640.is_zero_copy_to_listassertion. 0.70.0: sameListValuelimits as above. All fixed later.Sparse::encodeon bool and string arrays,dict_encodeon bool,Pco::from_primitiveon 8-bit types (unreachable!),DateTimePartsandDecimalBytePartson out-of-range values. The sweep falls back to the canonical column when an encoder panics.What this suggests
Either the three pre-2026 records should have
min_library_versionraised to 0.85.0, or the writer needs legacy serializers behind new IDs for the components above when an old edition is selected. pco has no writer-side option at all. DType variants need to be edition members or the writer must reject dtypes an edition's readers cannot parse, and the default strategy should restrict zone-map aggregates to the permitted set rather than failing. Thecheck-editionsfreeze guard currently forbids editing a frozen record, so raising the minimums needs a deliberate exception.Not done
vortex/editions/core/*.toml, reading each record'smin_library_version, runninggenerateandsweepwith that edition andold_reader.pywith that wheel.check-readercompares against the current reader, not an independent oracle. Where results differed, an independent pyarrow count was used to decide; every such case turned out to be a harness bug (serde_json's inexact default float parsing, now fixed withfloat_roundtrip), not a reader bug.cargo build -p vortex-compat,uvx ruff formatanduvx ruff checkon the script. Not run: clippy, rustfmt, nextest.🤖 Generated with Claude Code
https://claude.ai/code/session_01BS1uCYLpjJK6wBjvdMDgBV