Repository navigation
Conversation
Implements a true page-level Arrow-to-Parquet writer that bypasses the RecordConsumer/ColumnWriter pipeline entirely, writing pages directly to PageWriter. Architecture: - ArrowParquetWriter: manages ParquetFileWriter, row groups, and per-column strategy selection - ArrowColumnWriter: strategy interface for per-column writing - ArrowColumnWriterFactory: selects optimal strategy based on column type, nullability, and encoding - ZeroCopyPlainWriter: wraps Arrow data buffer directly as page BytesInput (zero copy for non-null fixed-width PLAIN columns) - NullablePlainWriter: scans validity bitmap, bulk-copies non-null runs, encodes definition levels as RLE runs - LevelEncoder: produces RLE-encoded repetition/definition levels - StatsComputer: computes page statistics from Arrow buffers in a single sequential scan For non-null INT32 columns, this writer performs ZERO per-value method calls. The Arrow buffer bytes ARE the Parquet page bytes. Phase 1 scope: flat schemas, fixed-width types (INT32, INT64, FLOAT, DOUBLE, FIXED_LEN_BYTE_ARRAY), PLAIN encoding. Variable-width types and dictionary encoding are future phases. Closes apache#3733
|
This pull request has been automatically marked as stale because it has had no activity for at least 2 months. If you are still working on this change or plan to move it forward, please leave a comment or push a new commit so we know to keep it open. Otherwise, this PR will be closed automatically in about one month. Thank you for your contribution to Apache Parquet! |
|
bump |
rowsPerPage divided the 1 MB page target by the sum of all column widths, and getTypeLength() is 0 for every primitive type except FIXED_LEN_BYTE_ARRAY, so each column was estimated at 32 bytes. On a 19-column schema that gave 1724 rows a page (~7 KB INT32 pages), and ZSTD, which compresses each page on its own, compressed them poorly. Size pages by the widest column, using each primitive type's PLAIN width. On a 19-column TPC-H lineitem-like table (512 MB of Arrow, ZSTD, 16384-row batches) this cuts the write from 2.57 s to 1.73 s and the file from 107.5 MB to 87.5 MB, the same size ExampleParquetWriter produces with dictionary encoding off.
|
I ran this PR as a writer for Apache Fluss lake tiering, which is one of the downstream users named in the description. I used head 1856d5b, built against parquet 1.18.2 on JDK 21, and the 10 tests pass. The data was a TPC-H lineitem-like table: 19 nullable columns (INT32, INT64, DOUBLE, DATE, STRING, TIMESTAMP) in 16384-row batches, written with ZSTD. For comparison I wrote the same rows with Page sizing. With dictionary off on both sides, this PR's file is 26.8 MB against 21.7 MB from int typeLen = col.getPrimitiveType().getTypeLength();
estimatedRowBytes += typeLen > 0 ? typeLen : VAR_WIDTH_ESTIMATE_BYTES;
...
int rowsPerPage = Math.max(1, TARGET_PAGE_SIZE_BYTES / Math.max(estimatedRowBytes, 1));
I tried sizing by the widest column instead, using primitive widths (BOOLEAN 1, INT32/FLOAT 4, INT64/DOUBLE 8, INT96 12, FLBA its length, binary 32), and taking the max instead of the sum:
With the change, the output matches Dictionary. After that change, the remaining gap to a dictionary-encoded file (16.9 MB from For reference, on the same machine (i9-13900K), the row-object path through Iceberg's JDK note. On JDK 17+, the writer needs Formatting. Running |
Size ArrowParquetWriter pages by the widest column
|
Thanks for taking a look and fixing the bug @mikepapadim, merged |
Summary
Adds
ArrowParquetWriterto theparquet-arrowmodule — a page-level writer that accepts ArrowVectorSchemaRootbatches and produces valid Parquet files without per-row object construction.Closes #3733. Related: #2264, #3353.
Motivation
Multiple downstream projects (Iceberg #17748, Fluss #4047, Paimon) work with Arrow-columnar data internally but must materialize row objects to write Parquet via
ParquetWriter<T>.write(T). This PR provides a direct Arrow-to-Parquet path, eliminating the row-object API mismatch. Arrow C++/Python have had this sincewrite_table().Design
Bypasses
RecordConsumer/ColumnWriterentirely. Writes assembled pages directly toPageWriterwith per-column strategy selection:BytesInput. O(1) level encoding. Data bytes are not transformed — Arrow's little-endian layout IS Parquet PLAIN encoding.Does not extend
ParquetWriter<T>becausewrite(T)increments an internal record count by 1 per call, incompatible with batch semantics.Key properties
ConcatenatingByteBufferCollector.collect()copies page bytes duringwritePage()— Arrow buffers can be freed afterwriteBatch()returnsStatsResultvalue object, no shared mutable statePageWriter.getMemSize()sum, not heuristicTypes supported
INT32, INT64, FLOAT, DOUBLE, BOOLEAN, BINARY (string), FIXED_LEN_BYTE_ARRAY — nullable and required.
Not included (follow-up PRs)
UnsupportedOperationException.UnsupportedOperationException.Relationship to #3530 (Performance Improvements series)
This PR is complementary to the encoding-level optimizations in #3530. Specifically:
PlainValuesWriter.writeInteger()use directByteBuffer.putInt(). Our zero-copy path bypassesPlainValuesWriterentirely for required columns, but nullable/varwidth paths would benefit from GH-3530: Optimize PLAIN encoding and decoding with direct ByteBuffer I/O #3565's faster fallback when dict encoding is eventually added.LevelEncoder.encodeFromValidityBitmap()usesRunLengthBitPackingHybridEncoderfor nullable columns. GH-3530: Optimize RLE hybrid encoder/decoder scalar hot-path performance #3568's optimizations would make this faster.writeIntegers(int[], offset, count)batch methods toValuesWriter. When this lands, a futureDictionaryWriterstrategy could use batch index encoding rather than per-value.Statistics: irreducible O(N) scan
The stats scan (min/max/NaN count) is the minimum work required to produce a valid Parquet file with predicate pushdown support. For the zero-copy path, it is the ONLY per-value work performed. Future optimizations possible:
Tests
10 round-trip tests (write via
ArrowParquetWriter, read via standardParquetReader):Dependencies added
parquet-hadoop(compile) —ParquetFileWriter,ColumnChunkPageWriteStorearrow-memory-netty(test) — Arrow allocator runtimehadoop-common+hadoop-mapreduce-client-core(test) — forParquetReaderin round-trip testsparquet-hadooptest-jar (test) —GroupReadSupportDiscussion point
Adding
parquet-hadoopas a compile dependency toparquet-arrowincreases the module's dependency footprint.ParquetFileWriterandColumnChunkPageWriteStoreexist only inparquet-hadoop— no alternative implementations. Every Java Parquet writer (Iceberg, Spark, Flink) depends onparquet-hadoop. Alternative: create a newparquet-arrow-hadoopmodule. Open to guidance.How to run tests