Skip to content

GH-51626: [C++][Parquet] Fix statistics of dictionary-encoded leaves under null parents - #51627

Open
kita-renji wants to merge 1 commit into
apache:mainfrom
kita-renji:GH-51626-parquet-dict-stats
Open

kita-renji wants to merge 1 commit into
apache:mainfrom
kita-renji:GH-51626-parquet-dict-stats

Conversation

@kita-renji

Copy link
Copy Markdown
Contributor

Rationale for this change

WriteArrowDictionary can write wrong Parquet statistics for dictionary<*, string|binary> columns (see #51626):

  1. Statistics are computed from the leaf indices before their validity is replaced with the one derived from the def levels. An index under a null parent (e.g. a null struct row) can be valid in the leaf array, so it is counted as a value and its dictionary value can become min/max.
  2. Unique() returns one null entry when a batch has null indices, so the shortcut that re-uses the whole dictionary for min/max fires when exactly one dictionary entry is unreferenced, and that entry can become min/max.

min/max are marked exact, and readers use them: DuckDB drops rows for s.x IS NULL and miscounts count(s.x), DataFusion returns a value that isn't in the data for min(c), max(c).

What changes are included in this PR?

  • Call MaybeReplaceValidity before update_stats in WriteIndicesChunk, so statistics see the same validity as the encoded data. This is the order WriteArrowDense already uses.
  • Don't count the null entry of Unique() when checking whether all dictionary entries are referenced.

Both are needed: with only the first change, the slots under null parents become null indices and case 2 puts the hidden value back into min/max.

Are these changes tested?

Yes. Two cases added to the NoNullCountWrittenForRepeatedFields (PARQUET-2067) suite in arrow_statistics_test.cc: a struct<dictionary<int32, utf8>> with null rows over valid child slots, and a top-level dictionary with a null and one unreferenced entry. Both fail without the change. All parquet C++ test binaries pass.

I also ran a randomized check (not included) that writes dictionary<string/binary> leaves nested in struct/list/large_list/fixed_size_list (up to two levels), with nulls at every level, values hidden under null parents, slices, several batches, small pages and row groups, dictionary fallback and V1/V2 pages, and compares the written statistics with ones computed from the logical values. On main it finds wrong statistics in most nested shapes (and trips the V2 DCHECK); with this change 4500 random cases match.

Are there any user-facing changes?

Files written with dictionary encoding get correct null_count and min/max for these columns. No API changes.

This PR contains a "Critical Fix". It fixes a bug that writes incorrect statistics, which makes other readers return wrong results. The data itself was always written correctly.

Was AI used for this PR?

In accordance to the AI generation guidelines, please disclose below whether and how AI was used in this PR.

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

🤖 Generated with Claude Code

…eaves under null parents

WriteArrowDictionary computed page statistics from the raw leaf indices,
before MaybeReplaceValidity applied the validity derived from the def
levels. An index slot under a null parent (e.g. a null struct row) could
be valid in the leaf array, so it was counted as a value and its
dictionary entry went into min/max.

Also, Unique() returns a null entry when the chunk has null indices, so
the dictionary re-use shortcut fired when exactly one dictionary entry was
unreferenced, putting that entry into min/max (flagged as exact).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #51626 has been automatically assigned in GitHub to PR creator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants