Fix correctness bugs: Int32 cast, silent type fallbacks, predicate panics#3
Conversation
…nics Addresses ISSUES.md #2, #3, #13, #19: - Fix Int32→u64 sign-extension in block number reads across all files (scanner stats, weight, block_index). Parquet stores UInt32 as Int32 physical type; reinterpret via u32 to avoid sign-extension (#19) - Remove silent Fallback variant from semi_join TypedExtractor; error on unsupported join key types instead of producing wrong keys (#2) - Add else-error branch to hierarchical make_group_key to prevent unsupported column types from silently collapsing groups (#3) - Cache true_count() result instead of calling it twice per batch in semi_join and hierarchical join loops (#13) - Replace .unwrap() with graceful let-else in predicate evaluate() to return all-false instead of panicking on type mismatch - Pre-build HashSet<Vec<u8>> for FixedSizeBinary InList predicates to avoid per-row-group set reconstruction - Extract read_block_number() helper in block_index.rs to deduplicate the Int32/UInt32/Int64/UInt64 downcast chains Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes several correctness issues in scan-time pruning, join key extraction, and predicate evaluation—primarily around Parquet’s unsigned physical representations and avoiding silent fallbacks/panics during query execution.
Changes:
- Reinterpret Parquet
Int32stats/values asu32when comparing/reading unsigned block numbers. - Remove silent join-key fallbacks by making unsupported key types error out, and avoid redundant
true_count()calls. - Make predicate evaluation resilient to type mismatches (avoid panics) and optimize FixedSizeBinary
INchecks via a prebuilt set.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/scan/scanner.rs | Fixes UInt32 stats reinterpretation for block-range pruning. |
| src/scan/predicate.rs | Prevents predicate panics on downcast mismatch; prebuilds FixedSizeBinary IN sets. |
| src/output/weight.rs | Fixes Int32→u64 sign-extension when reading block numbers/weights. |
| src/output/block_index.rs | Centralizes block-number reading (with unsigned reinterpretation) for indexing/range collection. |
| src/join/semi_join.rs | Removes fallback join-key extraction; unsupported key types now error; avoids double true_count(). |
| src/join/hierarchical.rs | Makes group-key building error on unsupported types; avoids double true_count(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -62,8 +50,13 @@ impl TypedExtractor { | |||
| arrow::datatypes::DataType::Boolean => Self::Boolean(col_idx), | |||
| arrow::datatypes::DataType::FixedSizeBinary(_) => Self::FixedBinary(col_idx), | |||
| arrow::datatypes::DataType::List(_) => Self::ListInt32(col_idx), | |||
There was a problem hiding this comment.
TypedExtractor::new treats any DataType::List(_) as ListInt32, but append() only serializes list elements when the list values are UInt32Array or Int32Array. For other list element types (e.g., List<String>, List<Struct>, List<UInt8>), this will currently write only the list length and then skip element bytes, causing different keys to collide and producing incorrect joins. Validate the list's child type in new() and return an error for unsupported list element types (or extend append() to handle the additional element types you want to support).
| arrow::datatypes::DataType::List(_) => Self::ListInt32(col_idx), | |
| arrow::datatypes::DataType::List(field) => { | |
| match field.data_type() { | |
| arrow::datatypes::DataType::UInt32 | |
| | arrow::datatypes::DataType::Int32 => Self::ListInt32(col_idx), | |
| child_dt => { | |
| return Err(anyhow!( | |
| "unsupported list element type for join key column: {:?}", | |
| child_dt | |
| )) | |
| } | |
| } | |
| } |
| @@ -62,8 +50,13 @@ impl TypedExtractor { | |||
| arrow::datatypes::DataType::Boolean => Self::Boolean(col_idx), | |||
| arrow::datatypes::DataType::FixedSizeBinary(_) => Self::FixedBinary(col_idx), | |||
| arrow::datatypes::DataType::List(_) => Self::ListInt32(col_idx), | |||
| _ => Self::Fallback(col_idx), | |||
| } | |||
| dt => { | |||
| return Err(anyhow!( | |||
| "unsupported join key column type: {:?}", | |||
| dt | |||
| )) | |||
| } | |||
| }) | |||
There was a problem hiding this comment.
TypedExtractor::new now returns an error for unsupported join-key types, but the test module only covers successful join paths. Add a regression test that constructs a batch with an unsupported key column type (e.g., Float64) and asserts semi_join returns Err, so this behavior can’t silently regress back to a fallback or panic.
| @@ -38,9 +38,11 @@ fn make_group_key(batch: &RecordBatch, row: usize, key_indices: &[usize]) -> Gro | |||
| buf.extend_from_slice(&(a.value(row) as u64).to_le_bytes()); | |||
| } else if let Some(a) = col.as_any().downcast_ref::<Int32Array>() { | |||
| buf.extend_from_slice(&(a.value(row) as u64).to_le_bytes()); | |||
There was a problem hiding this comment.
The Int32Array branch in make_group_key uses (a.value(row) as u64), which will sign-extend negative i32 values. If this branch is meant to support Parquet UInt32 columns materialized as Int32Array (same issue fixed elsewhere in this PR), this will produce incorrect group keys and break hierarchical joins. Consider reinterpreting via u32 (e.g., (a.value(row) as u32) as u64) or otherwise documenting/ensuring that Int32 here is truly signed.
| buf.extend_from_slice(&(a.value(row) as u64).to_le_bytes()); | |
| buf.extend_from_slice(&((a.value(row) as u32) as u64).to_le_bytes()); |
| pub(crate) fn build_block_index( | ||
| batches: &[RecordBatch], | ||
| bn_column: &str, | ||
| ) -> FxHashMap<u64, Vec<(usize, usize)>> { | ||
| let mut index: FxHashMap<u64, Vec<(usize, usize)>> = FxHashMap::default(); | ||
| for (batch_idx, batch) in batches.iter().enumerate() { | ||
| if let Some(col) = batch.column_by_name(bn_column) { | ||
| // Type-check once per batch, then iterate with the specific type | ||
| if let Some(a) = col.as_any().downcast_ref::<UInt64Array>() { | ||
| for row in 0..a.len() { | ||
| index | ||
| .entry(a.value(row)) | ||
| .or_default() | ||
| .push((batch_idx, row)); | ||
| } | ||
| } else if let Some(a) = col.as_any().downcast_ref::<UInt32Array>() { | ||
| for row in 0..a.len() { | ||
| index | ||
| .entry(a.value(row) as u64) | ||
| .or_default() | ||
| .push((batch_idx, row)); | ||
| } | ||
| } else if let Some(a) = col.as_any().downcast_ref::<Int64Array>() { | ||
| for row in 0..a.len() { | ||
| index | ||
| .entry(a.value(row) as u64) | ||
| .or_default() | ||
| .push((batch_idx, row)); | ||
| } | ||
| } else if let Some(a) = col.as_any().downcast_ref::<Int32Array>() { | ||
| for row in 0..a.len() { | ||
| index | ||
| .entry(a.value(row) as u64) | ||
| .or_default() | ||
| .push((batch_idx, row)); | ||
| for row in 0..col.len() { | ||
| if let Some(bn) = read_block_number(col.as_ref(), row) { | ||
| index.entry(bn).or_default().push((batch_idx, row)); | ||
| } |
There was a problem hiding this comment.
build_block_index (and similar loops) now calls read_block_number() per row, which re-runs the downcast chain on every iteration. Previously this file downcasted once per batch and then iterated, which avoids a hot-loop cost for large batches. Consider matching/downcasting once per col (e.g., to an enum/closure) and then iterating to keep the correctness fix without the per-row type checks.
Code Review (3-model consensus)🔴 Critical — Build broken: incomplete
|
Addressed review comments
|
…null-safe group keys - Validate list child type in TypedExtractor (UInt32/Int32/UInt16 only) - Refactor block_index.rs to BlockNumberReader enum (downcast once per column) - Handle null group keys in hierarchical joins (Ok(None) → skip row) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ListInt32 arm in TypedExtractor::append only handled UInt32 and Int32 elements. List<UInt16> was accepted by validation but element values were never written — only the list length — causing false join matches between rows with equal-length but different-valued lists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Review — Round 2🟠 High —
|
Missed in the original Int32 cast fix sweep — hierarchical join group keys for Int32 columns (parquet physical type for UInt32) were still sign-extending via `as u64` instead of reinterpreting via `as u32`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Round 2 — both addressed
|
… type mismatches Scalar equality filters were always compiled as ScalarValue::UInt64 regardless of column type, causing type mismatch → zero results on UInt32/UInt16/UInt8 columns. - Add numeric_scalar() that dispatches on metadata ColumnType - Add fallback type coercion in EqPredicate::evaluate for UInt32/UInt64 scalars to handle parquet physical type mismatches (e.g. metadata says UInt32 but parquet stores Int32 or UInt16) - Add regression tests: numeric_scalar dispatch + end-to-end filter on UInt32 column Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nics (#3) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nics Addresses ISSUES.md #2, #3, #13, #19: - Fix Int32→u64 sign-extension in block number reads across all files (scanner stats, weight, block_index). Parquet stores UInt32 as Int32 physical type; reinterpret via u32 to avoid sign-extension (#19) - Remove silent Fallback variant from semi_join TypedExtractor; error on unsupported join key types instead of producing wrong keys (#2) - Add else-error branch to hierarchical make_group_key to prevent unsupported column types from silently collapsing groups (#3) - Cache true_count() result instead of calling it twice per batch in semi_join and hierarchical join loops (#13) - Replace .unwrap() with graceful let-else in predicate evaluate() to return all-false instead of panicking on type mismatch - Pre-build HashSet<Vec<u8>> for FixedSizeBinary InList predicates to avoid per-row-group set reconstruction - Extract read_block_number() helper in block_index.rs to deduplicate the Int32/UInt32/Int64/UInt64 downcast chains Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Addresses ISSUES.md #2, #3, #13, #19:
u32to avoid sign-extension (scanner.rs,weight.rs,block_index.rs). Extractedread_block_number()helper to deduplicate the downcast chains.Fallbackvariant fromTypedExtractor; unsupported join key types now returnErrinstead of producing wrong key bytes.semi_joinandhierarchicaljoin loops..unwrap()withlet-elsereturning all-false on type mismatch. Pre-builtHashSet<Vec<u8>>for FixedSizeBinary InList predicates.Test plan
🤖 Generated with Claude Code