Repository navigation
fix(optimizer): keep scans that read virtual columns on ParquetSource - #523
Merged
anoop-narang merged 1 commit intoOct 9, 2026
Merged
Conversation
The liquid read path has no notion of DataFusion's virtual columns. It carries the `TableSchema` across but never produces one, so a scan whose projection includes a virtual column reads back a batch that lacks it, and resolving the column fails with an Arrow schema error naming only the file's own fields. `SELECT file_row_index() FROM t` is enough to hit it: projection pushdown rewrites the function into a row-number virtual column on the scan. Decline the swap for such scans, leaving them on `ParquetSource`, which derives virtual columns from the parquet reader. The guard keys on the virtual columns the scan actually reads (projection unioned with the pushed-down filter), not on what the table declares, so a table that declares a row-number column keeps the cache for queries that never read it. No projection at all reads the whole table schema, virtual columns included. Row numbers are absolute positions in the file; a shifted one would silently attach a positional delete or lineage record to the wrong row, so declining is the sound answer until the liquid reader can generate them itself.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #523 +/- ##
=======================================
Coverage ? 81.16%
=======================================
Files ? 80
Lines ? 13414
Branches ? 13414
=======================================
Hits ? 10888
Misses ? 2201
Partials ? 325 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
anoop-narang
added a commit
to hotdata-dev/liquid-cache
that referenced
this pull request
Oct 9, 2026
…datafusion-contrib#523) (#59) Queries that read a DataFusion virtual column fail once the liquid cache is on. ```sql SELECT file_row_index() AS pos, id FROM t ``` Plain DataFusion returns each row's position in its file. Through `LiquidCacheLocalBuilder` it errors: ``` SchemaError("Unable to get field named \"__datafusion_file_row_index\". Valid fields: [\"id\"]") ``` ## Why `file_row_index()` is answered by the parquet reader: projection pushdown rewrites it into a row-number virtual column (arrow's `RowNumber` extension type) on the scan's `TableSchema`, and `ParquetSource` asks the parquet reader to produce it. Table providers can declare such columns directly too, for example to apply positional deletes. `LocalModeOptimizer` swaps every parquet scan to `LiquidParquetSource`. The liquid read path carries the `TableSchema` across but never produces a virtual column, so the batch comes back without it and resolving the column fails. ## The fix `convert_parquet_scan` declines the swap when the scan reads a virtual column, so the scan stays on `ParquetSource` and logs a `BYPASS` line at info. The check covers what the scan actually reads (the projection, unioned with the pushed-down filter), not what the table declares, so a table carrying a row-number column keeps the cache for queries that never read it. A scan with no projection reads the whole table schema and is declined. Declining rather than producing positions in the liquid reader is deliberate for now: a row number is an absolute position in the file, and a plausible-but-shifted one would silently attach a delete or lineage record to the wrong row. Generating them in the liquid reader would be the follow-up that lets these scans use the cache. The filter half of the check is defensive. DataFusion 55's `ParquetSource::try_pushdown_filters` already refuses predicates that reference a virtual column, so today it should not trigger; it covers a source that does push one down, since the liquid reader would fail rewriting it against the file schemas. ## Tests - `datafusion-local/src/tests/virtual_column.rs`: runs the `file_row_index()` query above (12000 rows, spanning batches) through plain DataFusion and through the liquid cache, twice, and compares results. - `optimizers` unit tests: the guard keys on what is read, not what is declared (projection, filter, and no-projection cases), and `convert_parquet_scan` keeps a virtual-column scan on `ParquetSource` with an ordinary scan as a positive control. With only the new test applied to `main`: ``` $ cargo test -p liquid-cache-datafusion-local virtual_column test tests::virtual_column::file_row_index_matches_datafusion ... FAILED called `Result::unwrap()` on an `Err` value: ArrowError(SchemaError("Unable to get field named \"__datafusion_file_row_index\". Valid fields: [\"id\"]"), Some("")) ``` With the fix, `liquid-cache-datafusion` (62) and `liquid-cache-datafusion-local` (32) pass, and clippy is clean with `-D warnings`.
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.
Queries that read a DataFusion virtual column fail once the liquid cache is on.
Plain DataFusion returns each row's position in its file. Through
LiquidCacheLocalBuilderit errors:Why
file_row_index()is answered by the parquet reader: projection pushdown rewrites it into a row-number virtual column (arrow'sRowNumberextension type) on the scan'sTableSchema, andParquetSourceasks the parquet reader to produce it. Table providers can declare such columns directly too, for example to apply positional deletes.LocalModeOptimizerswaps every parquet scan toLiquidParquetSource. The liquid read path carries theTableSchemaacross but never produces a virtual column, so the batch comes back without it and resolving the column fails.The fix
convert_parquet_scandeclines the swap when the scan reads a virtual column, so the scan stays onParquetSourceand logs aBYPASSline at info. The check covers what the scan actually reads (the projection, unioned with the pushed-down filter), not what the table declares, so a table carrying a row-number column keeps the cache for queries that never read it. A scan with no projection reads the whole table schema and is declined.Declining rather than producing positions in the liquid reader is deliberate for now: a row number is an absolute position in the file, and a plausible-but-shifted one would silently attach a delete or lineage record to the wrong row. Generating them in the liquid reader would be the follow-up that lets these scans use the cache.
The filter half of the check is defensive. DataFusion 55's
ParquetSource::try_pushdown_filtersalready refuses predicates that reference a virtual column, so today it should not trigger; it covers a source that does push one down, since the liquid reader would fail rewriting it against the file schemas.Tests
datafusion-local/src/tests/virtual_column.rs: runs thefile_row_index()query above (12000 rows, spanning batches) through plain DataFusion and through the liquid cache, twice, and compares results.optimizersunit tests: the guard keys on what is read, not what is declared (projection, filter, and no-projection cases), andconvert_parquet_scankeeps a virtual-column scan onParquetSourcewith an ordinary scan as a positive control.With only the new test applied to
main:With the fix,
liquid-cache-datafusion(62) andliquid-cache-datafusion-local(32) pass, and clippy is clean with-D warnings.