Skip to content

Introduce implementation for FileType - #3608

Open
brkyvz wants to merge 19 commits into
apache:masterfrom
brkyvz:fileType
Open

brkyvz wants to merge 19 commits into
apache:masterfrom
brkyvz:fileType

Conversation

@brkyvz

@brkyvz brkyvz commented Jun 9, 2026 •

Copy link
Copy Markdown

Rationale for this change

Introduces the reference implementation for the new LogicalTypeAnnotation File as discussed in this design document

What changes are included in this PR?

Introduces a new LogicalTypeAnnotation called FILE. It enforces that a group with this type annotation have the following fields by name:

uri 		 		STRING	
offset				INT64      
size	 	 		INT64	
content_type		 STRING
checksum		 	STRING
inline 		 		BINARY

Are these changes tested?

Yes, unit tested

Are there any user-facing changes?

Introduces a new LogicalTypeAnnotation called FILE

@alkis

alkis commented Aug 5, 2026

Copy link
Copy Markdown

I had a look at this against the revised self-reference semantics in apache/parquet-format#603 and ended up writing some of it, so rather than leave it as review comments I opened it as a PR against this branch: brkyvz#1 (delta is just two commits on top of fileType).

The main piece is the writer-side API you'd need anyway: FileValueWriter.write(payload) returns either inline bytes or an offset/size pair based on a configurable threshold (ParquetProperties.withFileSelfReferenceThreshold), so object models don't choose between the two forms themselves. It has to write the payload eagerly, mid-record — offset/size are ordinary column values, so once one reaches a ColumnWriter it's encoded into a buffered page and a placeholder can't be patched up later.

Three things worth flagging regardless of whether you take the PR:

  1. The AAD uses a per-chunk counter, but the spec defines field 6 as the file offset. The counter isn't recoverable from the file — nothing stores it, so a reader has to walk the preceding values to rebuild it, which defeats exactly the property the offset-keyed AAD is there for ("available to a reader without counting preceding values... may therefore resolve a self-reference without decoding the pages it skips"). Switching to offset also drops the parameter from five signatures, since the value already carries it.

  2. decompressUnknownSize doesn't work for any codec, including SNAPPY. The drain loop ends on read() == -1, but NonBlockedDecompressorStream throws IOException("Corrupt file: Zero bytes read during decompression.") when its block is consumed rather than returning -1. It was never caught because CI dies at compile. Replaced with grow-and-retry into a dynamically sized buffer, which the spec explicitly allows; that covers LZ4_RAW too.

  3. parquet-hadoop has two test compile errors that block the module independent of anything I changed (they fail on be266d31): TestParquetMetadataConverter calls assertEquals/assertTrue with no JUnit import, and TestSelfReferenceFileWrite is missing the ParquetReadOptions import. Fixing those is what let me actually run things — currently 679/679 in parquet-column and 761/762 in parquet-hadoop.

One thing I deliberately did not change: I'd tightened schema validation to require inline whenever offset is declared, on the reasoning that uri is optional per value so a uri+offset+size schema can still emit a self-reference with no inline column chunk to inherit from. But your tests assert that schema is valid, so I reverted it — your call which way it should go.

Also worth noting for sequencing: this can't go green until a parquet-format release carries FileType (2.13.0's LogicalType union stops at 18 — that's the current CI failure), and #603 is still open, so the AAD detail could still move. To build locally I pinned parquet.format.version to an artifact holding parquet-format master's parquet.thrift.

Happy to fold any of it in differently, or split it up, if that's easier for you.

…#1)

* Add threshold-driven inline vs self-reference storage for FILE values

Writers hand FILE payloads to FileValueWriter, which decides from a
configured threshold whether to keep the bytes inline or store them out
of line as a self-reference. Both forms describe the same logical bytes,
so content_type and checksum are written identically either way and
consumers see no difference beyond which fields are set.

The payload is written eagerly, while the record is being written and
before the row group's column chunks are flushed. This is what makes the
offset knowable in time: offset and size are ordinary column values, and
once a value reaches a column writer it is encoded into a buffered page
and cannot be revised, so a placeholder could not be patched up later.
Writing at that point also leaves each column chunk contiguous on disk,
which the read path relies on when coalescing chunks into range reads.

Along the way, per parquet-format#603:

- Key the self-reference AAD on the file offset rather than a synthetic
  per-chunk counter. The spec defines AAD suffix field 6 as the offset,
  and the counter was not recoverable from the file: nothing stores it,
  so a reader had to walk preceding values to rebuild it, defeating the
  offset-based design that lets a value be resolved on its own. The
  ordinal parameter is gone from five signatures as a result.
- Decompress into a dynamically sized buffer, supporting every codec.
  The previous stream-drain loop was broken for all codecs, not just
  unframed ones: NonBlockedDecompressorStream throws rather than
  returning -1 once its block is consumed.
- Enforce the 2 GiB encrypted-module limit on write, directing oversized
  values to external references.
- Require inline whenever offset is declared. uri is optional per value,
  so a uri+offset+size schema still permits self-references, and one
  emitted without an inline column chunk has no reference point to
  inherit compression and encryption from.

Tests cover threshold routing, offset-keyed AAD round-trips in both GCM
and CTR modes, tamper detection when an offset is altered, and payloads
spanning several buffer doublings across SNAPPY, GZIP, ZSTD and LZ4_RAW.

Co-authored-by: Isaac

* Fix ZSTD resolution, revert schema tightening, fix test imports

Verified the change by building and running the suites locally, which
turned up three things:

ZSTD self-references could not be resolved at all. ZstandardCodec
returns null from createDecompressor because it decompresses only
through its stream, so driving the Decompressor directly failed with
"Could not obtain a decompressor". Such codecs are framed and report
end-of-input properly, so drain the stream for them and keep
grow-and-retry for the rest. Four tests were failing on this.

Reverted requiring `inline` whenever `offset` is declared, back to
requiring it only when `uri` is absent. Two existing tests
(testFileLogicalTypeExternalRangedReferenceWithoutInline,
testFileLogicalTypeOffsetWithSize) assert that a uri+offset+size schema
without `inline` is valid, so the stricter rule contradicted the
author's documented intent. The concern is real -- `uri` is optional per
value, so such a schema can still emit a self-reference with no
reference point -- but it belongs on the write path, and the FILE group
declaring `uri` is now documented as needing an always-inline threshold.

Fixed two pre-existing test compile errors that blocked the module:
TestParquetMetadataConverter used assertEquals/assertTrue with no JUnit
import (switched to the AssertJ style used throughout that file), and
TestSelfReferenceFileWrite was missing the ParquetReadOptions import.
Both fail on the base commit independently of these changes.

Also applied spotless formatting.

Local results: parquet-column 679/679 pass, parquet-hadoop 761/762. The
one failure, testEnumEquivalence on Encoding.ALP, is an artifact of the
local workaround for FileType being unreleased -- parquet.thrift was
substituted from parquet-format master, which defines an ALP encoding
the Java enum does not yet have. It is unrelated to these changes and
will not occur once a parquet-format release carries FileType.

Co-authored-by: Isaac

@emkornfield emkornfield left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the read side of self-reference vs inline needs work here, it seems that we aren't actually doing a full round trip but relying on in-memory structures to retrieve the bytes.

@divjotarora divjotarora left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems Types.java is the wrong place for some of the FILE validity checks as it only sees the schema, not the actual values. We probably need specialized checks in the writer and reader paths to check individual rows. I'm not sure where the best place to put this would be. Maybe check how VARIANT does this? At a high level, Codex claims:

And that you could do something similar:

  • FileValue representing one assembled FILE value.
  • FileConverters collecting child columns and validating in GroupConverter.end().
  • FileValueWriter validating before emitting fields.
  • A shared validator for offset/uri/size, non-negativity, and resolvability.

@emkornfield

Copy link
Copy Markdown
Contributor

CI failure: 'TestParquetMetadataConverter.testFileLogicalTypeIsIgnoredRatherThanFailing`

@brkyvz

brkyvz commented Oct 2, 2026

Copy link
Copy Markdown
Author

@divjotarora addressed your comments. Added reader/writer interfaces

@divjotarora

Copy link
Copy Markdown
Contributor

Hey @brkyvz, how should FileConverters behave with projected FILE groups? For example, suppose a valid stored value has uri and content_type, but the reader projects only content_type. The converter never sees uri, builds a value without a locator, fails validation, and returns null. It therefore cannot distinguish "URI was null in the file" from "URI was not projected." Is the intended contract that FILE projections must include enough fields to form a valid locator? If so, that should probably be documented/enforced, otherwise the converter needs to support partial values.

@brkyvz

brkyvz commented Oct 15, 2026

Copy link
Copy Markdown
Author

Thanks @divjotarora great point! Added checks in parquet-column/src/main/java/org/apache/parquet/io/ColumnIOFactory.java that avoid partial projections of FileType. It should be treated atomically

@gunnarmorling

Copy link
Copy Markdown

Hey, I've sent PR apache/parquet-testing#128 for adding FILE fixtures to parquet-testing, it might be interesting for testing here?

Revalidate FILE fields in GroupType.withNewFields and cover duplicate names, invalid annotations, generic projections, converter lifecycle, and persisted reader/writer behavior. Apply the repository's Spotless formatting to the FILE changes.

@Fokko Fokko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for working on this @brkyvz 👍 Left some comments, mostly around validation of the fields.

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.

6 participants