Issue #8080 : Support Hive-style partitioning in the Parquet File Output transform - #8254
Conversation
…le Output transform Data partitioning was only available in Native Spark mode. The standard Parquet File Output transform can now partition by one or more incoming fields, writing the Hive-style layout that Spark's partitionBy produces: /datalake/sales/year=2026/month=08/part-00-0000-3f2a1b9c.parquet The partition fields are not written into the files, matching partitionBy and what a reader expects when recovering the column from the path. Nulls become __HIVE_DEFAULT_PARTITION__, and values are percent-escaped so a value containing a separator stays one folder level. Four write modes decide what happens to data already in a partition folder: append (unchanged behaviour), overwrite partitions, fail if exists, and overwrite all. Overwrite-partitions clears a folder once per run rather than once per file, so a reopened partition cannot delete rows the same run just wrote. Every open Parquet writer buffers up to a full row group, so a wide partition key would otherwise exhaust memory. At most "maximum open partitions" writers are held open and the least recently written one is closed when another is needed. With no partition fields configured the transform behaves exactly as before; the partitioned path is a separate branch in processRow. Also fixes a missing '=' in messages_en_US.properties that left the row group size dialog label unresolved.
…iling backslash in partitionFolder
|
Added a few smaller changes: lombok and trailing \ for windows. |
|
Thanks @mattcasters, and thanks for pushing the changes rather than sending them back — all three The trailing backslash is a real miss on my part: On Lombok: I hand-wrote the accessors to match Pulled your commit and re-ran locally: the full module run is 48 tests, 0 failures |
|
Thanks for the feedback @vbhanuchander-lang. I had already approved the PR, was just waiting for github to give the green light. I really appreciate the high quality PRs! |
Closes #8080.
Brings Hive-style partitioned writes to the standard Parquet File Output transform, so the
layout that Native Spark mode can already produce no longer requires the Spark engine.
What it does
Configure one or more incoming fields as partition fields and the transform writes
instead of a single file set. With no partition fields configured the transform behaves exactly as
before — same single file, same name, same columns.
Design decisions worth a reviewer's attention
Partition columns are not written into the files. This matches
DataFrameWriter.partitionBy,which is what the issue asks for parity with, and it is what a reader expects when it recovers the
column from the folder name. Configuring every field as a partition field is rejected rather than
producing empty files.
Two things surfaced while testing that are worth calling out, because neither is obvious.
HopVfs percent-decodes the path it is given. Writing
region=EU%2FWestproducedregion=EU/West/on disk — two folder levels — because VFS resolves the path as a URI. The Hiveconvention still wants
%2Fin the folder name, so the partition key keeps its Hive-style form andonly the string handed to VFS has its
%doubled to%25; VFS decodes that back to a literal%.A literal
%in a value is escaped as%25for the same round-trip reason.A per-run token in the file name. Without it, two runs in Append mode both wrote
part-00-0000.parquetand the second silently replaced the first — which is the opposite ofappending. Each run now contributes eight hex characters to the name.
Nulls and awkward values follow the Hive convention. A null or empty partition value becomes
__HIVE_DEFAULT_PARTITION__, the same sentinel Hive and Spark use, so the output stays readable bythem. Values are percent-escaped for the characters that would otherwise break the layout — most
importantly
/, soregion=EU/Westproduces one folder namedregion=EU%2FWestrather than twolevels.
Write modes (
Existing datain the dialog) decide what happens to data already in a partitionfolder, and only apply while partitioning:
The subtle one is Overwrite partitions: it must clear a folder once per run, not once per
file. A partition that gets closed and reopened (see below) would otherwise delete the rows the same
run had just written to it. There is a test for exactly that.
Bounded memory:
Maximum open partitions(default 10). Every open Parquet writer buffers up toa full row group — the row group size defaults to 256 MB — so one writer per distinct partition
value would run a pipeline out of memory on a wide key. Spark avoids this by sorting; a streaming
transform cannot, so instead the transform keeps at most N writers open and closes the
least-recently-written one when it needs another. Reopening a partition starts a new
part-...file,which is why a partition can end up with several files. The row-count split (
Split into parts)applies per partition file as well.
Structure
The partitioned path is additive:
processRowbranches on whether partition fields are configured,and the existing single-file code is untouched on the other side of that branch. Building the Avro
and Parquet schema was extracted out of
openNewFile()intobuildSchema()so both paths share itwithout the partitioned path rebuilding it per file.
Tests
ParquetOutputPartitionTest, 16 tests (the module is 47 in total, all passing), covering the path building and escaping, the fieldexclusion and its three rejection cases, and end-to-end writes that assert the folder layout on disk
and read the Parquet files back:
matching rows are present
/stays one folder leveldeleting what the same run just wrote (forced by setting the open-partition limit to 1)
./mvnw -pl plugins/tech/parquet -am testis green, andspotless:checkpasses.One unrelated one-character fix
messages_en_US.propertieshadParquetOutputDialog.RowGroupSize.Label Row group sizewith no=,so that dialog label never resolved. Fixed while adding keys to the same file — happy to split it
out if you would rather it went separately.