Repository navigation
Conversation
Implement the Avro 2 schema, codec, resolution, container, Tables, limits, and prepared-operation architecture. Replace the legacy implementation and add optional codec and TimeZones extensions.
Add the full correctness, resource-limit, concurrency, performance, interop, cross-version, quality, smoke, and CI matrix. Include pinned Apache and generated fixtures plus recorded benchmark baselines.
Add the reviewed implementation plan, status record, changelog, migration guide, user manual, examples, and design-review archive for the Avro 2 rewrite.
quinnj
commented
Sep 22, 2026
quinnj
left a comment
Member
Author
There was a problem hiding this comment.
[Reviewed by AI] Reviewed container framing at e9691e7. No actionable findings in this scope. All-prefix truncation and sync-corruption checks passed across four built-in codecs and byte/stream sources on Julia 1.10 and 1.12 (7,888 checks each). Existing binary, container, and parallel-read tests also passed. This review does not cover the entire rewrite.
Resolve the physical parent when a writer opens so changing the working directory cannot publish into another folder. Preserve parent symlink traversal and final symlink replacement, and cover atomic writes, in-place writes, and abort cleanup. Generated-by: OpenAI Codex
Make the package project available during JET's virtual-module analysis and restore the load path afterward. Keep the existing analysis target and zero-reports gate. Co-authored by Codex
Compare the reservation guard against current raw availability after the holder is parked, retaining the original payload and tolerance and adding exact pending/settlement controls. Keep the resident buffer alive through the check. Admit JET 0.12 for compatible dependency loading and preserve strict stable analysis. Remove captured-local ambiguity and state existing private decoder/schema and input contracts without weakening resource accounting. Add public controls for unnamed tuple records and nongeneric column materialization. Authenticate coverage uploads with job-scoped OIDC and report transport failures. AI disclosure: This work was prepared with assistance from OpenAI Codex.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #21 +/- ##
===========================================
+ Coverage 77.43% 90.66% +13.23%
===========================================
Files 13 33 +20
Lines 811 15411 +14600
===========================================
+ Hits 628 13973 +13345
- Misses 183 1438 +1255 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Preserve the default OS sampling and max/clamp policy while allowing the internal helper to compare fixed samples. Verify fallback, reclaimable-memory selection, and integer saturation without comparing different OS moments. Keep all memory admission and reservation bounds unchanged. AI disclosure: This work was prepared with assistance from OpenAI Codex.
AI disclosure: This work was prepared with assistance from OpenAI Codex.
Delete the rewrite's working files, which are not part of the package: reviews/ (Codex plan and implementation review rounds, prompts, and responses), AVRO_REWRITE_PLAN.md, STATUS.md, benchmarks/ (baseline scripts and logs), and the pinned root and test Manifest files. The Manifests made CI test pinned dependency versions instead of the newest compatible versions that users install from the registry. Pkg.test now resolves the test environment from Project.toml and test/Project.toml, like other registered packages. docs/Manifest.toml stays, as on main. A byte-identical copy of every removed file is kept in ~/.julia/dev/Avro-2-dev-material on the maintainer's machine. Revert this commit to restore them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Apache Python writer and fastavro 1.13.1 and earlier strip only one of the four zlib checksum bytes from each deflate block, so every deflate file they write has 3 bytes after the final deflate block. Java and Python readers ignore those bytes; Avro.jl rejected the files with a CodecError. Ignore them too, so the 19 fastavro deflate fixtures now decode and are checked against their expected values like the other fixtures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Field, enum-symbol, and union-branch indexes were built by sorted insertion, which moves O(n^2) entries and exceeded max_compare_bytes_per_byte under default limits: a record of 1,000 fields in reverse order or 5,000 fields named x1, x2, ... failed to parse, an enum of 250 unsorted symbols failed, and Avro.write failed for a table with 150 columns named x1...x150. Append the names and sort once with the charged merge sort instead; duplicates are still reported at their second occurrence. Derived and constructed names are credited as the operation's input, as parsed schema text already is, so the comparison rule scales with them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ay branches
Encoding a NamedTuple, struct, or Dict into a union of several record types
picked the first record branch, so `(x=1, y="s")` against `[A{x}, B{x,y}]`
encoded as A and silently dropped `y`, and a value lacking A's fields failed
even when B fit. The value now goes to the first record branch it fills
completely with the most fields.
Numbers, characters, NamedTuples, and pairs are iterable in Julia but are
not arrays: `encode(array<int>, 5)` wrote `[5]`, and a union with an array
branch claimed scalar values meant for later branches. They are no longer
accepted as arrays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typed decoding converted some values silently and wrongly:
- a timestamp decoded into an `Avro.Timestamp` of another unit kept the raw
ticks (1 s of millis became 1 ms of micros), and a timestamp could become a
local timestamp; units now convert exactly or raise ConversionError, and
instants and wall-clock times stay apart;
- a decoded string, integer, float, or Bool used as an array, dict, or struct
source filled the target with nothing (a string into `Vector{UInt8}` gave
`UInt8[]`); it now raises ConversionError;
- a union target picked the first compatible member in declaration order, so
`long` data in `Union{Int32,Int64}` failed and `float` data in
`Union{Float16,Float64,String}` overflowed to Inf16; a branch's own Julia
type now wins, and a Float16 target rejects overflow;
- a record decoded into a `Dict` target was treated as a struct and failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Avro.Rows` and `Avro.eachdatum` with `validate=:fast` skipped the check that a block's declared datum count consumes its payload exactly, so a block holding three datums but declaring two returned two and dropped the third without an error, and a zero-count block with a payload was ignored. `Avro.Table` and `eachblock` already reject both in either mode; the streaming readers now do too. Avro.jl 1.x null-codec padding is still accepted with `legacy=:avrojl1`, now in both modes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `Avro.write` derives the schema from the first partition and encodes rows by column name, so a later partition's extra column was dropped silently. A partition whose columns differ from the derived schema now raises ArgumentError (an explicit `schema=` still selects columns by name). - On Julia 1.11+ an IOBuffer wrapping a Vector whose memory starts at an offset (after `take!` from a PipeBuffer, or `popfirst!`) was read from index 1 of the underlying memory, so `Avro.Table(IOBuffer(v))` saw the wrong bytes. Sources now start at the buffer's first byte. - Atomic writes publish a `mktemp` file, which is owner-only (0600), so new files were unreadable to other users and replaced files lost their mode. A replaced file keeps its mode; a new file gets the mode `open` would give it under the current umask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The undocumented, untested method pushed each element of its argument, so `write(w, "hello")` wrote five one-character datums and `write(w, row)` wrote a NamedTuple's fields as separate datums. `push!(w, datum)` is the documented way to write one datum. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Tables.rows(t::Avro.Table)` reports the Table's stored schema (names and types held in fields, not type parameters), which Tables.jl's generic row-to-column fallback cannot use, so `DataFrame(Tables.rows(t))` threw a TypeError. The row view now hands Tables.jl the Table's columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Projecting a record substitutes the projected record for every reference
to it, but nested values still decode with all of their fields. With
`Node{v, next::[null, Node]}` and `select=(:next,)`, the table's schema
said nested nodes had only `next` while the values carried `v`, and writing
the table dropped every nested `v` without an error. Selecting a field
whose type refers back to the record now raises ArgumentError unless every
field is selected; other selections are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a writer union branch failed to resolve, resolution cleared its whole
pair memo, including the in-progress plan a recursive record stores so its
self-references terminate. A recursive record holding such a branch was
then resolved again from scratch, recursing until a StackOverflowError
(about four minutes for `R{a: [null, X{v: string}, R]}` against a reader
with `X{v: int}`). Only the memo entries the failed branch added are removed
now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
minsize memoised every node's size, including sizes computed while an
ancestor was on the stack and therefore treated as infinite. In
`Org{founder: Person, company: Company}` with `Person.employer: [null,
Company]` and `Company.ceo: Person`, Company was cached as having no finite
datum, so minsize(Org) was typemax(Int) instead of 5 and decoding an array
of Org, including a file Avro.Writer had just written, failed with "a schema
with no finite datum".
minsize now uses Knuth's generalisation of Dijkstra's algorithm: records sum
their fields, unions take their cheapest branch plus its index, and nodes
are finalised in increasing size, which gives every node its least size in
O(e log n).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`record.schema` and `record.values` returned the record's internal schema and value vector even when the record had fields with those names, so `r.values` for a record with a `values` field silently returned the wrong thing. A field of that name now wins; `Avro.schema(r)` still returns the schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AvroTimeZonesExt was an empty module, so loading TimeZones changed nothing, while the logical types manual promised ZonedDateTime typed targets through it (decoding into one raised ConversionError). Drop the extension, its weak dependency and compat entries, and the claim; document the exact unit conversion typed Timestamp targets get instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Point the specification links at a page that exists. - Fix claims probes contradicted: the sort-order example's argument order; the error an unresolvable writer union branch raises (ResolutionError, when a datum uses it); which sources `Avro.write` takes a retained schema from; that every limit raises LimitError (codec memory raises CodecError); that the fuzz harness is differential (it is a seeded mutation fuzzer); that the deprecated shims print warnings (Base.depwarn is silent by default); `select=` on typed Rows. - Render only public docstrings in the reference. - Replace the two benchmark pages' conflicting, unsourced numbers with the gates `test/perf.jl` enforces, their baseline, and the last recorded run. - Attach the DatumWriter call forms to DatumWriter, list the three 1.x defects `legacy=:avrojl1` handles, and give fingerprint's real return type. - Drop marketing and change-history wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments, docstrings, testset names, and three error messages cited
sections of the design plan ("plan §4.4"), Codex review items ("round-3 item
1", "D03", "R10"), and implementation phases. That material is no longer in
the repository, so the references are dangling. Remove them, keeping each
comment's explanation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete definitions nothing calls (a reference scan over src, ext, test, docs and examples, each hit read by hand): unsafe_substring, skiplong, JSONSyntaxError and its showerror, WS, FrozenJSON, the one-argument FrozenVector constructor, COMPLEX_KEYWORDS, reserve_replacement!, rowcount, props, a four-argument defaultmatches, resolvesoftrecord, describe, growbufextra, cellslack, planneddatumspan, both datumspan methods and the SpanCursor they built, a three-argument begindatum!, a two-argument convertleaf, jsonerror(msg), a two-argument storedschema, and the splitfullname/resolvefullname/resolvereference/normalizealias copies of the name algorithm that only their own tests used (the parser has budgeted versions). Also drop fields nothing reads (Writer.atomic, JSONContext.strict, TablePreflight.slack), keywords nobody passes (prescan's stats, ledgerbudget's direction), and the `budget === nothing` guards in spanplan, whose callers always pass a Budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolving a writer record against a reader record runs under a budget with no input credit when called directly (`Avro.resolve`), so the per-field name lookups exhausted the fixed work allowance: records with 2,000 fields failed with "raise max_compare_bytes_per_byte". Each reader field name looked up is now credited as input, the same rule parsed schema text follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Avro.jl 1.x sized the block buffer from the first row, so string columns with longer later values failed to write (#17). Pin the 2.0 behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The string-column test assigned `s`, which the Tables testset already uses for its schema, so a later testset passed a vector as `schema=`. Rename it, and scope the other new outer-level test variables with `let`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Summary
replace the Avro.jl 1.x internals with the planned 2.0 schema, value, binary, container, resolution, typed, projection, and sort-order architecture
enforce one bounded resource-accounting model across parsing, encoding, decoding, codecs, parallel reads, defaults, and public operations
add prepared datum APIs, streaming rows, materialized tables, projection, single-object encoding, legacy-file support, codec extensions, and complete migration documentation
add generated and Apache fixtures, Java and fastavro differential tests, cross-version exchange, performance and RSS gates, Aqua, JET, smoke tests, and the supported Julia CI matrix
use registered DataDecimals 1.0 for ordinary decimal results and accept fixed-scale shared decimal inputs
accept Durations 1.0 values with checked millisecond conversion; retain the full unsigned wire range in generic Avro duration results
Takeover audit
The recovered implementation had completed phases 0 through 5. A final independent audit fixed these release blockers:
max_depthacross reader defaults, JSON encoding, and OCF writer stagingtojson, andcompareunder one public-operation work budgetmax_byteswhile skipping inactive sized-map keyssrc/span.jland enable the required Arrow 2 and DataFrames smoke jobBreaking changes
Avro.write(dst, x)now means container output. UseAvro.encodeorAvro.encode!for datum encoding.Avro.Tablereplacesreadtablefor materialized columns.Avro.Rowsprovides streaming rows.readtableandwritetableremain as one-cycle deprecation shims. The migration guide covers legacy Avro.jl 1.x files.Shared-value validation
Validation before shared-value adoption
e5c3509: 26 jobs passed, 0 failed, and the disabled Arrow 3 candidate job was the only planned skipWriter destination stability
Opening a writer for
out.avroand changing the working directory beforeclosepreviously left the intended file unchanged and could replaceout.avroin the new directory. The writer now resolves the physical parent when it opens and uses that same destination for temporary-file creation and publication. Parent symlinks and..keep their filesystem meaning. Atomic writes still replace a final symlink while preserving its referent.Focused regressions cover atomic and in-place writes, abort cleanup, and symlink paths. The API and container manual describe the directory selection at open time.
Validation for this correction:
SubStringpaths, changes to the working directory or parent symlink, atomic and in-place writes, aborts, do-blocks, and caller-owned streams.The writer destination correction was researched and implemented with Codex. This bounded review covers the writer correction; review of the complete rewrite remains separate.
Quality analysis environment and current CI repairs
JET evaluates Avro in a virtual module during the quality gate. The package project remains available only during that analysis and is removed in
finally. The analysis target and the requirement of zero reports are unchanged.The test dependency range now admits JET 0.12 alongside the previous versions. This resolves the JuliaInterpreter precompilation failure before nightly native tests. JET 0.12 explicitly supports full analysis on stable Julia 1.12/1.13 and provides dependency-loading stubs on newer Julia. The stable quality job still performs real package analysis; the nightly native job never requested analysis.
The newer stable checker found captured local variables and implicit private input contracts that the prior checker did not report. Prepared readers, the enum constructor, and inspection diagnostics now use separate closure-local names. Nullable documentation values are read once. Binary comparison and row decoding express their existing decoder/schema contracts. Unnamed tuple fields and nongeneric column materialization receive the package's existing error types; metadata keys are validated before copying. All resource-accounting and decode paths are retained.
The overlapping-reservation test now compares the current unreserved OS-memory reading with Avro's available-memory reading after the reservation worker is parked. Its original 64 MiB payload, 24 MiB noise tolerance, lower bound, resident-storage checks, and failing-admission checks remain. Additional checks verify the exact pending counter, an upper bound, settlement restoration, and the resident buffer's lifetime.
A bounded Linux control reproduces the stale historical-baseline failure when an unrelated 96 MiB process exits normally. Its historical delta was -132,935,640 bytes although the current subtraction and pending counter both measured 67,108,904 bytes; all nine positive controls passed. This establishes a cause of the test's false failure, without identifying the external activity responsible for the earlier hosted observation.
Coverage uses the verified v7 uploader with job-scoped, conditional OIDC authentication and strict upload-error reporting. The previous successful job's logs recorded token-required upload failures. Coverage percentages remain informational; no coverage threshold, native gate, or quality assertion is changed.
Validation for this bounded correction:
42713fe3ceeac0d8e67feca2fc1bdbeb134b2012was accepted and processed as one report at 90.04%. Its Windows Julia 1.11 four-thread job exposed the separate sampling-test issue below; its compile-cost and all six latency gates passed.This bounded correction does not constitute approval of the complete rewrite. The required independent review remains a separate gate.
Consistent host-memory sample checks
The Windows Julia 1.11 test compared a host-memory result of 13,201,567,744 bytes with a later OS reading of 13,201,600,512 bytes. On Windows the helper returns the clamped OS reading, so memory released between those calls invalidates the test's inequality. The macOS comparison also used readings taken at different moments.
The internal helper now accepts optional sampled values while preserving its zero-argument OS sampling, integer saturation, and macOS reclaimable-memory maximum. Deterministic checks cover those same policies, and live platform queries still run. Admission limits, pending reservation subtraction, memory bounds, the 24 MiB noise allowance in the separate resident-memory test, and latency gates are unchanged.
The full Limits suite passes 148 assertions on actual Julia 1.10.12, 1.13.1, and installed nightly 1.14.0-DEV.3316. Stable quality validation passes Aqua's 10 checks and real JET analysis of all 2,820 definitions with zero reports. The full Julia 1.13.1 native suite at
a285ada58da9fdb55568f2c774a52f1626360493passed 62,509 checks, including compile-cost and all six latency gates (maximum 4.90 seconds against 10 seconds).Worker tests under admitted memory budgets
Parallel decoding correctly falls back to serial operation when the available-memory ceiling cannot reserve the head block and a worker block. The worker-specific tests used 8 KiB data blocks with a 256 MiB output cap; a controlled planner fixture measured a minimum worker ceiling of 1,192,102,937 bytes. The hosted macOS Julia 1.10 test selected the serial path, so its worker-count and injected worker-failure assertions did not exercise their intended branches.
Worker-specific fixtures now use a 16 KiB block cap, 1 MiB output cap, and the existing 16 MiB minimum codec cap. Their measured worker reservation is 55,479,321 bytes, below the unchanged 88,113,152-byte first-unit guard. Every admitted reader therefore has enough budget for those fixture workers. The 64 MiB datum and byte defaults, wide-limit acceptance cases, 375 MB total-limit fixture, production admission policy, and native memory and latency gates remain unchanged.
Permanent controls cover the old worker-admission boundary one byte below and exactly at the threshold, worker admission at the fixture's minimum reader budget, and restoration of pending reservations. All existing worker, failure-ordering, and scheduling assertions remain.
Validation for this test-only correction:
0eb1e12a5e22dd383d7bc3a2abe375ae937dac0cpassed all 26 active jobs. Raw logs confirm the native minimum/current/1.11 matrix, prerelease and nightly tests, real stable Aqua/JET and Arrow 2 smoke, Java/fastavro interoperability, cross-version exchange, strict docs, and trim smoke completed without errors. The nightly native suite passed 62,469 checks in 92m21s; the macOS minimum four-thread lane that exposed this fixture issue now passes 62,512 checks.JuliaData/Avro.jland processed as the expected one coverage report, covering 13,776 of 15,298 lines (90.05%). Both coverage status checks pass. The existing disabled Arrow 3 candidate remains the active run's only planned skip; the duplicate same-repository pull-request run skips its jobs by the existing workflow condition.Co-authored by Codex
AI disclosure: This work was prepared with assistance from OpenAI Codex.
Maintenance sweep (Oct 15, 2026)
Changes pushed to this branch as separate commits; every behavior fix has a regression test:
x1…x150failedAvro.write; 1,000 reverse-ordered fields failedparseschema).Dicttargets.select=on a recursive record raises instead of mis-describing nested values; a failed union branch inside a recursive record no longer recurses until a stack overflow; minsize is exact for mutually recursive records (files Avro.jl wrote were unreadable);record.schema/record.valuesreturn fields of those names when the record has them;Base.write(::Writer, x)and the empty TimeZones extension are gone.Closes #6
Closes #13
Closes #15
Closes #17
Closes #18
Closes #19
🤖 Generated with Claude Code