Skip to content

feat(core): port Avro schema comparison and Parquet list normalization - #642

Closed
linliu-code wants to merge 7 commits into
apache:mainfrom
linliu-code:feat/mor-reader-schema-1
Closed

feat(core): port Avro schema comparison and Parquet list normalization#642
linliu-code wants to merge 7 commits into
apache:mainfrom
linliu-code:feat/mor-reader-schema-1

Conversation

@linliu-code

Copy link
Copy Markdown
Contributor

Stacked on #639, #640, #641 — their commits appear here until they merge. Review only the last commit.

Two schema helpers the merge-on-read reader depends on:

  • schema::avro_schema_utils — decides whether two Avro schemas are projection-equivalent, so the reader can tell a genuine schema change from a reordering or a narrowing that needs no work.
  • schema::parquet_list_norm — normalizes the legacy two-level repeated-group and modern three-level Parquet list encodings, so batches from files written by different engines line up.

Also brings the two parquet fixtures parquet_list_norm's tests read. They are force-added past the **/data ignore, the same way the existing fixtures under crates/test/data are tracked.

Unlike the reader_v2 modules these are pub under schema, matching their upstream placement — nothing calls them yet. Full workspace suite green.

🤖 Generated with Claude Code

linliu-code and others added 7 commits July 31, 2026 16:19
Adds the context the merge-on-read file group reader needs and the resolver
that derives it from a table's configs. Nothing consumes it yet: the reader
itself lands in later changes, and the existing read path is untouched.

Resolving the context first gives the reader a defined target before any of it
is ported, and puts the read-semantics decisions in one reviewable place rather
than spread across the port.

Two decisions worth review:

- `append_only` is rejected rather than mapped. The merge-on-read reader always
  merges by record key, so no merge mode reproduces "keep every version". The
  table config derives `append_only` whenever meta fields are disabled or no
  ordering field is set, which makes it reachable for ordinary tables, so
  guessing a mode here would silently change which rows a query returns.

- `should_merge_use_record_position` stays off. Log blocks already carry record
  positions but nothing reads them, so merging by key is what a read does today.

Deferred until something reads them: the schema handler, the record context,
and the bootstrap flags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`resolve_instant_range` reproduces the bounds
`FileGroupReader::create_instant_range_for_log_file_scan` computes, so that a
read through either path admits the same log blocks. Nothing enforced that:
the two live in different modules and neither calls the other, so editing one
would have drifted silently.

Compares the `Debug` rendering rather than field by field, since
`InstantRange`'s fields are private. That also means a field added to the
struct is covered here for free instead of being silently skipped.

Verified the assertion can fail, by mutating the resolver twice and confirming
each was caught: flipping `end_inclusive`, and dropping the start timestamp.

Widens the legacy method to `pub(crate)`. No behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the entry point callers will use once the merge engine exists. `read`
returns `Unsupported` for now.

The entry point lands ahead of the engine so the shape callers depend on is
settled before anything is built against it, and so the gap is something a
test can point at rather than an absence.

It shares a name with the file group reader that serves reads today, which it
replaces once the engine behind it is written — so it carries the name it will
keep, and the eventual swap is a module deletion rather than a rename touching
every call site. Until then the two are told apart by module path, and nothing
outside the module uses this one.

`read` fails rather than returning an empty batch: an empty batch is
indistinguishable from a file slice that genuinely has no rows, which would let
an unfinished reader look like it worked.

Construction stays separate from reading, so callers wiring up a reader do not
have to handle a failure until they ask for rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three fixes from self-review:

Configs that steer this crate's own behavior (`hoodie.internal.*`,
`hoodie.plan.*`) were being swept into the table properties, which are meant to
be what the table declares about itself. A reader has no way to tell a
swept-in crate config from a real table property, so they now reach it through
neither map.

The module carried a blanket `allow(dead_code)` because nothing consumes it
yet. That also silenced items dead by mistake — it was already hiding that
`MergeMode::CommitTimeOrdering` cannot be constructed, since a table with no
ordering field derives `append_only`, which the resolver rejects. Each item now
carries its own allow, so a newly dead one still warns, and the unreachable
variant says why it is unreachable and what makes it reachable.

The reader construction test asserted a field the resolver tests already cover
and named a guarantee the type system already gives, since `new` cannot fail.
It now checks that the reader hands back the parameters it was built with,
using non-default values so substituted defaults would fail it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four modules with no dependencies inside the reader subsystem: read
statistics, the profiling macro, the iterator mode, and the input split that
names what a read covers.

Ported as-is from the merge-on-read reader. Nothing consumes them yet, so each
file carries a file-scope allow(dead_code); every item in them is live upstream,
so a per-item allow would just be noise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The delete context carries what a read needs to recognize a delete: the marker
field and value a payload uses to mark one, resolved from table config. The
output converter projects a merged batch down to the columns a caller asked for.

Both are leaves — no dependencies inside the reader subsystem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two schema helpers the merge-on-read reader needs.

`avro_schema_utils` decides whether two Avro schemas are projection-equivalent,
which is how the reader tells a genuine schema change from a reordering or a
narrowing that needs no work.

`parquet_list_norm` normalizes the two Parquet list encodings — the legacy
two-level repeated group and the modern three-level one — so batches read from
files written by different engines line up. Brings the two fixtures its tests
read; they are force-added past the `**/data` ignore, the same way the existing
fixtures under crates/test/data are tracked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linliu-code

Copy link
Copy Markdown
Contributor Author

Closing: this line is superseded.

The merge-on-read work now lives on a single line (#663-#673, plus #675), which
starts from the port as one squashed commit rather than this decomposition of
it. That squashed commit contains everything this chain built, so nothing here
is lost -- it is the same tree, reached by a different route.

#660 carries the full squash for review, and is now up to date with the tip.

@linliu-code linliu-code closed this Aug 9, 2026
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