Repository navigation
fix: clear error instead of panic on mismatched column statistics - #25694
jackylee-ch wants to merge 16 commits into
Conversation
count's statistics fast path indexed column_statistics[col.index()] directly, so a TableProvider reporting fewer column statistics than its schema made SELECT count(col) panic. Use .get(idx)? in both branches, matching the guard already in sum.rs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #25694 +/- ##
=======================================
Coverage 82.93% 82.94%
=======================================
Files 1154 1154
Lines 458002 458075 +73
Branches 458002 458075 +73
=======================================
+ Hits 379856 379930 +74
+ Misses 55123 55122 -1
Partials 23023 23023 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
kosiew
left a comment
There was a problem hiding this comment.
Thanks for fixing this. Both COUNT and COUNT(DISTINCT) now handle missing column statistics safely, and the new regression tests cover both paths. The change looks good to me. I have one optional suggestion for additional end-to-end coverage.
| Ok(()) | ||
| } | ||
|
|
||
| #[test] |
There was a problem hiding this comment.
It would be useful to add an end-to-end regression test with incomplete column statistics, NULLs, and duplicates to verify that aggregation is preserved and returns the correct counts. Please test COUNT(b) and COUNT(DISTINCT b) separately, since the optimizer stops at the first unresolved expression, and make sure the column index remains out of bounds after any projection or remapping. This is optional since the existing unit tests already cover both guarded accesses.
There was a problem hiding this comment.
Edit: superseded by d032a22. Short column statistics now fail planning with an error naming the provider, which also covers COUNT(DISTINCT b) (rewritten into a GROUP BY), and the end-to-end test short_column_statistics_are_reported checks that error for count(b) and a plain scan. An earlier version of this reply said test_count_distinct_optimization gained a short-statistics case; that was wrong: the case passed without the guard and was never pushed. The count unit test covers both branches.
A statistics provider that drops the trailing column entry keeps the aggregated column's index out of bounds whatever the projection, so `count`'s statistics fast path is reached from SQL. The query panics on main and returns the correct count with the guard in place.
alamb
left a comment
There was a problem hiding this comment.
Thank you for this PR @jackylee-ch and for the review @kosiew
I feel like this is just papering over a bug and by sliently ignoring the problem we will make debugging harder
Changing the code so it doesn't panic seems fine, but if the table provider returns the wrong number of statistics I think that should return a clear error
|
Agreed — it should be an error. Two candidates: the provider/source boundary, or One thing first: planning-time rules compute statistics ( |
I don't understand what you are asking me What is the "broad gate"? |
`Statistics::column_statistics` must hold one entry per schema field, and consumers index it by column. Check that where `StatisticsContext` receives statistics from an operator or a statistics provider, and fail with an error naming the culprit instead of panicking in a consumer.
`value_from_stats` is public, so a direct caller can still pass short statistics; fold the two `count` cases into one loop. Link `ColumnStatistics` in the `StatisticsProvider` docs.
`min` and `max` index `column_statistics` like `count` did, and `value_from_stats` is public, so use `.get(idx)?` there as well. The end-to-end test now also checks that a plain scan fails planning.
Extra entries never indexed out of bounds; only missing ones did.
`FilterExec` indexes its input's column statistics when it is built, so a table provider returning too few entries also panicked on `WHERE`.
A statistics provider returning the statistics of the whole table for a scan of only `b` used to make `min(b)` and `max(b)` return the minimum and maximum of `a`. Check end to end that planning fails instead, and that both statistics sources reject one entry too many as well as too few. Also pin what file scans accept: with a projection, as all built-in sources have, table and file group statistics of any length, missing trailing entries being unknown and extra ones ignored; without one, exactly one entry per table column. Say so in the upgrade guide.
`FileScanConfig` pads table and file group statistics it is given and ignores extra entries when its source has a projection, so a missing or extra entry before the last column shifts the statistics of the columns after it. Describe that as unchecked rather than as accepted.
`FileScanConfig` looks up the table and file group statistics it is given by column position, so an extra entry anywhere but at the end shifted the statistics of every later column by one, and `min`/`max` could be answered from another column's values. Return an internal error naming the statistics instead. Missing trailing entries are still unknown, which virtual columns and some table providers rely on.
|
Sorry, that was unclear. "Broad gate" meant one check in Too few entries can make filters, joins, |
|
I am sorry -- I still don't get it. How about we start with a clear and concise description of what problem you are solving (from the end user API perspective, not a code description) Describe:
It seems like you are trying to add better error messages when a table provider provides incorrect statistics ? But that is not clear |
The error now reads, for example, "MyExec returned 1 column statistics, expected 2 (one per field of its schema)", instead of starting with "Assertion failed: column_statistics == fields (left: 1, right: 2)". Also drop the FileScanConfig check for extra table and file group column statistics. When its FileSource has a projection, FileScanConfig fills in missing statistics as unknown on purpose, for virtual columns and some table providers, so it needs a different check; only the statistics of plans and statistics providers are checked now. Note on Statistics::default() that it does not fit a schema with fields. In the upgrade guide, use self.schema() in the example and mention Statistics::project for plans that drop or reorder their input's columns.
|
I've updated the description; does it answer your questions? Your guess is right: a clear planning error instead of a panic or a wrong answer. In short:
|
Which issue does this PR close?
analysis.rsbounds check panic #10536, where planning panicked on a user's custom statistics; propagate error instead of panicking on out of bounds in physical-expr/src/analysis.rs #10992 turned only that panic, inExprBoundaries::try_from_column, into an error.Rationale for this change
Extensions can report their own
Statistics: a customExecutionPlan, such as a table provider's scan, or aStatisticsProviderregistered on the session.column_statisticsmust have one entry per field of the plan's schema, but nothing validates this when statistics are reported, and it is easy to get wrong: starting fromStatistics::default()gives no entries, and returning a child's statistics from a plan with fewer columns gives too many.Three downstream projects have hit this:
AggregateExec(index out of bounds: the len is 0 but the index is 0), because the scan fell back toStatistics::default().WHEREpanicked inProjectionExprs::project_statistics, becauseLanceKnnExecreported no column statistics.Internal error: Could not create ExprBoundaries: ... col_index has gone out of bounds, because a wrapper around the scan that hides internal columns passed on its input's column statistics unchanged (FixSchemaCastScanExec's schema conversion infn partition_statisticsspiceai/spiceai#14258).Each failure surfaced far from its cause, as an index out of bounds, with nothing naming the plan that reported the statistics. Rules and operators read a column's statistics by its index, so a mismatch stays hidden until some code reads that column, and a DataFusion upgrade can add such code. Too many entries can also give wrong answers: in a test where a scan of
bgot the statistics of bothaandb,SELECT min(b), max(b)returned the min and max ofa.What changes are included in this PR?
The planner and optimizer rules get plan statistics through
StatisticsContext. It now checks that the statistics of every plan and statistics provider have one entry per field, so a rule or operator that would read wrong statistics gets an error instead, naming the plan or provider that reported them, e.g.MyExec returned 1 column statistics, expected 2 (one per field of its schema). The defaultEnsureRequirementsrule, which adds the repartitions and sorts a plan needs to run, computes the statistics of each operator's input, so even a plainSELECT *over such a plan fails at planning with this error. Every other query we tried, from filters to joins and aggregates, failed the same way, so an extension's own tests should catch the mistake. Padding with unknown statistics instead would hide it.value_from_stats, which computes an aggregate from statistics alone, is public, so callers can pass it statistics that never went through this check, and it returns anOption, so it cannot report an error. Thecount,minandmaximplementations therefore returnNoneon too few entries instead of panicking, assum's already does.Not covered: the table-level and per-file statistics that a table provider passes to a file scan. Files can legitimately lack statistics for some columns (virtual columns have none; delta-rs skips them when there is no predicate to prune with), so a missing entry there is not a bug, and the built-in file scans fill it in as unknown. That layer needs a different rule, allowing missing entries but not extra ones, so this PR keeps to one rule in one place and drops the extra-entries check an earlier version added there. The statistics the scan itself reports are still checked.
What is the testing strategy for this PR?
End-to-end tests in
core/tests/physical_optimizer/aggregate_statistics.rsreproduce too few and too many entries from SQL (onmainthey panic or return the wrongmin/max) and check the new error. Unit tests cover plans and providers reporting too few and too many entries. The check is one length comparison, about 5 ns, each time a plan's statistics are computed. Thecompute_statisticsmicrobenchmarks, which only compute statistics, are up to 7% slower. Total planning time, this PR vsmain(median of 3 runs): TPC-H (21 queries, all but q15) 15.24 vs 15.17 ms; TPC-DS (99 queries) 240.3 vs 240.1 ms.Are there any user-facing changes?
Yes, breaking for extensions: a plan or statistics provider with the wrong number of column statistics now fails planning, even for queries such as
SELECT *that worked before. The upgrade guide describes the fix: start fromStatistics::new_unknown(&schema), or project a child's statistics to the plan's columns withStatistics::project. Please add theapi changelabel.