Skip to content

GH-2142, GH-3751: Backport proto-bytes fixes to 1.18.x (#3750, #3752) - #3844

Open
puskarpeter wants to merge 2 commits into
apache:parquet-1.18.xfrom
puskarpeter:GH-2142-GH-3751-backport-1.18.x
Open

puskarpeter wants to merge 2 commits into
apache:parquet-1.18.xfrom
puskarpeter:GH-2142-GH-3751-backport-1.18.x

Conversation

@puskarpeter

@puskarpeter puskarpeter commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Rationale for this change

Backport of #3750 and #3752 to 1.18.x. Both are clean cherry-picks of the master commits (c3def13 and 398b63e), no conflicts. They are bundled because #3752 builds on #3750 and cannot be applied without it.

Both fix bugs that affect 1.18.x users today:

Could you please cherry-pick the two commits onto parquet-1.18.x as they are instead of squash-merging, so each fix keeps its own commit like on master?

What changes are included in this PR?

Are these changes tested?

Yes, the tests from both PRs are included. The full parquet-protobuf suite passes on this branch, built with --release 11.

Are there any user-facing changes?

Same as #3750 and #3752.

…ache#3750)

### Rationale for this change

Protobuf allows empty message definitions, but Parquet forbids empty groups. Converting a message
that merely *contains* a field of an empty message type produces a schema with an empty group,
which writer construction rejects with `InvalidSchemaException: Cannot write a schema with an
empty group`. Such fields appear in real-world schemas (deprecated stubs, marker/placeholder
messages), and a single one makes the whole message type unwritable.

### What changes are included in this PR?

`ProtoSchemaConverter.addMessageField` terminates a field whose message type has no fields as a
`BINARY` column holding the serialized message — the same mechanism PARQUET-1711 uses for
recursion beyond `maxRecursion`. Since an empty message serializes to zero bytes, the column is
cheap, and field **presence** still round-trips (null = unset vs empty bytes = set):

- singular field → `optional binary stub` (or `required`, per the field);
- repeated field, parquet-specs mode → LIST-wrapped binary via the existing
  `addRepeatedPrimitive`, so element cardinality survives;
- repeated field, old style → `repeated binary`;
- map value type → `optional binary value` inside the `key_value` group (keys stay typed).

`ProtoWriteSupport.createMessageWriter`'s existing truncated-field check (primitive BINARY where a
message field was declared → `BinaryWriter`) is generalized to look through the LIST/MAP wrapper
(`getGroupType` → `getContentType`), so the writer tree lines up with these schemas.

A message that is empty at the **root** is still rejected — there is no parent field to hold the
bytes, and a Parquet file with zero columns is not representable.

**Read side** (second commit, after review): `ProtoMessageConverter` used to cast every message
field's Parquet type to a group, so a message field stored as `BINARY` failed with a
`ClassCastException` while the converter tree was built — which also means files with
PARQUET-1711 recursion truncation have been unreadable by `ProtoParquetReader` since 1.13.0 (apache#995
shipped with an explicit "TODO: ReadSupport"). A new `ProtoBinaryMessageConverter` parses the
bytes back through `parentBuilder.newBuilderForField(field)` and hands the message to the existing
parent container, so singular fields, LIST elements, old-style repeated fields and map values all
round-trip without touching `ListConverter`/`MapConverter`. Parse failures surface as
`ParquetDecodingException`.

### Are these changes tested?

Yes. New `ProtoEmptyMessageTest` (new test messages `Stub`/`StubBox` in `Trees.proto`) writes
through the real write path (`ProtoParquetWriter` → `MessageColumnIO`, both specs-compliant and
old style) and reads back with `GroupReadSupport` and with `ProtoParquetReader`:

- singular / repeated / map-value empty-message fields round-trip with correct cardinality and
  zero-byte values;
- presence round-trips (set empty message vs unset field);
- `ProtoParquetReader` reads the written messages back equal to the originals (specs-compliant and
  old style), and the same read path round-trips a `BinaryTree` truncated at `maxRecursion`;
- an empty root message still fails with `InvalidSchemaException` ("Cannot write a schema with an
  empty group").

`ProtoSchemaConverterTest.testEmptyMessageFields` pins the converted schema. The full
parquet-protobuf suite passes (117 tests).

### Are there any user-facing changes?

Message types that previously could not be written to Parquet at all now can; fields of empty
message types appear as (possibly LIST/MAP-wrapped) `binary` columns and read back into the
original messages with `ProtoParquetReader`. Files with `maxRecursion` truncation, previously
unreadable by `ProtoParquetReader`, now read back as well. No change for schemas that were
previously writable. Error behavior for an empty root message is unchanged.

Closes apache#2142
…oto fields (apache#3752)

The maxRecursion truncation (PARQUET-1711) replaced recursive fields
with a hardcoded optional binary, while ProtoWriteSupport still wraps
repeated/map fields' writers in ArrayWriter/RepeatedWriter/MapWriter.
Writing data that nests deeper than maxRecursion through a repeated
recursive field crashed with a ClassCastException in parquet-specs mode
and corrupted the file in the old style (inconsistent repetition
levels: reads fail with ParquetDecodingException or return a wrong
tree). A map field exhausting the recursion budget collapsed entirely
- keys included - into one binary, and writing data through it crashed
the same way.

Truncate to proto bytes preserving the field's shape instead, reusing
the terminate-as-bytes path introduced for empty message types
(apacheGH-2142): LIST-wrapped binary for repeated fields in specs mode,
repeated binary in the old style, and the MAP structure kept with the
recursive value truncated inside key_value (the map branch now runs
before the recursion check). Truncated optional fields are unchanged;
proto2 required fields now keep their required repetition. Each
truncated cell round-trips as the serialized subtree, and with the
binary-to-message read path from apacheGH-2142 the truncated messages read
back losslessly through ProtoParquetReader.

Signed-off-by: Puškár, Peter <peter.puskar@firma.seznam.cz>
@puskarpeter

Copy link
Copy Markdown
Contributor Author

@wgtmac Here is the promised backport of the truncation fixes to 1.18.x :)

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.

1 participant