Commit b5c6d72
feat: enable page-level Parquet stats + add rg_partition_prefix_len marker (#6377)
* feat: enable page-level Parquet stats + add rg_partition_prefix_len marker
Foundation for the streaming column-major merge engine workstream.
Switches the writer's default from EnabledStatistics::Chunk to
EnabledStatistics::Page so every newly-written file carries a Column
Index and Offset Index in its footer. Without this, single-RG files
produced by future PRs would have one min/max per file — useless for
selective queries. The default is exposed as a knob
(`ParquetWriterConfig::with_page_statistics`) so callers can opt out
when the footer overhead isn't worth it.
Adds a numeric marker `qh.rg_partition_prefix_len` in the file's KV
metadata and a matching `rg_partition_prefix_len: u32` field on
`ParquetSplitMetadata`. The marker records how many leading sort
schema columns RG boundaries align with: 0 = no claim (legacy default),
N = aligned with the first N sort columns. Single-RG files vacuously
satisfy any prefix; future writers will set N = sort_schema.len().
Compaction scope now includes `rg_partition_prefix_len`. Splits with
different prefix values land in different buckets; the merge engine
validates input files agree on prefix and rejects mismatches at both
the metastore-struct layer and the on-disk KV layer. Until the
streaming engine lands, the merge writer demotes the output's prefix
to 0 because it cannot enforce alignment.
New developer tooling:
- `quickwit_parquet_engine::storage::inspect_parquet_page_stats` library
function returning a structured per-RG / per-column / per-page report,
plus `verify_partition_prefix` for the strong-form alignment check.
- `inspect_parquet` binary in the parquet-engine crate with `--json`,
`--all-pages`, and `--verify-prefix` flags.
Footer-size delta on a representative shape (100K rows × 6 cols):
+19.5% (672 KB → 804 KB). The page index scales with column count,
not data volume, so production-sized 50 MB splits show < 0.3% overhead.
Test count: 367 → 382 (15 new). Clippy/doc/license/log/machete clean.
* fix: preserve rg_partition_prefix_len on single-RG merge outputs
Avoids a compaction-bucket leak that would otherwise appear once PR-3
ships single-RG ingest before PR-6 ships the streaming column-major
merge engine.
Previously, every merge unconditionally set the output's
`rg_partition_prefix_len` to 0, even when the writer happened to
produce a single-RG output that vacuously satisfies any alignment
claim. With single-RG ingest active and merge demoting on every
operation, post-PR-3 ingest splits would leak out of the
`prefix = sort_len` bucket on their first merge and never rejoin
it — newer ingests would not merge with merge outputs.
New rule: predict the output's row group count via
`num_rows.div_ceil(row_group_size)`. If ≤ 1 RG, propagate the inputs'
prefix; otherwise demote to 0. Both the metastore split metadata
(`merge_parquet_split_metadata`) and the file's KV metadata
(`build_merge_kv_metadata`) follow the same rule, so they always
agree about what's on disk.
A `debug_assert!` checks that the prediction matches the actual row
group count returned by `ArrowWriter::close()` — catches a future
config change that adds a byte-based RG threshold and silently
invalidates the KV claim.
`MergeOutputFile` gains a `num_row_groups: usize` field so the
metastore-side rule can be applied without re-parsing the file.
Test changes:
- Rename `test_output_prefix_len_demoted_to_zero` to
`test_output_prefix_len_demoted_when_multi_rg`; pin the demotion
to the `num_row_groups > 1` case.
- New `test_output_prefix_len_preserved_when_single_rg` asserting
the propagation case.
- New `test_merge_demotes_prefix_when_output_is_multi_rg`
exercising the real writer with `row_group_size = 2` and verifying
the file's KV records 0 via the inspector.
- Extend `test_merge_accepts_matching_rg_partition_prefix_len` to
inspector-verify the single-RG output's KV preserves the prefix.
Test count: 382 → 384.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: end-to-end page-index pruning through the metrics query path
Gate-A verification before PR-3 (single-RG ingest cutover): proves that
page-level statistics written by PR-1 are actually consumed by the
production query path for pruning, not just embedded inertly in the
footer.
Findings:
- The metrics read path at `MetricsParquetTableProvider::scan` already
calls `ParquetSource::with_enable_page_index(true)`, so DataFusion
loads the column index + offset index when reading. No new wiring
needed on the reader side.
- DataFusion's `PruningMetrics` (`page_index_rows_pruned`) counter on
`DataSourceExec` is the testable signal — pruned > 0 means pages
were eliminated using their min/max from the column index.
The new integration test
(`quickwit-datafusion/tests/metrics.rs::test_page_index_pruning_via_query`)
builds a single split with two metric_names interleaved, forces the
metric_name column into ~16 pages within one row group, runs
`WHERE metric_name = 'cpu.usage'`, walks the executed plan, and asserts
`page_index_rows_pruned >= 4096` (the rows from the *other* metric)
plus correctness of the returned rows.
Plumbing change: `ParquetWriterConfig::with_data_page_row_count_limit`
exposes Parquet's per-page row count rollover threshold. The size-based
`data_page_size` knob alone can't force multi-page output when
dictionary-encoded columns RLE-compress to a handful of bytes
regardless of row count. Default 0 = unbounded; production behavior
unchanged.
Tests: 14/14 metrics integration tests pass (was 13).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent b68591e commit b5c6d72
15 files changed
Lines changed: 2136 additions & 27 deletions
File tree
- quickwit
- quickwit-datafusion/tests
- metrics_splits
- quickwit-parquet-engine/src
- bin
- merge
- policy
- split
- storage
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
36 | 38 | | |
37 | 39 | | |
38 | 40 | | |
| |||
99 | 101 | | |
100 | 102 | | |
101 | 103 | | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
102 | 230 | | |
103 | 231 | | |
104 | 232 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
42 | 51 | | |
43 | 52 | | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
44 | 63 | | |
45 | 64 | | |
46 | 65 | | |
47 | 66 | | |
48 | 67 | | |
49 | 68 | | |
50 | 69 | | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
51 | 124 | | |
52 | 125 | | |
53 | | - | |
| 126 | + | |
54 | 127 | | |
55 | 128 | | |
56 | 129 | | |
| |||
0 commit comments