fix(table): compute COUNT(*) for format tables instead of returning 0 - #624
Merged
JingsongLi merged 1 commit intoJul 28, 2026
Conversation
A format table has no manifest, so planning cannot know how many rows a data file holds. `format_table_scan` recorded that as `row_count: 0`, and 0 is a number a reader is entitled to trust: - `DataSplit::raw_merged_row_count` returned `Some(0)` for a raw-convertible split with no deletion files; - DataFusion turned that into `Precision::Exact(0)` for the whole scan; - the `aggregate_statistics` rule then rewrote `SELECT COUNT(*)` into the constant 0 and never opened a file. `SELECT *` was unaffected because it reads the files, which is why the gap went unnoticed: the same table answered 0 rows and returned rows. The same 0 also silenced column statistics. `ColumnStatsAccumulator::add_file` skips a file whose `row_count` is 0, so every format-table file was treated as empty and the accumulator kept its default `null_count = 0`, handed out as `Precision::Exact` — an exact answer derived from files nobody read. Make "unknown" expressible instead of overloading a legal value. `DataFileMeta::ROW_COUNT_UNKNOWN` is -1 and `row_count_known()` tests for it; `DataSplit::row_counts_known()` reports whether a whole split is known, and `merged_row_count()` returns `None` as soon as one file is not. `row_count()` now sums only known files and is documented as a lower bound rather than a total. Any producer that cannot determine a row count can adopt the same convention; nothing here is specific to format tables. Tests: a format table whose `COUNT(*)` must match the rows a scan returns (0 instead of 5 before this change), an empty table and a table holding an empty file that must still count 0 exactly, and a partitioned table with a partition predicate — that path was already correct, because a pushed-down predicate makes the statistics inexact and the rule does not fire.
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.
Purpose
A format table has no manifest, so scan planning cannot know how many rows a data file holds.
format_table_scanrecorded that asrow_count: 0— and 0 is a number a reader is entitled to trust. Two consumers trusted it:DataSplit::raw_merged_row_count()returnedSome(0), DataFusion took it asPrecision::Exact(0), and theaggregate_statisticsrule rewroteSELECT COUNT(*)into the constant 0 without opening a file.SELECT *was unaffected because it reads the files, so the same table answered 0 rows and returned rows.ColumnStatsAccumulator::add_filereturns early for a file whoserow_countis 0, so every file looked empty and the accumulator's initialnull_count = 0went out asPrecision::Exact— a statistic derived from files nobody read.Approach
Make "unknown" expressible instead of overloading a legal value.
DataFileMeta::ROW_COUNT_UNKNOWNis-1, withrow_count_known()to test it:merged_row_count()returnsNoneas soon as one file is unknown, so no arithmetic over the placeholder ever reaches a caller;row_count()sums only known files and is documented as a lower bound, withrow_counts_known()to ask before presenting it as a total;format_table_scanfills in the sentinel.Nothing here is specific to format tables — any producer that cannot determine a row count can adopt the convention — and a file that genuinely holds zero rows still reports
0and stays exact.The column-statistics path is fixed by the same change, but indirectly:
-1no longer matches therow_count == 0guard, soadd_filefalls through tovalue_stats_for_field, finds no statistics, and marks min/max/null invalid. Making that check explicit (row_count_known()before the== 0early return) would say what is meant rather than relying on the sentinel missing the guard; happy to add it if reviewers prefer.Tests
source.rsunit tests, onmerged_row_count():None, androw_count()is 0 rather than a totalNone,row_count()is the 10 it does knowfirst_row_idsetSome(0)— unknown is a distinct value, not a synonym for emptycrates/integrations/datafusion/tests/format_table_statistics.rs, end to end through SQL:COUNT(*)andCOUNT(id)both equal what a scan returns; 0 before this changepartition_statistics()toocargo test -p paimon --lib source::stays green.Notes
DataSplit::serializewritesrow_countinto the v8 wire format as-is, so a consumer that reads splits planned by this crate needs to understand the sentinel. Flagging in case a cross-language path exists that I am not aware of.file://warehouse currently finds no splits at all:table_pathkeeps thefile:///form while listed status paths come back asfile:/, so thestrip_prefixinpartition_row_from_pathdrops every file. Separate defect, not touched here.