perf(parquet): forward dictionary encoding through append compaction rewrite - #257
Open
SteNicholas wants to merge 3 commits into
Open
perf(parquet): forward dictionary encoding through append compaction rewrite#257SteNicholas wants to merge 3 commits into
SteNicholas wants to merge 3 commits into
Conversation
SteNicholas
force-pushed
the
PAIMON-230
branch
4 times, most recently
from
August 28, 2026 08:00
42bdc18 to
269f33f
Compare
Collaborator
|
LGTM |
SteNicholas
force-pushed
the
PAIMON-230
branch
from
August 28, 2026 12:27
269f33f to
92d8218
Compare
lxy-9602
reviewed
Aug 29, 2026
SteNicholas
added a commit
to SteNicholas/paimon-cpp
that referenced
this pull request
Aug 31, 2026
…STRING Addresses the review on apache#257. The compaction rewrite no longer turns `parquet.read.enable-dictionary-passthrough` on. It is a table option, off by default, and the rewrite only vetoes it - when the output is not Parquet, when `parquet.enable-dictionary` is false, or when a shredding writer is active - logging the reason at DEBUG for a table that did ask for it. Forwarding trades compaction CPU for output size, and a Parquet column chunk carries one dictionary, so a multi-file rewrite can keep the first input file's and write the rest of the row group plain; whether that trade is worth taking depends on the data, so it is left to the table. Restrict the reader to STRING. Parquet stores BINARY in the same BYTE_ARRAY leaf and dictionary-encodes it the same way, but no value accessor here can read a `dictionary(int32, binary)`: ColumnarUtils::GetView() asserts on it and returns an empty view in a release build, and LiteralConverter rejects it. Since the option applies to every read of the table, the gate gets that restriction, not the layout predicate - ArrowUtils::IsDictionaryLayoutRecoverableValueType() still accepts `utf8|binary`, which is what the writer can recover, so the writer keeps the capability for a future producer. Also from the review: use CastingUtils::Cast() instead of arrow::compute::Cast(); hold the Arrow pool adaptor in DataFileIndexWriter as a member declared before the index writers, so it outlives every buffer allocated through it; drop the batch schema cache in ParquetFormatWriter::ResolveBatchSchema(); read the two Parquet option names in core as local constants rather than through the format layer's headers. The changing-dictionary benchmark now rotates one alphabet and shifts the indices back by the same amount, so it writes the same logical column as its pair and the delta is the fallback rather than a difference in the data.
SteNicholas
force-pushed
the
PAIMON-230
branch
from
August 31, 2026 07:28
92d8218 to
3feaaa1
Compare
SteNicholas
added a commit
to SteNicholas/paimon-cpp
that referenced
this pull request
Aug 31, 2026
…STRING Addresses the review on apache#257. The compaction rewrite no longer turns `parquet.read.enable-dictionary-passthrough` on. It is a table option, off by default, and the rewrite only vetoes it - when the output is not Parquet, when `parquet.enable-dictionary` is false, or when a shredding writer is active - logging the reason at DEBUG for a table that did ask for it. Forwarding trades compaction CPU for output size, and a Parquet column chunk carries one dictionary, so a multi-file rewrite can keep the first input file's and write the rest of the row group plain; whether that trade is worth taking depends on the data, so it is left to the table. Restrict the reader to STRING. Parquet stores BINARY in the same BYTE_ARRAY leaf and dictionary-encodes it the same way, but no value accessor here can read a `dictionary(int32, binary)`: ColumnarUtils::GetView() asserts on it and returns an empty view in a release build, and LiteralConverter rejects it. Since the option applies to every read of the table, the gate gets that restriction, not the layout predicate - ArrowUtils::IsDictionaryLayoutRecoverableValueType() still accepts `utf8|binary`, which is what the writer can recover, so the writer keeps the capability for a future producer. Also from the review: use CastingUtils::Cast() instead of arrow::compute::Cast(); hold the Arrow pool adaptor in DataFileIndexWriter as a member declared before the index writers, so it outlives every buffer allocated through it; drop the batch schema cache in ParquetFormatWriter::ResolveBatchSchema(); read the two Parquet option names in core as local constants rather than through the format layer's headers. The changing-dictionary benchmark now rotates one alphabet and shifts the indices back by the same amount, so it writes the same logical column as its pair and the delta is the fallback rather than a difference in the data.
SteNicholas
force-pushed
the
PAIMON-230
branch
from
August 31, 2026 08:00
5a245dd to
ba3f2cd
Compare
SteNicholas
added a commit
to SteNicholas/paimon-cpp
that referenced
this pull request
Aug 31, 2026
…STRING Addresses the review on apache#257. The compaction rewrite no longer turns `parquet.read.enable-dictionary-passthrough` on. It is a table option, off by default, and the rewrite only vetoes it - when the output is not Parquet, when `parquet.enable-dictionary` is false, or when a shredding writer is active - logging the reason at DEBUG for a table that did ask for it. Forwarding trades compaction CPU for output size, and a Parquet column chunk carries one dictionary, so a multi-file rewrite can keep the first input file's and write the rest of the row group plain; whether that trade is worth taking depends on the data, so it is left to the table. Restrict the reader to STRING. Parquet stores BINARY in the same BYTE_ARRAY leaf and dictionary-encodes it the same way, but no value accessor here can read a `dictionary(int32, binary)`: ColumnarUtils::GetView() asserts on it and returns an empty view in a release build, and LiteralConverter rejects it. Since the option applies to every read of the table, the gate gets that restriction, not the layout predicate - ArrowUtils::IsDictionaryLayoutRecoverableValueType() still accepts `utf8|binary`, which is what the writer can recover, so the writer keeps the capability for a future producer. Also from the review: use CastingUtils::Cast() instead of arrow::compute::Cast(); hold the Arrow pool adaptor in DataFileIndexWriter as a member declared before the index writers, so it outlives every buffer allocated through it; drop the batch schema cache in ParquetFormatWriter::ResolveBatchSchema(); read the two Parquet option names in core as local constants rather than through the format layer's headers. The changing-dictionary benchmark now rotates one alphabet and shifts the indices back by the same amount, so it writes the same logical column as its pair and the delta is the fallback rather than a difference in the data.
SteNicholas
force-pushed
the
PAIMON-230
branch
from
August 31, 2026 08:19
ba3f2cd to
dc2c759
Compare
lszskye
reviewed
Aug 31, 2026
lszskye
reviewed
Aug 31, 2026
SteNicholas
added a commit
to SteNicholas/paimon-cpp
that referenced
this pull request
Aug 31, 2026
Addresses the review on apache#257. `FlattenUnresolvableDictionaries()` decided what to decode from whether a dictionary survives the Arrow C data interface, but what decides it is whether the writer receiving the batch recovers an encoding from the layout at all, and only ParquetFormatWriter does. A reader can hand over a dictionary for reasons the Parquet passthrough option does not govern - a format's own lazy-decoding setting - and those stay on when the rewrite vetoes the passthrough. A `dictionary(int32, utf8)` column could therefore reach a writer that imports it against the logical type, where a dictionary child fails on the buffer count. The built-in ORC reader widens to `dictionary(int64, large_utf8)`, which was already decoded for its shape, so reaching this needs a reader outside this repository. CompactRewrite now derives `preserve_layout_recoverable_dictionaries` from the veto: under a veto every dictionary is decoded before the export, whatever its shape. This also stops a dictionary reaching a Parquet writer whose table sets `parquet.enable-dictionary` to false. Rename ArrowUtils::ResolveParquetDictionaryStructType() to ResolveDictionaryStructTypeFromLayout(), and the file-local IsResolvableDictionary() to IsDictionaryLayoutRecoverable(). Neither is Parquet-specific: they read `ArrowArray::dictionary` and the caller's declared type, and DataFileWriterBase::AddFileIndexBatch runs the former for every format. Moving it into the Parquet layer instead would make core depend on Parquet symbols.
SteNicholas
added a commit
to SteNicholas/paimon-cpp
that referenced
this pull request
Aug 31, 2026
Addresses the review on apache#257. `FlattenUnresolvableDictionaries()` decided what to decode from whether a dictionary survives the Arrow C data interface, but what decides it is whether the writer receiving the batch recovers an encoding from the layout at all, and only ParquetFormatWriter does. A reader can hand over a dictionary for reasons the Parquet passthrough option does not govern - a format's own lazy-decoding setting - and those stay on when the rewrite vetoes the passthrough. A `dictionary(int32, utf8)` column could therefore reach a writer that imports it against the logical type, where a dictionary child fails on the buffer count. The built-in ORC reader widens to `dictionary(int64, large_utf8)`, which was already decoded for its shape, so reaching this needs a reader outside this repository. CompactRewrite now derives `preserve_layout_recoverable_dictionaries` from the veto: under a veto every dictionary is decoded before the export, whatever its shape. This also stops a dictionary reaching a Parquet writer whose table sets `parquet.enable-dictionary` to false. Rename ArrowUtils::ResolveParquetDictionaryStructType() to ResolveDictionaryStructTypeFromLayout(), and the file-local IsResolvableDictionary() to IsDictionaryLayoutRecoverable(). Neither is Parquet-specific: they read `ArrowArray::dictionary` and the caller's declared type, and DataFileWriterBase::AddFileIndexBatch runs the former for every format. Moving it into the Parquet layer instead would make core depend on Parquet symbols.
SteNicholas
force-pushed
the
PAIMON-230
branch
from
August 31, 2026 12:58
d123bf7 to
0237475
Compare
SteNicholas
added a commit
to SteNicholas/paimon-cpp
that referenced
this pull request
Sep 1, 2026
…STRING Addresses the review on apache#257. The compaction rewrite no longer turns `parquet.read.enable-dictionary-passthrough` on. It is a table option, off by default, and the rewrite only vetoes it - when the output is not Parquet, when `parquet.enable-dictionary` is false, or when a shredding writer is active - logging the reason at DEBUG for a table that did ask for it. Forwarding trades compaction CPU for output size, and a Parquet column chunk carries one dictionary, so a multi-file rewrite can keep the first input file's and write the rest of the row group plain; whether that trade is worth taking depends on the data, so it is left to the table. Restrict the reader to STRING. Parquet stores BINARY in the same BYTE_ARRAY leaf and dictionary-encodes it the same way, but no value accessor here can read a `dictionary(int32, binary)`: ColumnarUtils::GetView() asserts on it and returns an empty view in a release build, and LiteralConverter rejects it. Since the option applies to every read of the table, the gate gets that restriction, not the layout predicate - ArrowUtils::IsDictionaryLayoutRecoverableValueType() still accepts `utf8|binary`, which is what the writer can recover, so the writer keeps the capability for a future producer. Also from the review: use CastingUtils::Cast() instead of arrow::compute::Cast(); hold the Arrow pool adaptor in DataFileIndexWriter as a member declared before the index writers, so it outlives every buffer allocated through it; drop the batch schema cache in ParquetFormatWriter::ResolveBatchSchema(); read the two Parquet option names in core as local constants rather than through the format layer's headers. The changing-dictionary benchmark now rotates one alphabet and shifts the indices back by the same amount, so it writes the same logical column as its pair and the delta is the fallback rather than a difference in the data.
SteNicholas
added a commit
to SteNicholas/paimon-cpp
that referenced
this pull request
Sep 1, 2026
Addresses the review on apache#257. `FlattenUnresolvableDictionaries()` decided what to decode from whether a dictionary survives the Arrow C data interface, but what decides it is whether the writer receiving the batch recovers an encoding from the layout at all, and only ParquetFormatWriter does. A reader can hand over a dictionary for reasons the Parquet passthrough option does not govern - a format's own lazy-decoding setting - and those stay on when the rewrite vetoes the passthrough. A `dictionary(int32, utf8)` column could therefore reach a writer that imports it against the logical type, where a dictionary child fails on the buffer count. The built-in ORC reader widens to `dictionary(int64, large_utf8)`, which was already decoded for its shape, so reaching this needs a reader outside this repository. CompactRewrite now derives `preserve_layout_recoverable_dictionaries` from the veto: under a veto every dictionary is decoded before the export, whatever its shape. This also stops a dictionary reaching a Parquet writer whose table sets `parquet.enable-dictionary` to false. Rename ArrowUtils::ResolveParquetDictionaryStructType() to ResolveDictionaryStructTypeFromLayout(), and the file-local IsResolvableDictionary() to IsDictionaryLayoutRecoverable(). Neither is Parquet-specific: they read `ArrowArray::dictionary` and the caller's declared type, and DataFileWriterBase::AddFileIndexBatch runs the former for every format. Moving it into the Parquet layer instead would make core depend on Parquet symbols.
SteNicholas
force-pushed
the
PAIMON-230
branch
from
September 1, 2026 07:28
0237475 to
ff7c1c2
Compare
SteNicholas
added a commit
to SteNicholas/paimon-cpp
that referenced
this pull request
Sep 1, 2026
…STRING Addresses the review on apache#257. The compaction rewrite no longer turns `parquet.read.enable-dictionary-passthrough` on. It is a table option, off by default, and the rewrite only vetoes it - when the output is not Parquet, when `parquet.enable-dictionary` is false, or when a shredding writer is active - logging the reason at DEBUG for a table that did ask for it. Forwarding trades compaction CPU for output size, and a Parquet column chunk carries one dictionary, so a multi-file rewrite can keep the first input file's and write the rest of the row group plain; whether that trade is worth taking depends on the data, so it is left to the table. Restrict the reader to STRING. Parquet stores BINARY in the same BYTE_ARRAY leaf and dictionary-encodes it the same way, but no value accessor here can read a `dictionary(int32, binary)`: ColumnarUtils::GetView() asserts on it and returns an empty view in a release build, and LiteralConverter rejects it. Since the option applies to every read of the table, the gate gets that restriction, not the layout predicate - ArrowUtils::IsDictionaryLayoutRecoverableValueType() still accepts `utf8|binary`, which is what the writer can recover, so the writer keeps the capability for a future producer. Also from the review: use CastingUtils::Cast() instead of arrow::compute::Cast(); hold the Arrow pool adaptor in DataFileIndexWriter as a member declared before the index writers, so it outlives every buffer allocated through it; drop the batch schema cache in ParquetFormatWriter::ResolveBatchSchema(); read the two Parquet option names in core as local constants rather than through the format layer's headers. The changing-dictionary benchmark now rotates one alphabet and shifts the indices back by the same amount, so it writes the same logical column as its pair and the delta is the fallback rather than a difference in the data.
SteNicholas
added a commit
to SteNicholas/paimon-cpp
that referenced
this pull request
Sep 1, 2026
Addresses the review on apache#257. `FlattenUnresolvableDictionaries()` decided what to decode from whether a dictionary survives the Arrow C data interface, but what decides it is whether the writer receiving the batch recovers an encoding from the layout at all, and only ParquetFormatWriter does. A reader can hand over a dictionary for reasons the Parquet passthrough option does not govern - a format's own lazy-decoding setting - and those stay on when the rewrite vetoes the passthrough. A `dictionary(int32, utf8)` column could therefore reach a writer that imports it against the logical type, where a dictionary child fails on the buffer count. The built-in ORC reader widens to `dictionary(int64, large_utf8)`, which was already decoded for its shape, so reaching this needs a reader outside this repository. CompactRewrite now derives `preserve_layout_recoverable_dictionaries` from the veto: under a veto every dictionary is decoded before the export, whatever its shape. This also stops a dictionary reaching a Parquet writer whose table sets `parquet.enable-dictionary` to false. Rename ArrowUtils::ResolveParquetDictionaryStructType() to ResolveDictionaryStructTypeFromLayout(), and the file-local IsResolvableDictionary() to IsDictionaryLayoutRecoverable(). Neither is Parquet-specific: they read `ArrowArray::dictionary` and the caller's declared type, and DataFileWriterBase::AddFileIndexBatch runs the former for every format. Moving it into the Parquet layer instead would make core depend on Parquet symbols.
SteNicholas
force-pushed
the
PAIMON-230
branch
from
September 1, 2026 08:03
ff7c1c2 to
7768f35
Compare
…rewrite An append-only compaction rewrite copies rows into the new file without inspecting any value, so expanding a dictionary-encoded Parquet column on read and hashing it again on write is work neither side needs. Forward the encoding instead. Reader: `parquet.read.enable-dictionary-passthrough`, off by default, makes ParquetFileBatchReader request `set_read_dictionary` for non-nested STRING/BINARY columns whose every data page, in every row group of the file, is dictionary-encoded. A dictionary page alone cannot be the signal - a column that outgrew its page limit still carries the page it had already emitted - so the gate reads the encoding statistics. Writer: ParquetFormatWriter recovers each batch's encoding from its layout, because exporting through the Arrow C data interface drops the type. A layout pins down neither the index nor the offset width, so only `dictionary(int32, utf8|binary)` is recoverable; CompactRewrite decodes anything else per column while the type is still known - the ORC reader's `dictionary(int64, large_utf8)` under lazy decoding, dictionaries below the top level - and leaves the rest encoded. A dictionary holding nulls in its values is flattened at the writer, the one shape parquet::arrow rejects outright. Compaction opts in only when the output is Parquet, `parquet.enable-dictionary` is on and no shredding plan is active; anything else forces the read option off. A file index on a forwarded column materializes that column alone. Note that a Parquet column chunk carries one dictionary, so when the input files supply different ones the output keeps the first and falls back to plain for the rest of the row group. The rewritten data is unchanged, but the output file may be larger than one written from materialized values.
…STRING Addresses the review on apache#257. The compaction rewrite no longer turns `parquet.read.enable-dictionary-passthrough` on. It is a table option, off by default, and the rewrite only vetoes it - when the output is not Parquet, when `parquet.enable-dictionary` is false, or when a shredding writer is active - logging the reason at DEBUG for a table that did ask for it. Forwarding trades compaction CPU for output size, and a Parquet column chunk carries one dictionary, so a multi-file rewrite can keep the first input file's and write the rest of the row group plain; whether that trade is worth taking depends on the data, so it is left to the table. Restrict the reader to STRING. Parquet stores BINARY in the same BYTE_ARRAY leaf and dictionary-encodes it the same way, but no value accessor here can read a `dictionary(int32, binary)`: ColumnarUtils::GetView() asserts on it and returns an empty view in a release build, and LiteralConverter rejects it. Since the option applies to every read of the table, the gate gets that restriction, not the layout predicate - ArrowUtils::IsDictionaryLayoutRecoverableValueType() still accepts `utf8|binary`, which is what the writer can recover, so the writer keeps the capability for a future producer. Also from the review: use CastingUtils::Cast() instead of arrow::compute::Cast(); hold the Arrow pool adaptor in DataFileIndexWriter as a member declared before the index writers, so it outlives every buffer allocated through it; drop the batch schema cache in ParquetFormatWriter::ResolveBatchSchema(); read the two Parquet option names in core as local constants rather than through the format layer's headers. The changing-dictionary benchmark now rotates one alphabet and shifts the indices back by the same amount, so it writes the same logical column as its pair and the delta is the fallback rather than a difference in the data.
Addresses the review on apache#257. `FlattenUnresolvableDictionaries()` decided what to decode from whether a dictionary survives the Arrow C data interface, but what decides it is whether the writer receiving the batch recovers an encoding from the layout at all, and only ParquetFormatWriter does. A reader can hand over a dictionary for reasons the Parquet passthrough option does not govern - a format's own lazy-decoding setting - and those stay on when the rewrite vetoes the passthrough. A `dictionary(int32, utf8)` column could therefore reach a writer that imports it against the logical type, where a dictionary child fails on the buffer count. The built-in ORC reader widens to `dictionary(int64, large_utf8)`, which was already decoded for its shape, so reaching this needs a reader outside this repository. CompactRewrite now derives `preserve_layout_recoverable_dictionaries` from the veto: under a veto every dictionary is decoded before the export, whatever its shape. This also stops a dictionary reaching a Parquet writer whose table sets `parquet.enable-dictionary` to false. Rename ArrowUtils::ResolveParquetDictionaryStructType() to ResolveDictionaryStructTypeFromLayout(), and the file-local IsResolvableDictionary() to IsDictionaryLayoutRecoverable(). Neither is Parquet-specific: they read `ArrowArray::dictionary` and the caller's declared type, and DataFileWriterBase::AddFileIndexBatch runs the former for every format. Moving it into the Parquet layer instead would make core depend on Parquet symbols.
SteNicholas
force-pushed
the
PAIMON-230
branch
from
September 1, 2026 08:07
7768f35 to
56d4f25
Compare
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
Linked issue: close #230
An append-only compaction rewrite copies rows into the new file without inspecting any value, so expanding a dictionary-encoded Parquet column on read and hashing it again on write is work neither side needs. This forwards the encoding instead.
Reader.
parquet.read.enable-dictionary-passthroughmakesParquetFileBatchReaderrequestset_read_dictionaryfor non-nestedSTRINGcolumns whose every data page, in every row group of the file, is dictionary-encoded. A dictionary page alone cannot be the signal — a column that outgrew its page limit still carries the page it had already emitted — so the gate reads the encoding statistics. The file schema keeps reporting the logical type; only the emitted batches carry the dictionary.BINARYis deliberately not forwarded, although Parquet stores it in the sameBYTE_ARRAYleaf and dictionary-encodes it the same way: no value accessor here can read adictionary(int32, binary).ColumnarUtils::GetView()asserts on a dictionary whose values are neitherSTRINGnorLARGE_STRINGand returns an emptystring_viewin a release build, andLiteralConverterrejects it outright. Every consumer understands aSTRINGdictionary because the ORC reader has always produced one under lazy decoding; none was ever handed aBINARYone. Widening this needs those consumers first.Writer.
ParquetFormatWriterrecovers each batch's encoding from its layout, because exporting through the Arrow C data interface drops the type. A layout pins down neither the index nor the offset width, so onlydictionary(int32, utf8|binary)is recoverable —ArrowUtils::IsDictionaryLayoutRecoverableValueType(). That is the writer's capability and it matches Velox'sVARCHAR/VARBINARY; theSTRING-only restriction above belongs to the reader, not to the format, and is applied at the gate so the two do not get conflated.CompactRewritedecodes anything else per column, while the type is still known: the ORC reader'sdictionary(int64, large_utf8)under lazy decoding, dictionaries below the top level, non-binary value types. The rest stay encoded, so one awkward column does not cost the others their encoding — unless a veto is in force, in which case every dictionary is decoded whatever its shape (see Gating). A dictionary holding nulls in its values is flattened at the writer — the one shapeparquet::arrowrejects outright.Gating. The option is off by default and the rewrite never turns it on; enabling it is the table's decision, taken after measuring.
GetDictionaryPassthroughVetoReason()only vetoes it — when the output is not Parquet, whenparquet.enable-dictionaryis false, or when a shredding plan is active — and logs the reason at DEBUG for a table that did ask for the passthrough. Setting the option therefore never makes a rewrite fail; at worst it has no effect.A veto is enforced on the batches as well as on the option, because turning the option off is not sufficient. A reader hands over a dictionary for reasons this option does not govern — a format's own lazy-decoding setting — and those stay on.
ParquetFormatWriteris the only writer that recovers an encoding from a layout, so any other would import such a batch against its logical type and fail on the buffer count.CompactRewritetherefore derivespreserve_layout_recoverable_dictionariesfrom the veto and decodes everything under one. The built-in ORC reader widens todictionary(int64, large_utf8), which is decoded for its shape anyway, so reaching this needs a reader outside this repository;ArrowUtilsTest.TestFlattenUnresolvableDictionariescovers it directly.A file index configured on a forwarded column materializes that column alone.
Known trade-off. A Parquet column chunk carries one dictionary, so when the input files supply different ones the output keeps the first and falls back to plain for the rest of that row group. Output row groups are cut by
parquet.block.sizeand by the writer's memory limit, which are not aligned to input file boundaries. The rewritten data is unchanged, but the output file can be larger than one written from materialized values, and that column then loses eligibility for the next compaction round. This is why the option is opt-in rather than automatic, and it is documented incompaction.rst.Design follows Velox's
perf(parquet): Dictionary passthrough and selective flattening in Parquet writer(facebookincubator/velox#17986): per-column selective flatten, only VARCHAR/VARBINARY passed through, dictionaries with null values flattened, import schema reconciled per batch. It differs in two places. Velox still holds a typedVectorinside its writer, whereasFormatWriter::AddBatch(ArrowArray*)here receives an untyped array, so the flatten has to run one layer up, before the type is dropped. And Velox's reader can hand out a dictionary forVARBINARYbecause its consumers are type-erased overStringView, while the accessors here switch on the Arrow type id — hence the reader-sideSTRINGrestriction.Tests
Unit:
ArrowUtilsTest.TestResolveDictionaryStructTypeFromLayout— layout-derived resolution; rejects non-binary-like value types,large_utf8, and dictionaries below the top level; preserves a caller-declared dictionary type.ArrowUtilsTest.TestFlattenUnresolvableDictionaries— selective flatten:dictionary(int64, large_utf8)anddictionary(int64, utf8)decoded while theint32neighbour stays encoded; nested dictionary decoded; unchanged batch returned by identity; sliced batch keeps its offset. And withpreserve_layout_recoverable_dictionariesfalse, the layout-recoverabledictionary(int32, utf8)is decoded too, and a batch of only such columns is no longer returned by identity.ReaderUtilsTest.TestApplyBitmapToReadBatchKeepsDictionaryEncoding— a deletion vector filters batches by slice +arrow::Concatenate; pins that the encoding survives it.DataFileIndexWriterTest.TestDictionaryEncodedIndexedColumnRoundTrip— bitmap index built from forwarded columns across two batches with different dictionaries, which also covers the writer-held Arrow pool.ParquetFileBatchReaderTest.TestDictionaryPassthrough— on / explicitly off / option absent / file without dictionary pages.ParquetFileBatchReaderTest.TestDictionaryPassthroughSkipsBinaryColumn—f8(STRING) is forwarded andf9(BINARY) is not, from the same file with the same physical type and pages.ParquetFileBatchReaderTest.TestDictionaryPassthroughSkipsFallbackToPlain— a column that falls back to plain inside one chunk is declined.ParquetFileBatchReaderTest.TestDictionaryPassthroughRequiresEveryRowGroup— first row group fully dictionary-encoded, second falls back; the whole column is declined.ParquetFormatWriterTest.TestWriteDictionaryEncodedColumn/ChangingAcrossBatches/WithNullsInDictionary/WithNullRows/WithDuplicateValues/EmptyBatch/OfBinaryColumn/OfUnsupportedTypeIsRejected.ParquetFormatWriterTest.TestGetEstimateLengthWithDictionaryBatches—GetEstimateLength()andReachTargetSize()still drive file rolling when batches arrive encoded.Integration:
AppendCompactionInteTest.TestAppendTableCompactionDictionaryPassthrough(Parquet + ORC) — the table opts in; asserts the input read types (sasdictionary(int32, utf8)on Parquet,idandbmaterialized,sasdictionary(int64, large_utf8)on ORC), a full read of those still-encoded files through the whole read path, the rewrite, that the output column has fallen back to plain, and a predicate read through the bitmap index.AppendCompactionInteTest.TestAppendTableCompactionDictionaryPassthroughDefaultOff— a table that says nothing about the option lands on the same rows and on a better encoded output, which is why it is not on by default.AppendCompactionInteTest.TestAppendTableCompactionDictionaryPassthroughVetoedByShredding— the option is set and vetoed; the input arrives encoded and the compacted column comes back dictionary-encoded end to end, which only a rewrite from materialized values produces.Benchmarks (
benchmark/parquet_format_benchmark.cpp):BM_ParquetWrite_String,BM_ParquetWrite_DictionaryStringIntoStringSchemaandBM_ParquetWrite_ChangingDictionaryStringIntoStringSchemawrite the same logical column at the same cardinality — the third rotates the alphabet and shifts the indices back by the same amount, so every batch brings a different dictionary over identical data. The second against the first is what forwarding buys; the third against the second is what the fallback costs in time; the third against the first is the output size a rewrite that materialized and rebuilt would have produced.parquet_format_benchmark_test.cpppins that premise. These are format-writer microbenchmarks and do not stand in for compaction time, CPU and peak memory on a real table — that measurement is what a user should make before enabling the option, which is why it ships off by default.API and Format
ArrowUtils::IsDictionaryLayoutRecoverableValueType(),ArrowUtils::ResolveDictionaryStructTypeFromLayout()andArrowUtils::FlattenUnresolvableDictionaries(). Neither of the first two is Parquet-specific — they readArrowArray::dictionaryand the caller's declared type, andDataFileWriterBase::AddFileIndexBatchruns the resolution for every format. The flatten takespreserve_layout_recoverable_dictionarieswith no default, so a new caller has to state whether its destination recovers an encoding from a layout.parquet.read.enable-dictionary-passthrough, defaultfalse. It is a read option, so it applies to every read of the table and not only to the compaction rewrite; an eligible column then reaches the consumer as an ArrowDictionaryArray.AppendOnlyFileStoreWritereads the two Parquet option names as local constants instead of including the format layer's headers, so core does not depend on symbols a replacement Parquet plugin need not export.Documentation
docs/source/user_guide/compaction.rst— a "Dictionary Passthrough" section: what it does, that it is opt-in and why, the read-option caveat, per-file eligibility and the vetoes, that a veto is enforced on the batches and not only on the option, and a "Trade-off" subsection on the single dictionary per column chunk.docs/source/examples/benchmark.rst— the flat / shared-dictionary / changing-dictionary trio and what each pair measures.