fix: surface numRecords from struct-stats-only checkpoints via ScanFile.stats - #2542
Conversation
…le.stats Delta tables written with writeStatsAsJson=false and writeStatsAsStruct=true store file statistics only in add.stats_parsed. The add.stats JSON string is absent from the checkpoint, so ScanFile.stats is null for checkpoint entries. When has_stats_parsed is true, get_add_transform_expr now uses COALESCE(add.stats, ToJson(add.stats_parsed)) instead of just add.stats. This populates the JSON stats string from the struct stats when JSON stats are absent, with zero overhead when JSON stats already exist. Only numRecords is surfaced — minValues/maxValues/nullCount are schema-dependent and already consumed directly as structs by the data-skipping path. Closes delta-io#2541. Partially addresses delta-io#1075.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2542 +/- ##
=======================================
Coverage 88.67% 88.67%
=======================================
Files 182 182
Lines 61722 61730 +8
Branches 61722 61730 +8
=======================================
+ Hits 54729 54737 +8
Misses 4863 4863
Partials 2130 2130 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Thanks for raising this, had a comment:
Given that, would it be cleaner to teach |
Thanks for the suggestion. I looked into reading |
|
I ran a bench framework locally with a 2000-add v2 sidecar checkpoint (
Could we put this behind a flag for now then, similar to We can add Long-term we should still surface We can also just tag this as an issue and merge this PR first if you'd prefer that |
| ) -> ExpressionRef { | ||
| let stats_expr = if skip_stats { | ||
| Arc::new(Expression::Literal(Scalar::Null(DataType::STRING))) | ||
| } else if has_stats_parsed { |
There was a problem hiding this comment.
we might want to mention the coalesce behavior in the doc comment
Thanks for checking that, @DrakeLin. The ~0.26ms/2000 files overhead seems acceptable for log replay given the correctness fix it provides. Regarding the flag suggestion — kernel is usually consumed indirectly via engines (delta-rs in our case), so the flag would need to be threaded through the engine layer and exposed in its API. Looking a bit in delta-rs, that's does not seem like a trivial change for a short-term fix. |
|
/bench |
Benchmark for 97b2f7ctags: `base`
|
What changes are proposed in this pull request?
Delta tables configured with delta.checkpoint.writeStatsAsJson=false and delta.checkpoint.writeStatsAsStruct=true store file statistics only in add.stats_parsed (a native parquet struct). The add.stats JSON string is absent from the checkpoint. During log replay, get_add_transform_expr reads only add.stats, so ScanFile.stats is null for all checkpoint entries. Data skipping via stats_parsed is unaffected — this only impacts consumers
reading the ScanFile.stats JSON string (e.g. for num_records).
When has_stats_parsed is true, get_add_transform_expr now uses COALESCE(add.stats, ToJson(add.stats_parsed)) instead of just add.stats. This populates the JSON stats string from the struct when JSON stats are absent, with zero overhead when they already exist. Only numRecords is surfaced through this path — minValues/maxValues/nullCount are schema-dependent and already consumed directly as structs by the data-skipping path.
Closes #2541. Partially addresses #1075.
How was this change tested?
Added an integration test.