Skip to content

Table partitioning optimizations in Parquet and other formats#14

Merged
tsafin merged 44 commits into
masterfrom
tsafin/parquet_partitioning
Jul 18, 2026
Merged

Table partitioning optimizations in Parquet and other formats#14
tsafin merged 44 commits into
masterfrom
tsafin/parquet_partitioning

Conversation

@tsafin

@tsafin tsafin commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Staged, measured optimization plan for TPC-H/TPC-DS generation throughput (docs/parallel_partitions_opts.md), executed phase by phase with a hard correctness gate (independent PyArrow read, schema, aggregates, ordered row hash) before any performance claim is accepted. Every phase below was actually implemented and benchmarked — nothing is estimated or assumed.

Optimization results waterfall

Ideas tried, briefly

  • Batch size tuning (Phase 1) — expose Arrow RecordBatch row count as a CLI flag instead of a hard-coded 8192, sweep 8192-262144.
  • Buffered Parquet output (Phase 2) — wrap the Parquet sink in arrow::io::BufferedOutputStream to coalesce small writes.
  • Largest-table-first scheduling (Phase 3, 15) — reorder the all-table parallel queue so the biggest table starts first (LPT), instead of smallest-first.
  • Bounded producer/consumer pipeline (Phase 4, 13) — overlap dbgen+convert of batch N+1 with Parquet encode/write of batch N via a bounded SPSC queue; later extended to TPC-H's --parallel path and all of TPC-DS.
  • Official dbgen partition equivalence (Phase 5) — establish a correct, byte-exact way to split lineitem generation into N deterministic partitions (seed-snapshot-per-partition, since dbgen's own set_state() jump-ahead doesn't reproduce this embedded build's RNG stream).
  • Parallel partition files (Phase 6) — fork N worker processes, each generating one partition to its own Parquet file.
  • Adaptive partition/worker count (Phase 7) — auto-select partition count from worker count instead of a fixed policy.
  • Merge partitions into one ordered Parquet file (Phase 8) — patch Thrift footer offsets and concatenate partition mini-files into a single valid Parquet file, avoiding N-files-per-table output.
  • Fuse dbgen→Arrow conversion (Phase 9) — skip the memcpy step between intermediate row vectors and final Arrow buffers by wrapping vectors directly (Buffer::Wrap) instead of copying.
  • Narrow physical types (Phase 10) — use 32-bit keys instead of 64-bit where a scale factor's key range fits, to reduce memory traffic and Parquet encoding size.
  • TPC-DS batch-row tuning (Phase 12) — same idea as Phase 1, applied to the shared 24-table TPC-DS generation path.
  • Writer-specific buffering (Phase 14) — extend Parquet's buffering to Paimon/Iceberg's internal data-file writer; audited CSV/ORC/Lance for the same opportunity.
  • Deterministic partitioning beyond lineitem (Phase 16 + follow-up) — investigate/implement partition support for partsupp and TPC-DS fact tables.
  • Writer-specific async I/O (Phase 17) — extend io_uring injection to Paimon/Iceberg's data-file writer; reconciled CSV's separate AsyncIOContext mechanism.
  • Stale item re-checks (post-Phase-18) — re-verify four previously-deferred items (io_uring+buffering composition, narrow types, Lance shared io_uring anchor, ORC adapter) against the current build/API state.

Results

# Idea Measured delta Outcome
1 Batch size 8192→65536 −17.3% (20.98s→17.35s, SF=5 lineitem) Accepted, new default
2 Buffered Parquet output, 32 MiB −3.7% to −6.1% depending on size Accepted, new default (32 MiB)
3 Largest-table-first scheduling (TPC-H) −1.5% (within noise) Accepted, measured neutral
4 Bounded pipeline, capacity 1 −26.3% (17.53s→12.92s) Accepted, new default
5 Serial dbgen partition equivalence N/A (correctness foundation) Accepted (enables Phase 6)
6 Parallel partition files (lineitem) +18.0% slower (7.2s fixed planning prepass eats the gain) Opt-in only, not default
7 Adaptive partition/worker count <5%, no consistent winner No policy adopted
8 Merge partitions into one ordered Parquet file +86.5% slower than single-process baseline Mechanism built, tested, kept as opt-in library call; not wired to CLI
9 Fuse dbgen→Arrow conversion (Buffer::Wrap, real code + benchmark) −0.03%, no measurable difference Rejected; verified opt-in --true-zero-copy kept (byte-identical, harmless)
10 Narrow 32-bit physical types (real code + benchmark) −9.98% (17.13s→15.42s, SF=5 lineitem, interleaved-pairs protocol) Accepted as opt-in --narrow-keys; kept off by default (changes public schema)
12 TPC-DS --batch-rows tuning per-table best 13-16% faster, but inconsistent winner across tables Flag added, default unchanged (8192)
13 Common bounded write pipeline, extended TPC-H orders ≈ −10%; TPC-DS store_sales ≈ −25 to −30% Accepted, capacity 1
14 Paimon/Iceberg output buffering 0-3% (noise) Opt-in API kept, off by default
15 TPC-DS all-table LPT scheduling −16.2% at --parallel-tables 4; neutral at 8 Accepted, new default
16 Deterministic partitioning for partsupp/TPC-DS +14.5% (2 partitions) to −3.9% (4 partitions, within noise) Rejected; code kept opt-in
17 Paimon/Iceberg io_uring injection Paimon +3% slower; Iceberg 16-38% variance, no repeatable direction Opt-in API kept, off by default
Stale item re-checks io_uring+buffer composition: +1.8%, neutral No code, docs-only

Net effect on the stated objective (full lineitem SF=5, single-process default path): Phases 1+2+4 together move the median wall time from 27.33s to 12.96s — roughly a 2.1x speedup (−52.6%), verified byte-identical/hash-identical to the original reference at every step. Multi-table scheduling and pipeline fixes (Phases 3, 13, 15) add smaller, workload-dependent gains (0-30%) on top of that for constrained-concurrency or TPC-DS scenarios. Every idea aimed at intra-table parallelism (partition files, ordered merge, partsupp/TPC-DS partitioning) failed to beat the already-fast single-process path in this environment — a fixed per-run planning/prepass cost (7.2s for lineitem, 3.8s for partsupp) and WSL2's virtio-blk disk bottleneck consistently ate any parallel throughput gain. Phase 10's narrow-keys mode is the one exception among the opt-in, non-default mechanisms: it is a real, measured ~10% win, kept opt-in only because it changes the public Parquet schema, not because it failed to perform.

Note on #9/#10: both were originally evaluated by profiling only. Both have since been implemented for real (--true-zero-copy, --narrow-keys) and benchmarked at SF=5 with full correctness gates (byte-identical/ordered-equal output, ctest 65/65). #9's profiling-based rejection held up under real measurement (−0.03%, no effect). #10's profiling-based rejection did not hold up — the real implementation is ~10% faster, because narrowing shrinks Parquet's encoded page bytes (a size effect the CPU-only profile couldn't see), not because it reduces conversion CPU time.

Test plan

  • Full ctest suite green after every accepted phase
  • scripts/verify_lineitem.py (schema, aggregates, grouped counts, ordered SHA-256 hash) passes against the SF=5 baseline for every accepted default change
  • Independent PyArrow read + Table.equals() ordered comparison for every partition/merge experiment
  • All-table SF=1 and SF=5 regression across every enabled writer (CSV, Parquet, ORC, Paimon, Iceberg, Lance)
  • Phase 9/10 real-code retest: SF=1/SF=5 byte-identical or cast-equal output, ctest 65/65, interleaved-pairs benchmark protocol for Phase 10's ambiguous initial sweep

tsafin and others added 30 commits July 15, 2026 01:13
Adds the staged optimization plan (docs/parallel_partitions_opts.md) and a
reusable lineitem verification script (scripts/verify_lineitem.py) that emits
row count, min/max keys, grouped counts, exact numeric sums, and a stable
ordered row hash for correctness gating of every later phase.

Baseline (lineitem SF=5, zstd, zero-copy, --max-rows 0), median of 5
sequential runs: 27.33s wall, 29,999,795 rows, 1.366 GB, ~190 MB peak RSS.
Output is byte-identical across runs; wall-time variance is WSL2 disk-stall.
Exposes the previously hard-coded RecordBatch size (DBGEN_BATCH_SIZE and
the scattered 10000/8192 literals) as a single --batch-rows CLI option and
threads it through every generation path.

SF=5 lineitem sweep (median of 3 sequential runs): 65536 is 17.3% faster
than the old 8192 default (17.35s vs 20.98s), the only size within 3% of
the best. Sizes >=131072 regress on WSL2 due to larger write bursts.

Batch size is output-invariant (identical ordered hash across batch sizes);
full SF=5 output matches the baseline reference exactly.

Also fixes a tuple-vs-list false mismatch in verify_lineitem.py's schema
comparison and adds it as the reusable correctness gate.
…d Parquet output

Phase 2 of docs/parallel_partitions_opts.md. Wrap the Parquet sink in an
Arrow BufferedOutputStream (configurable, 0 = disabled). SF=5 lineitem sweep:
64 MiB fastest (16.56s) but 32 MiB (16.98s) captures nearly all the gain with
lower memory, ~4% median improvement over unbuffered (17.64s). Output remains
byte-identical (1,366,231,370 bytes, hash unchanged, 29,999,795 rows).
… path

Reorder generate_all_tables_parallel() table queue to longest-processing-time-
first. Measured neutral on WSL2 (disk-bandwidth-bound, -1.5% within noise) but
kept as the theoretically correct zero-risk default; single-table path
unchanged. See docs/parallel_partitions_opts.md Phase 3.
…capacity 1

Add --pipeline-queue-capacity: a bounded SPSC queue (BatchQueue) plus a
PipelinedWriter decorator that runs a consumer thread for Parquet
encode/compress/write while the producer thread continues generation and
conversion. The zero-copy converter allocates self-owned Arrow buffers, so
batches are lifetime-safe across the thread boundary. Preserves FIFO row
order, bounds memory to O(capacity*batch), and propagates writer failures
without deadlock (close wakes both sides).

SF=5 lineitem: 17.53s -> 12.92s median (-26%) at capacity 1; capacity 1 is the
smallest within 3% of the best, so it is the new default. Output is
byte-identical to the sequential path; aggregates and ordered hash match the
baseline reference. Peak RSS unchanged (~270 MB). Adds 6 pipeline unit tests
(order, capacity bound, close-wakes-both, error propagation).
Capture exact legacy RNG boundary states so lineitem parent ranges can be generated independently without changing the retained logical dataset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Record full-scale equivalence evidence and reopen the parallel partition phases now that the retained dataset can be partitioned exactly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fork isolated lineitem workers from one canonical partition plan so multi-file generation preserves exact row order while bounding process concurrency and publishing outputs atomically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the exact SF=5 correctness gate and show that the serial snapshot-planning cost keeps parallel files opt-in despite faster worker execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Record that fixed partition policies are within measurement variance and that serial planning overhead prevents a defensible automatic policy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Record the public Arrow metadata API limitation that prevents safely relocating all Parquet page and index offsets into one ordered file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Record the final SF=5 profile showing dbgen text generation and compression dominate while row materialization and conversion account for only 3.88% of samples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Record that the final profile does not justify changing the compatible int64 schema into an explicit narrow physical type mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the remaining Phase 7 placeholder with its committed evaluation hash so every phase has a terminal documented result.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SF=1 triage across 8 TPC-H tables x 6 formats and 24 TPC-DS tables x 6
formats, all 144+48 combinations succeeded with matching row counts.
Fixed batch_pipeline_test stale-discovery issue by rebuilding the target.
Measurement only, no production code changed.
Exposes the previously hard-coded 8192-row batch threshold in
run_generation() as a validated CLI flag, covering all 24 TPC-DS
tables through the shared dispatch path. Measured SF=5 store_sales
and catalog_sales across 8192-131072; neither table's optimum clears
the acceptance band by enough to justify a new shared default, so
8192 is kept and the flag is left available for per-invocation tuning.
Extend the Phase 4 bounded generation/write pipeline to the --parallel
all-table path, which was missing it (only the single-table CLI path
wrapped the writer). See docs/parallel_partitions_opts.md Phase 13.
Generalize the TPC-H Phase 4 BatchQueue/PipelinedWriter decorator to
TPC-DS by wrapping the writer at both the single-table and per-table
child call sites. Default capacity 1, matching TPC-H. See
docs/parallel_partitions_opts.md Phase 13.
…iles

Wraps only the internal Parquet FileOutputStream in a BufferedOutputStream when requested; manifest, snapshot, and OPTIONS/schema metadata files are untouched.
…files

Wraps only the internal Parquet FileOutputStream in a BufferedOutputStream when requested; Avro manifest, manifest-list, and metadata JSON files are untouched.
Reuses --parquet-output-buffer-bytes to wrap Paimon/Iceberg's internal
Parquet data-file streams, same mechanism as ParquetWriter.
New flag wraps each writer's internal Parquet data-file stream in a
BufferedOutputStream, mirroring the TPC-H mechanism. Default 0 (disabled).
Paimon/Iceberg buffering added but rejected as default (within noise
of no-op); CSV/ORC/Lance investigated and correctly left unchanged.
Small-first ordering tails the heaviest fact tables at the end of a
constrained worker window, delaying their start. Largest-estimated-work-first
(measured SF=5 single-process elapsed time) starts heavy tables immediately.
tsafin and others added 10 commits July 16, 2026 00:10
Phase 6's own overhead (~7.2s snapshot prepass) already made partitioned
lineitem slower than compatibility mode; partsupp and flat TPC-H tables
have far smaller baselines and would be dominated by the same fixed cost.
TPC-DS has no snapshot/restore primitive at all, and its own jump-ahead
facility shares the same divergence risk already rejected for TPC-H.
… writes

Same per-child-process ring pattern already used for Parquet's zero-copy
path; wired into both executables behind the existing --io-uring flag.
Mirrors lineitem's serial-partition snapshot mechanism for partsupp,
whose fixed SUPP_PER_PART child count needs no per-parent counting
pass, to re-measure Phase 16's partsupp rejection with real numbers
instead of an extrapolation from lineitem's prepass cost.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ment

Phase 16 rejected partsupp partitioning by dividing lineitem's ~7.2s prepass
overhead into partsupp's runtime, without ever measuring a partsupp-specific
prepass. Built one and measured it directly at SF=10: prepass cost is ~3.7s
(not lineitem's 7.2s), confirming the fixed-child-count hypothesis, but the
corrected real numbers still show no repeatable win (2/2 regresses +14.5%;
4/4 is within baseline noise). Rejection stands, now on measured grounds.
Vendors Thrift-generated parquet::format types (header-only; libparquet.so
already exports the same symbols, avoiding an ODR double-free from
duplicating its static globals) to patch row-group byte offsets by a
cumulative delta and merge independently-encoded partition mini-files into
one ordered Parquet file without decoding or re-encoding payload bytes.
Mechanism works correctly (byte-identical merged output, full SF=1/SF=5
correctness via verify_lineitem.py) but total wall time is 86.5% slower
than the single-process baseline, since it layers on top of Phase 6's
already-slower partitioned generation. Accepted as a verified opt-in
mechanism, rejected as default.
@tsafin
tsafin requested a review from Copilot July 16, 2026 09:52
@tsafin

tsafin commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

@claude review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the project’s “parallel partitions” optimization work by adding bounded generation→write pipelining, configurable batch sizing and output buffering, deterministic dbgen partition support (including serial/parallel partition generation paths), and an opt-in Parquet mini-file footer merge prototype to restore single-file ordered output after partitioned encoding.

Changes:

  • Added a bounded SPSC BatchQueue + PipelinedWriter decorator and wired it into TPCH/TPCDS generation paths.
  • Added/propagated CLI controls for batch sizing, pipeline queue capacity, and Parquet data-file output buffering (including Paimon/Iceberg data-file sinks).
  • Added deterministic partition planning/generation support (lineitem + partsupp) and a prototype Parquet footer merge implementation with tests and licensing notice.

Reviewed changes

Copilot reviewed 23 out of 26 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
third_party/parquet_format/NOTICE.md Documents provenance/licensing and regeneration rules for vendored Parquet Thrift types.
third_party/parquet_format/parquet_types.h Vendored Thrift-generated Parquet format types used by the footer merge prototype.
third_party/parquet_format/parquet_types.tcc Thrift read/write templates used by the footer merge prototype.
tests/parquet_footer_merge_test.cpp Adds unit tests for ordered Parquet mini-file merge correctness.
tests/dbgen_batch_iterator_test.cpp Adds partition-sequence correctness tests (including an opt-in full-scale digest gate).
tests/CMakeLists.txt Registers new gtest executables for the pipeline and footer-merge tests.
tests/batch_pipeline_test.cpp Adds unit tests for bounded queue + pipelined writer behavior.
src/writers/parquet_writer.cpp Adds optional BufferedOutputStream wrapping for Parquet streaming writes.
include/tpch/parquet_writer.hpp Exposes set_output_buffer_bytes() API for ParquetWriter.
src/writers/parquet_footer_merge.cpp Implements Parquet mini-file merge by copying payload bytes and patching Thrift footer offsets.
include/tpch/parquet_footer_merge.hpp Declares MergeParquetFiles() API.
src/writers/paimon_writer.cpp Adds optional buffering and optional io_uring-backed Parquet data-file sinks.
include/tpch/paimon_writer.hpp Exposes buffering and io_uring toggles for PaimonWriter’s internal Parquet data files.
src/writers/iceberg_writer.cpp Adds optional buffering and optional io_uring-backed Parquet data-file sinks.
include/tpch/iceberg_writer.hpp Exposes buffering and io_uring toggles for IcebergWriter’s internal Parquet data files.
src/tpcds_main.cpp Adds batch sizing / pipeline capacity / output buffering flags and updates table scheduling order.
README.md Documents new tpcds_benchmark --batch-rows option.
src/main.cpp Adds TPCH flags for batch/pipeline/buffering and adds partitioned parallel-files execution paths.
src/dbgen/dbgen_wrapper.cpp Adds seed snapshot semantics and partition planning/generation + partition batch iterators.
include/tpch/dbgen_wrapper.hpp Declares partition descriptor types and partition batch iterator APIs.
src/dbgen/dbgen_stubs.c Adds seed state copy/set helpers; makes seed snapshot capture idempotent.
include/tpch/batch_pipeline.hpp Introduces BatchQueue and PipelinedWriter implementation.
scripts/verify_lineitem.py Adds an independent correctness gate tool for deterministic aggregates/hashes over lineitem Parquet outputs.
docs/parallel_partitions_opts.md Adds/updates extensive design + measurement notes for the optimization plan and phases.
CMakeLists.txt Links thrift and wires the footer-merge source and include paths into tpch_core.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +91 to +105
void ValidateSchemaMatches(const parquet::format::FileMetaData& reference,
const parquet::format::FileMetaData& candidate,
const std::string& path) {
if (reference.schema.size() != candidate.schema.size()) {
throw std::runtime_error("MergeParquetFiles: schema mismatch (field count) in " + path);
}
for (size_t i = 0; i < reference.schema.size(); ++i) {
const auto& a = reference.schema[i];
const auto& b = candidate.schema[i];
if (a.name != b.name || a.type != b.type || a.__isset.type != b.__isset.type) {
throw std::runtime_error("MergeParquetFiles: schema mismatch at field " +
std::to_string(i) + " in " + path);
}
}
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b3fbdfa: ValidateSchemaMatches now uses Thrift's generated SchemaElement::operator== (covers repetition_type, logicalType, converted_type, field_id, precision/scale, num_children, all respecting __isset) instead of the name/type-only check.

Comment on lines +183 to +192
parquet::format::FileMetaData merged;
merged.__set_version(files[0].metadata.version);
merged.__set_schema(files[0].metadata.schema);
if (files[0].metadata.__isset.column_orders) {
merged.__set_column_orders(files[0].metadata.column_orders);
}
if (files[0].metadata.__isset.created_by) {
merged.__set_created_by(files[0].metadata.created_by);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b3fbdfa: the merged footer now copies key_value_metadata from the first input file when present (preserves Arrow's ARROW:schema entry and any other user metadata). encryption_algorithm/footer_signing_key_metadata weren't added since none of this prototype's writers produce encrypted Parquet files (no encryption support exists anywhere in this codebase); happy to add pass-through for those too if there's a concrete use case, but didn't want to add untested code paths for a feature that isn't otherwise supported.

Comment thread tests/batch_pipeline_test.cpp Outdated
Comment on lines +115 to +116
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT_EQ(pushed.load(), 2); // filled capacity, blocked on 3rd

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b3fbdfa: replaced the fixed 50ms sleep with a bounded poll (WaitUntil helper, up to 5s, checking every 2ms) on pushed.load() == 2. Should no longer flake on slow/loaded CI.

Comment thread tests/batch_pipeline_test.cpp Outdated
Comment on lines +120 to +121
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT_EQ(pushed.load(), 3);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b3fbdfa, same WaitUntil polling helper applied to the pushed.load() == 3 assertion after draining one batch.

Comment thread tests/parquet_footer_merge_test.cpp Outdated
fs::path dir_;

void SetUp() override {
dir_ = fs::temp_directory_path() / fs::path("parquet_footer_merge_test_" + std::to_string(::testing::UnitTest::GetInstance()->random_seed()));

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b3fbdfa: temp dir name now includes PID + a static atomic counter + a nanosecond steady_clock timestamp, not just gtest's random_seed(). Should be unique across parallel ctest binaries and repeated runs.

@tsafin

tsafin commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b46b8db10

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/writers/parquet_footer_merge.cpp Outdated
for (size_t i = 0; i < reference.schema.size(); ++i) {
const auto& a = reference.schema[i];
const auto& b = candidate.schema[i];
if (a.name != b.name || a.type != b.type || a.__isset.type != b.__isset.type) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate the complete Parquet schema before concatenating

When inputs have identical field names and physical types but differ in repetition, logical/converted type, field IDs, or nesting, this check accepts them and the merged footer adopts the first file's schema. The later row groups can then be decoded using incompatible level or logical-type metadata, silently corrupting results; compare the complete SchemaElement representation (or reject all non-identical schemas) before copying payloads.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b3fbdfa: same fix as the Copilot comment on this file — ValidateSchemaMatches now compares full SchemaElement equality via Thrift's generated operator== rather than just name/type.

Comment on lines +184 to +185
merged.__set_version(files[0].metadata.version);
merged.__set_schema(files[0].metadata.schema);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve footer key-value metadata during merge

Every successful merge drops the first input's key_value_metadata because the new footer only copies version, schema, column orders, and creator. This loses user metadata and, for files written with Arrow schema storage enabled, can discard the ARROW:schema payload that preserves Arrow-specific schema details such as field metadata and dictionary types; copy the reference footer's key-value metadata into merged.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b3fbdfa: same issue as the Copilot comment on this file — the merged footer now preserves key_value_metadata from the first input, which covers the ARROW:schema case you flagged.

tsafin and others added 4 commits July 17, 2026 00:22
Phase 9: wire up the dormant Buffer::Wrap "true zero-copy" lineitem
converter via --true-zero-copy. Fixes a use-after-free in the dormant
code (PipelinedWriter wrap defeats the ParquetWriter* dynamic_cast,
dropping the batch's lifetime manager before a background thread reads
the wrapped buffers). Measured -0.03% vs standard converter at SF=5 --
confirms the original profile-based rejection.

Phase 10: add --narrow-keys, an opt-in int32 schema for lineitem's four
key columns (orderkey/partkey/suppkey/linenumber), with an overflow
guard for scale factors where keys exceed INT32_MAX. Measured -9.98%
at SF=5 across 10 interleaved runs -- a real, previously-undetected
Parquet-encoding-size effect the CPU-only profile couldn't see. Kept
opt-in since it changes the public schema.
Visualizes the Phase 1/2/4 default-path speedup (27.33s -> 12.96s,
-52.6%, SF=5 lineitem) as a waterfall, plus every other measured idea
(Phase 3/6/8/9/10/13/15/16/17) as a color-coded delta chart. All
numbers pulled directly from docs/parallel_partitions_opts.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ValidateSchemaMatches now compares full SchemaElement equality
  (repetition_type, logicalType, field_id, etc.) instead of just
  name/type, so merges no longer accept mismatched schemas that could
  silently corrupt row-group decoding (Copilot + Codex P1).
- Preserve key_value_metadata in the merged footer, so Arrow's
  ARROW:schema entry (field metadata, dictionary types) survives the
  merge instead of being silently dropped (Copilot + Codex P2).
- batch_pipeline_test: replace fixed-sleep assertions with a bounded
  polling helper to remove CI flakiness under scheduler jitter.
- parquet_footer_merge_test: make the per-test temp directory unique
  via pid+counter+timestamp instead of gtest's random_seed, which can
  collide across parallel ctest binaries.
@tsafin

tsafin commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Addressed all review feedback from Copilot and Codex in b3fbdfa:

  • Schema validation too weak (parquet_footer_merge.cpp): switched from a name/type-only check to Thrift's full generated SchemaElement::operator==, which also covers repetition_type, logicalType, converted_type, scale, precision, field_id.
  • Merged footer dropped key_value_metadata: now preserved from the first input file's footer, so Arrow's ARROW:schema entry (field metadata, dictionary types) survives the merge. Encryption fields (encryption_algorithm, footer_signing_key_metadata) are intentionally still not copied — the merge prototype doesn't support encrypted inputs, and it seemed better to leave them absent than copy metadata that may not apply to the merged file's actual layout. Flagging that decision in case anyone wants encrypted-file support later.
  • Flaky fixed-sleep test assertions (batch_pipeline_test.cpp): replaced both fixed 50ms sleeps in CapacityBoundsInFlightBatches with a bounded polling helper (WaitUntil, checks every 2ms up to 5s).
  • Non-unique temp dir (parquet_footer_merge_test.cpp): directory name now combines PID, a static atomic counter, and a nanosecond timestamp instead of relying solely on gtest's random_seed().

Verified: full rebuild, ctest 65/65 passing, plus the two affected test binaries run individually.

@tsafin
tsafin merged commit 9a971a3 into master Jul 18, 2026
75 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants