Repository navigation
fix(deps): bump DataFusion for upstream fixes to wrong results from filter pushdown, simplification and planning - #14430
Conversation
…ilter pushdown, simplification and planning Pins spiceai/datafusion at spiceai/datafusion#235, which backports seventeen upstream fixes for queries that return wrong rows under Spice's defaults: dynamic filters built from the wrong aggregates or resolved onto a same-named column, `NOT IN` losing its NULL semantics to a pushed filter or a sort-merge join, null-equal joins and `INTERSECT` losing their NULL match, NULL-unsafe constant folding of `log`/`power`, `XOR` and `~ '.*'`, merged nested projections, `LIMIT … OFFSET` under a sort, `COUNT … ORDER BY`, the `TopK` aggregation dropping NULL groups, and cast statistics. Every fix gets a guard that runs its upstream regression query in a session built from Spice's default configuration and fails on the previous pin: `crates/cayenne/tests/datafusion_dynamic_filter_backports_test.rs` for the filter-pushdown fixes, over Parquet and over Cayenne tables, and `crates/runtime-datafusion/src/fork_backport_guards.rs` for the rest. `docs/dev/fork_patches.md` gains a row for each. The pin row stays marked TEMPORARY until the fork PR merges.
✅ Pull with Spice PassedPassing checks:
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The Substrait compliance harness still attributes results to the previous DataFusion revision.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Updates DataFusion to a fork revision containing correctness backports for wrong SQL results.
Changes:
- Bumps all DataFusion dependencies to the backport revision.
- Adds 18 regression guards covering Parquet and Cayenne execution.
- Documents each fork patch and its guard.
| File | Description |
|---|---|
Cargo.toml |
Updates DataFusion workspace pins. |
Cargo.lock |
Locks the new revisions and test dependency. |
docs/dev/fork_patches.md |
Records backports and regression guards. |
crates/runtime-datafusion/Cargo.toml |
Adds tempfile for tests. |
crates/runtime-datafusion/src/lib.rs |
Registers the guard module. |
crates/runtime-datafusion/src/fork_backport_guards.rs |
Tests planning, simplification, and execution fixes. |
crates/cayenne/tests/datafusion_dynamic_filter_backports_test.rs |
Tests pushdown fixes against Parquet and Cayenne. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Regression run on this branch (pin
|
… is built against `DATAFUSION_FORK_REV`, which the harness prints and writes into each result's `datafusion_pin`, still named the previous pin, so results from this build would have been credited to it. The constant and the README's pin row now name the current revision, and a test fails whenever the constant and the workspace's `[patch.crates-io]` pin disagree, so the next move of the pin cannot leave it behind.
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The correctness-critical optimizer backports require final human review after the upstream fork PR merges and the temporary pin is replaced.
Review effort: Balanced
Findings: None
Resolved since last review (1)
…ith a correlated NOT IN patch and scalar-subquery nullability Backported alone, apache/datafusion#25348 made correlated `NOT IN` worse on DataFusion 54.1 than on the current pin: a constant `NOT IN` with an equality correlation dropped rows on a NULL correlation key, and two more correlated shapes failed to plan. spiceai/datafusion#235 now plans a correlated `NOT IN` in a `WHERE` clause as the `NOT EXISTS` it equals there, and backports apache/datafusion#24516 (an empty scalar subquery is NULL) with its prerequisite #23429. Guards: `correlated_not_in_is_answered_as_not_exists`, `an_empty_scalar_subquery_is_null` and `an_in_subquery_over_a_nullable_column_is_nullable` in `fork_backport_guards.rs`, with their `docs/dev/fork_patches.md` rows.
|
Regression run on the new pin (
On the fork, the full sqllogictest suite fails the same 27 files at the same queries as on |
…ports Repoint the [patch.crates-io] revs, Cargo.lock, fork_patches.md, and substrait-compliance pin at e9dc1dd4deed (spiceai/datafusion#235 merge on spiceai-54) and drop the TEMPORARY marker.


📝 Summary
Bumps the
spiceai/datafusionpin toe9dc1dd4deonspiceai-54, the merge of spiceai/datafusion#235, which backports eighteen upstream fixes for queries that return wrong rows and carries a Spice patch for correlatedNOT IN. DataFusion 54.1 has none of them, and Spice's defaults reach each one: dynamic filter pushdown is on for joins,TopKand aggregates, and every Spice session setsparquet.pushdown_filters = true, so a Parquet scan applies a pushed filter row by row, as a Cayenne (Vortex) scan does for every predicate it accepts.Filter pushdown and dynamic filters:
MIN(a), MAX(a), MAX(b), MIN(c + 1): the aggregate's dynamic filter came from the plain-column aggregates alone and pruned the rows holdingMIN(c + 1)TopKdynamic filter pushed below an operator with two same-named columns was resolved by name onto the wrong one: the join lost its only match, orORDER BY p.amount LIMIT 1returned the wrong rowx NOT IN (subquery)returned rows although the subquery holds a NULL: the filter pushed into the subquery's scan dropped its NULLsx NOT IN (subquery)kept a NULLxwhen the pushed filter emptied the subquery's scanIS NOT DISTINCT FROMjoins andINTERSECTlost theirNULL = NULLmatchPlanning, simplification and execution:
runtime.query.prefer_hash_join: falseand more than one partition,NOT IN (subquery)was planned as a sort-merge join and a NULL in the subquery stopped excluding rowsTopKaggregation dropped groups whoseMIN/MAXis NULLlog(a, 1),power(a, 0),a XOR a,col ~ '.*'and their kin were folded to constants, wrong where the input is NULLi + 1layers added threeORDER BYover aLIMIT … OFFSETsubquery returned too few rowsCOUNT(a, c ORDER BY b)counted onlya, and a groupedCOUNT(a ORDER BY b)panicked3 NOT IN (subquery)returned rows although the subquery holds a NULLMIN/MAXover a cast column was answered from uncast Parquet statistics(SELECT 1 WHERE FALSE) IS NULLansweredfalse: a scalar subquery that returns no rows is NULL, but was typed with its column's nullabilityThe fork branch also carries #24428 (a
FilterRemapperfast path #25259 builds on), #23429 (theINsubquery nullability #24516 builds on) and five test-only adaptations to 54.1. Every backport is agit cherry-pick -x, with each conflict resolution recorded in its commit message; spiceai/datafusion#235 lists them.A Spice patch for correlated
NOT IN. Backported alone, #25348 made correlatedNOT INworse on 54.1 than it is on the current pin. Its null-aware anti join takes one key and checks the subquery's NULLs over every build row. So a NULL correlation key dropped rows although the subquery held no NULL, a shape the current pin answers correctly. A subquery NULL that the correlation excludes still removed every outer row. Two more shapes failed to plan: a columnNOT INwith an equality correlation, and a constant one with a non-equality correlation. The second is the planning error this PR used to introduce.Upstream fixes these in the null-aware join executor (#25339, #25560), on top of null-aware mark joins 54.1 does not have. spiceai/datafusion#235 instead plans a correlated
NOT INin aWHEREclause as theNOT EXISTSit equals there: a plain anti join onx IS NULL OR y IS NULL OR x = yand the correlation. An uncorrelatedNOT INkeeps the null-aware hash join. Measured against SQLite on 81 queries (9NOT INshapes over 9 datasets with NULLs in the outer value, the subquery's values and the correlation keys):11624fb82d)5ac6b8edb5)d1815c2a37, merged ase9dc1dd4dewith the same tree)A scalar aggregate that the decorrelation groups by the correlation keeps the plan it has on the current pin, which fails to plan, because the grouped form loses the row the aggregate returns over no input. A volatile
xalso keeps its current plan.Not backported, and still wrong on the fork: #24932 and #25234, which build on larger upstream changes (EnsureRequirements, a
time ± intervalresult-type change), and aNOT INunderORorIS NULL, which is evaluated by a mark join that is not null-aware on 54.1 (#25560, on #21585).Reproduction
Every guard runs its fix's upstream regression query in a session built from Spice's default DataFusion configuration (
runtime_datafusion::session_config::get_df_default_config).crates/cayenne/tests/datafusion_dynamic_filter_backports_test.rscovers the filter-pushdown fixes, over Parquet files and, where the shape is reachable, over Cayenne tables whose rows are in Vortex files.crates/runtime-datafusion/src/fork_backport_guards.rscovers the rest. On the current pin (11624fb82d), all 21 fail:On this branch:
test result: ok. 8 passedandtest result: ok. 13 passed.The three guards for the correlated
NOT INpatch and #24516 also fail on #235 without them (5ac6b8edb5), where the correlated constantNOT INwith an equality correlation answersOk([])and its non-equality form fails to plan with "null_aware LeftAnti join requires equi-join keys, but the join has none".The full gate on the pinned head
dec5d25c37(remote sign-off):14189 tests run: 14189 passed (2 slow, 1 flaky), 134 skipped, all 21 guards included, andscripts/check_fork_patches.pyreportsfork-patch ledger: 31 pinned forks, all recorded. The flaky test,cayenne'sschema_evolution_live_decimal_scale_change_drops_statistics, failed its first try with the same panic on trunk's pin (11624fb82d) in two other branches' gates the same day.📚 Docs
docs/dev/fork_patches.md: a row and a guard for each of the eighteen fixes, #23429 and the correlatedNOT INpatch, and a note that fixes cherry-picked from upstreammainare carried like Spice patches until a re-cut lands on a release that has them. The pin row namese9dc1dd4deonspiceai-54.tools/substrait-compliance: the harness'sDATAFUSION_FORK_REVand the README's pin row name the new pin, and a test now fails when they disagree with the workspace pin, so the harness cannot credit its results to a revision it was not built against.👀 Notes for Reviewers
e9dc1dd4de, the rebase-merge of fix: backport upstream fixes for wrong results from filter pushdown, dynamic filters, simplification and planning datafusion#235 onspiceai-54. Its tree is identical to the tested headd1815c2a37's (d6cfcb2cfd), so the measurements above hold for it.spiceai-54has since merged four unparser fixes (fix(unparser): scope a filter on a projection output that cannot be repeated, gated by dialect (refs spiceai/spiceai#12751) datafusion#227, Update to use correct data-components-contrib commit #230, Can't get 'spice run' #231 and v0.1.1-alpha Endgame #232, for An unnamed projection output stays unbindable when the projection becomes a derived table #12751, Unparser emits wrong SQL for a RightMark join: wrong relation, missing mark column, no EXISTS #13022, Federated FULL OUTER JOIN still drops rows when the filtered input is a join rather than a bare scan #12593 and unparser: an EXISTS build-side join key can lose its binding and escape to the outer query #13493). This pin leaves them out; they move the pin in a PR of their own, with their ledger rows and guards.MIN/MAXaggregate no longer carries a dynamic filter, and a null-equal join's pushed filter gainsOR key IS NULL.