Conversation
0fd2ec7 to
5d260fe
Compare
974c894 to
1787e4b
Compare
chenzl25
left a comment
There was a problem hiding this comment.
Requesting changes for two blocking scan-correctness issues. Both affect public reader configurations and can silently return the wrong rows.
| _predicate: &BoundPredicate, | ||
| ) -> Result<Box<PredicateResult>> { | ||
| if is_variant_ref(reference) { | ||
| return self.build_always_true(); |
There was a problem hiding this comment.
This turns v IS NULL (and every Variant predicate below) into TRUE, but I cannot find the promised downstream re-evaluation: get_row_filter installs this converter result as the Arrow row filter, and TableScan::to_arrow does not apply the bound predicate again after the Parquet stream. The new fixture actually demonstrates the bug: id 1 has a non-NULL Variant and id 2 is NULL, yet both mutually exclusive predicates expect [1, 2]. Please evaluate the Variant predicate here or in a guaranteed downstream filter. The expected results should be [2] for IS NULL, [1] for IS NOT NULL, and [] for v IS NOT NULL AND id = 2.
There was a problem hiding this comment.
Fixed in f287d36: IS NULL / IS NOT NULL are now evaluated exactly in the row filter — the group-level field id resolves to the group's first leaf, and the projected struct column's validity is the variant's nullability. Test expectations are [2] / [1] / [] as suggested. The remaining operators cannot bind on a variant column (pinned by test_bind_predicates_on_variant_column), and their converter arms now error instead of returning true.
| vec![2], | ||
| ), | ||
| ] { | ||
| let reader = ArrowReaderBuilder::new(file_io.clone(), Runtime::current()).build(); |
There was a problem hiding this comment.
Please also exercise this test with .with_row_selection_enabled(true). I reproduced that configuration returning []: because the Variant root id is absent from the leaf-id map, PageIndexEvaluator::not_null takes MissingColBehavior::CantMatch and builds a skip-all selection before the Arrow row filter runs. An unresolvable Variant must be conservative at the page-index layer (select_all), and the regression should cover both row-selection settings.
There was a problem hiding this comment.
Fixed in f287d36: the page-index and row-group metrics evaluators now treat variant references as might-match instead of missing columns, and the test runs under both row_selection settings. Verified the row-selection leg fails ([] vs [1]) with the carve-out removed.
1787e4b to
f287d36
Compare
chenzl25
left a comment
There was a problem hiding this comment.
The previous two blockers are fixed. This revision still has two blocking correctness gaps in supported Variant/schema-evolution paths.
| // Collect Parquet column indices from field ids. A group-level field id (a | ||
| // variant column) resolves through the group map to its first leaf. Field ids | ||
| // found in neither map are ignored due to schema evolution. | ||
| let group_field_id_map = build_group_field_id_map(parquet_schema); |
There was a problem hiding this comment.
This group map is rebuilt from the original Parquet schema, so it cannot see IDs assigned only to the resolved Arrow schema for an id-less file. build_field_id_map_from_arrow_schema records only leaf metadata, while the position-fallback map also skips top-level groups. A name-mapped or position-fallback Variant therefore misses both maps and bound_variant_reference treats the present column as missing (IS NULL becomes all-true and IS NOT NULL all-false). I reproduced this by removing the embedded IDs from the new fixture and supplying a NameMapping: v IS NULL returned [1, 2] instead of [2] with row selection disabled. Please resolve group IDs from the post-mapping/fallback Arrow schema and cover both id-less paths.
There was a problem hiding this comment.
Fixed in 6488547 — restructured rather than patched: the four per-layer maps are replaced by one per-file FileFieldResolution, built from the parquet schema when it embeds ids and otherwise from the post-mapping/fallback arrow schema (the Rust equivalent of java writing ApplyNameMapping/addFallbackIds ids back into the MessageType). The matrix test now covers embedded/name-mapped/position-fallback × both row_selection settings; disabling the arrow-schema source reproduces exactly your [1, 2] vs [2].
| return Ok(None); | ||
| }; | ||
|
|
||
| let root_info = self |
There was a problem hiding this comment.
This root-ID check rejects a valid nested Variant. SchemaBuilder now creates accessors for nested Variants, so s.v IS NULL binds, and the group map resolves the Variant ID to its first leaf; however, get_column_root returns the outer struct, whose ID differs from the nested Variant ID. I reproduced the scan failing because the Variant is not a root column in the Parquet schema. Removing only this check would still test the outer Struct validity because the filter batch is top-level. Please either carry the nested Arrow path (including ancestor nullability) into evaluation or reject nested Variant predicates during binding and document that restriction.
There was a problem hiding this comment.
Fixed in 6488547 by carrying the nested path into evaluation: the resolver records each group's struct path, the root-column check is gone, and the row filter ANDs validity down that path — so s.v IS NULL is true when s is NULL, matching java's null-layer accessors. New test covers {s NULL, s.v NULL, s.v set} under both row_selection settings; with the descent disabled it fails [1] vs [1, 2]. Nested variants on id-less files stay unresolvable (this port's name mapping is top-level-only, pre-existing) — noted in the PR body.
9b53c8c to
5ac65f3
Compare
33e3a1a to
a4664ea
Compare
5ac65f3 to
25c6b62
Compare
|
A note for re-review context: the bugs across the revisions of this PR fall into two classes with different structural answers. Class 1 — fragmented field-id resolution. Each layer (projection mask, row filter, row-group metrics, page index) answered "where does field id X live in this file" with its own map, all keyed on leaf ids — a variant (id on the storage group) resolved nowhere, and id-less files resolved differently per layer. This PR closes the class structurally: one Class 2 — the exactness contract is unwritten. The arrow RowFilter is the last filter: nothing re-evaluates after it, so every pushdown arm is load-bearing for correctness (the earlier always-true guards, the page-index CantMatch, the row-group This PR fixes the class-2 instances reachable through its own paths and turns the undecidable cases into loud errors, but the class remains: any future arm or type is one conservative-shaped mistake away from silently wrong rows (e.g. the pre-existing missing-column |
…iles Three legs of the predicate path assumed every leaf carries a field id or every field is primitive, both broken by variant columns (the field id lives on the storage group; the metadata/value leaves are id-less): - Bind: Schema::build_accessors skipped variant fields entirely, so binding any predicate on a variant column failed with "Accessor for Field not found". Variant fields now get a position-only accessor at every struct level, like iceberg-java's Accessors; reading the value as a datum errors. IS NULL / IS NOT NULL are the only operators that bind on a variant column (literal conversion and the NaN float check reject the rest, like java); a test pins this invariant. Null-check constant folding now also requires every ancestor to be required (java's allAncestorFieldsAreRequired): a required nested field under an optional struct is still nullable. - Resolution: the per-layer field-id maps (leaf map, arrow-schema map, position-fallback map) each answered "where is this field id" on their own and all keyed on leaf ids, so a variant resolved nowhere. They are replaced by one per-file FileFieldResolution built by a single walk over the parquet tree, which alone does the leaf numbering; only the id source differs: embedded parquet ids, or the top-level arrow-schema ids that name mapping / position fallback assigned (java writes those back into the MessageType; the arrow schema is this port's equivalent). Whether a file has embedded ids is decided once, on the parquet schema at any depth like java's ParquetSchemaUtil.hasIds, and shared with the pipeline's projection branch so projection and filtering cannot pick different id sources. Duplicate field ids and malformed field-id metadata are rejected instead of silently resolved; only predicate-referenced ids are recorded. - Evaluation: a variant reference used to fall into the missing-column arms: exact null semantics gave silently wrong rows in the arrow row filter, and with row selection enabled the page-index evaluator's not_null skipped every row. IS NULL / IS NOT NULL are now evaluated exactly in the row filter: the group's first leaf is projected and validity is AND-ed down the group's struct path, so nested variants (s.v) and null ancestors are handled like java's null-layer accessors. The resolved storage is validated to be a variant group (binary metadata child); an id resolving to a leaf, or a nested reference unresolvable on an id-less file (name mapping is top-level-only), errors instead of silently taking missing-column semantics. Filter-batch positions are computed per distinct projected root. Variant references reject the remaining operators in one place (bound_reference; unreachable via bind). The page-index and row-group metrics evaluators treat variant references as might-match, like java's ParquetMetricsRowGroupFilter; the row-group evaluator now rejects un-rewritten NOT like the page-index one, instead of inverting conservative answers into prune-everything. An empty filter projection is left empty: batches still carry the row count, and padding an arbitrary leaf broke map-first files and zero-leaf files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
25c6b62 to
d294ab2
Compare
Stacked on top of #222 → #224 → #225 (moved to the top of the stack so the other three can merge independently; content unchanged, only rebased). A variant column's field id lives on its parquet storage group; the
metadata/valueleaves are id-less. That broke three legs of the scan predicate path, each of which resolved references through leaf ids only. This PR restructures the path into three layers with one field-id resolution shared by every consumer. The latest revision folds in the fixes from a full self-review pass (all mechanisms adversarially verified against sabotaged code).Bind
Schema::build_accessorsskipped variant fields, so binding any predicate on a variant column failed with "Accessor for Field not found". Variant fields now get a position-only accessor at every struct level, like java'sAccessors; reading the value as a datum errors.IS NULL / IS NOT NULL are the only operators that bind on a variant column — literal conversion (
Datum::to) and the NaN float check already reject the rest, matching java'sUnboundPredicate#bind— pinned bytest_bind_predicates_on_variant_column. Null-check constant folding now also requires every ancestor to be required (java'sallAncestorFieldsAreRequired): previously a required nested field under an optional struct foldedIS NULLtoAlwaysFalseand dropped the ancestor-null rows at plan time.Field id resolution (one walk instead of four maps)
The per-layer maps (
build_field_id_map, arrow-schema map, position-fallback map) each answered "where is this field id" on their own, all keyed on leaf ids — a variant resolved nowhere, and on id-less (migrated) files nowhere twice. They are replaced by one per-fileFileFieldResolutionbuilt by a single walk over the parquet tree, which alone does the leaf numbering (no hand-rolled arrow traversal left to drift fromfilter_leaves); only the id source differs:ParquetSchemaUtil.hasIds, and shared with the pipeline's projection branching — projection and filtering can no longer pick different id sources for the same file);MessageType; the arrow schema is this port's equivalent).Duplicate field ids and malformed field-id metadata are rejected instead of silently last-wins/dropped; only predicate-referenced ids are recorded.
Evaluation
s.vbinds and evaluates through the ancestor chain (a NULLsmakess.vNULL), the columnar equivalent of java's null-layer accessors.metadatachild (checked for filter-only references, not just projected ones), and any nested reference unresolvable on an id-less file (this port's name mapping is top-level-only; java's is path-recursive — the pre-PR behavior was a loud bind failure, and silent missing-column constants would return wrong rows for a present column).bound_reference).ParquetMetricsRowGroupFilter's variant carve-out. The row-group evaluator now rejects un-rewrittenNOTlike the page-index one already did (java rewrites NOT at the filter boundary): itsnot()inverted conservative might-match answers into prune-everything.Tests
test_variant_null_predicates_evaluate_exactly:[2]/[1]/[]across {embedded ids, name-mapped, position-fallback} × row_selection {off, on}.test_nested_variant_null_predicates_evaluate_exactlyandtest_two_variants_under_one_struct_root: ancestor-null semantics and shared-root batch positioning.test_stray_nested_id_disables_name_mapping_consistently,test_partial_ids_trust_embedded_ids: the single hasIds decision (java semantics) keeps projection and filtering consistent.test_missing_column_predicate_on_map_first_file: constant predicates over missing columns no longer project anything.test_nested_variant_predicate_errors_without_embedded_ids,test_variant_predicate_on_non_variant_storage_errors: the loud-error paths.test_bind_null_checks_consider_optional_ancestors,eval_rejects_not_predicate,test_resolve_file_field_ids(incl. duplicate-id rejection),test_bind_predicates_on_variant_column.[patch]; RW does not exercise the changed paths (it neither pushes variant predicates nor enables row selection).