fix(sql): fix wrong results from queries on renamed Parquet columns - #7318
Conversation
The Parquet row-group bloom-filter pushdown resolved the filtered column by name against the Parquet file's column names. Those names are frozen at conversion time, so a column rename leaves them stale. When another column already bears the query's current name, the pushdown checked the wrong column's bloom filter, got a false negative, and skipped the row group, silently dropping valid rows. The normal data-decode path was unaffected because it maps columns by stable column id (getColumnId, which equals the table writer index), not by name. This change makes the native-table pushdown resolve columns the same way: - PushdownFilterCondition now carries the column's stable writer index. - ParquetFileDecoder.Metadata and ParquetMetaFileReader gain getColumnIndexById(), mirroring PageFrameMemoryPool.buildColumnIdMap's positional fallback for external files without field ids. - prepareFilterList takes a resolveByColumnId flag. Native-table cursors pass true (resolve by id, rename-safe); read_parquet() cursors pass false to keep name resolution, matching how read_parquet projects external files by name. Each pushdown path now resolves columns the same way its data-decode path does.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Remove the unreachable ParquetFileDecoder.Metadata.getColumnIndexById overload. The only callers of the Metadata variant of prepareFilterList are the two read_parquet() cursors, and both resolve columns by name (resolveByColumnId=false), so the by-id branch on the legacy metadata was never taken. Collapse the now-dead ternary in prepareFilterListImpl into a direct ParquetMetaFileReader.getColumnIndexById() call, guarded by an assert documenting that only native-table partitions resolve by id. Broaden the renamed-column regression coverage. The existing test only exercised SYMBOL equality on the bloom-filter path, but the same column resolution governs every pushdown predicate. Add rename-scenario tests for the min/max-stats path: equality, range, BETWEEN, IN, OR-of-equalities, and IS [NOT] NULL. Each test sets up a column name swap so that resolving by the stale parquet name targets the wrong column's statistics, and asserts both that matching rows survive and that genuinely-absent values still prune. Verified that all six new tests, and the existing bloom test, fail when the by-id resolution is reverted to by-name.
Review (level 3, mission-critical pass)Full pass: change-surface map, all agent dimensions (correctness, concurrency, performance/optimality, resources, tests, code quality, metadata, cross-context, adversarial), and per-finding source verification. No Rust files in the diff. Change summary (verified)The bug: native-table Parquet row-group pruning resolved the filtered column against the frozen Parquet column name. After The fix threads the column's stable
The fix is also strictly broader than the reported symptom: it additionally repairs the plain-rename case (old code silently lost pruning) and the drop-then-re-add-same-name case (old code could wrongly prune). CriticalNone. ModerateNone. Minor
Downgraded (false positives, verified and dismissed)
Summary
Generated with a level-3 automated review pass. |
jovfer
left a comment
There was a problem hiding this comment.
LGTM, if we OK to ignore dead code (minor 1 in the review above)
|
@jovfer thanks, dead code was removed
|
|
/azp run macwin |
|
Azure Pipelines successfully started running 1 pipeline(s). |
[PR Coverage check]😍 pass : 22 / 24 (91.67%) file detail
|
|

Fixes #7316
Summary
Equality and IN filters on a renamed column of a Parquet partition could
return too few rows (often zero), silently dropping valid data. The defect
is in Parquet row-group bloom-filter pushdown, not in any index or in the
bloom filter itself.
The pushdown resolves the filtered column against the Parquet file's column
names. Those names are frozen when the partition is converted to Parquet, so
a later
RENAME COLUMNleaves them stale. When some other column alreadycarries the query's current name (common after a chain of renames), the
pushdown looks up that name, lands on the wrong Parquet column, checks that
column's bloom filter, gets a false negative, and skips the whole row group
before any row is scanned. The rows are never returned.
The normal data-decode path was never affected: it maps columns by stable
column id (
getColumnId, which equals the table writer index), so it alwaysreads the correct column regardless of renames. Only the pruning path used
name resolution, which is why a plain
col::string = 'x'(cast suppressespushdown) returned the rows while
col = 'x'did not.Root cause walkthrough
Reproduced from
WalWriterFuzzTest.testCreateTableAsParquet(seeds
1504463487934207962L, 2622578810447144450L). The fuzz table renamedc2 -> new_col_5while a different column had earlier been namednew_col_5.In the Parquet metadata:
c2(id=1) -- the actual symbol column holding'YV'new_col_5(id=15) -- an unrelated columnThe query
WHERE "new_col_5" = 'YV'resolved the namenew_col_5to Parquetindex 10 and tested that column's bloom filter, which does not contain
'YV',so the row group was pruned and the query returned 0 rows. The reference
non-WAL table returned the 6 matching rows.
Fix
Make the native-table pushdown resolve columns by stable id, the same way the
data-decode path already does:
PushdownFilterExtractor.PushdownFilterConditioncarries the column's stablewriter index (column id).
ParquetMetaFileReadergainsgetColumnIndexById(), mirroringPageFrameMemoryPool.buildColumnIdMap'sid < 0 ? positionalfallback forexternal files without field ids. Native-table partitions reach the pruning
path through
ParquetPartitionDecoder.metadata(), which returns aParquetMetaFileReader, so this is the only resolver that runs by id.ParquetRowGroupFilter.prepareFilterListtakes aresolveByColumnIdflag.Native-table cursors (
FwdTableReaderPageFrameCursor,BwdTableReaderPageFrameCursor) passtrueand resolve by id.read_parquet()cursors passfalseand keep name resolution, matching howread_parquet()already projects external files by name incanProjectMetadata.Cleanup
prepareFilterListImplpreviously carried a by-id branch onParquetFileDecoder.Metadataas well, but no caller ever reached it: the onlycallers of the
Metadataoverload are the tworead_parquet()cursors, andboth pass
resolveByColumnId=false. The commit drops the unreachableParquetFileDecoder.Metadata.getColumnIndexById()overload and collapses thedead ternary into a direct
ParquetMetaFileReadercall, guarded by an assertthat documents the invariant.
Effects and tradeoffs
same rows as native partitions. This is the data-loss fix.
read_parquet()behaviour is unchanged: it keeps name-based resolution, whichis the right semantics for arbitrary external files (no QuestDB column ids,
columns matched by name).
name and id point at the same column the result is identical; only the lookup
key changed. Row groups are still pruned for genuinely absent values (covered
by tests that assert the prune still fires).
getColumnIndexByIdis a linear scan over the partition'scolumn list, run once per partition during filter-list preparation, the same
shape as the existing
getColumnIndex(name)scan it replaces. No extra workon the per-row or per-row-group path.
format, statistics, and decode paths are untouched.
Test plan
ParquetRowGroupPruningTest#testBloomFilterSymbolRenamedColumnrenames asymbol column into a name previously held by another column, then asserts
equality on the bloom-filter path returns the right rows and that an absent
value still prunes.
shares the same column resolution:
testRenamedColumnMinMaxEquality,testRenamedColumnRange,testRenamedColumnBetween,testRenamedColumnInList,testRenamedColumnOrEquality, andtestRenamedColumnIsNull(the last coveringthe null-count path for
IS [NOT] NULL). Each swaps two columns' names so thestale parquet name targets the wrong column's statistics, then asserts matching
rows survive and genuinely-absent values still prune.
by-id resolution is reverted to by-name, and pass with the fix.
WalWriterFuzzTest#testCreateTableAsParquetpasses on the reported seeds andacross several fresh random-seed runs.
ParquetRowGroupPruningTest(127),ReadParquetFunctionTest(110),ParquetTest, sqllogicparquet.ParquetTest(46), andAlterTableRenameColumnTest(30) all pass.