Skip to content

GH-600: Allow TimestampType to annotate FLBA(12) - #601

Open
divjotarora wants to merge 1 commit into
apache:masterfrom
divjotarora:flba12-ts
Open

GH-600: Allow TimestampType to annotate FLBA(12)#601
divjotarora wants to merge 1 commit into
apache:masterfrom
divjotarora:flba12-ts

Conversation

@divjotarora

Copy link
Copy Markdown
Contributor

Rationale for this change

This PR implements the changes described in the proposal document to add support for extended precision nanosecond timestamps that cover the full ANSI SQL timestamp range (years 0000-9999).

What changes are included in this PR

Spec changes to allow the TimestampType logical type to annotate the FIXED_LEN_BYTE_ARRAY physical type with type_length = 12.

Do these changes have PoC implementations?

The parquet-java change is in progress.

Closes #600

Comment thread LogicalTypes.md
since the Unix epoch.

For the `FIXED_LEN_BYTE_ARRAY` carrier (with `type_length = 12`), the value is a
signed 96-bit two's-complement little-endian integer count of `unit`s since the

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 suggest we do big-endian here so that we can use the same signed two complements byte compare we use for DECIMAL in FLBA.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I chose little-endian because it seems to match the rest of the spec (DECIMAL is the only deviation). But I don't have a strong preference, happy to change it to big-endian. I'd like to hear from others to see if anyone else agrees/disagrees.

@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.

LGTM, we can bikeshed more on little endian vs big-endian to finalize this.

CurtHagenlocher added a commit to clast-project/engineered-wood that referenced this pull request Aug 22, 2026
…ncated bounds, an ungated annotation (#215)

* fix(parquet): gate the TIMESTAMP annotation on its physical type

Two pre-existing defects, both latent behind the same assumption: that TIMESTAMP
only ever arrives on INT64. apache/parquet-format#601 is about to end that, so
they stop being theoretical.

THE READ DEFECT. `ArrowSchemaConverter.FromLogicalType` mapped `TimestampType` to
an Arrow `TimestampType` without looking at `column.PhysicalType` -- unlike
`MakeDecimalType`, which has always switched on it. The read path maps
`Int64Type or TimestampType or Time64Type` onto a `long` value buffer, so a
TIMESTAMP annotation on a 12-byte column was reinterpreted eight bytes at a time
and produced plausible-looking wrong dates. Not an error, not a refusal --
silently wrong data, which is the worst of the three. The same hole existed on
the converted-type path, where TIMESTAMP_MILLIS / TIMESTAMP_MICROS are likewise
INT64-only.

Both now fall through to the physical type, which is lossless. Twelve honest
bytes beat a wrong date.

THE WRITE DEFECT. `SignedOrderMatchesLogical` decides whether the deprecated
`Statistics.min`/`max` may be emitted, and answered `true` for every
`TimestampType`. Its real precondition is narrower than "this Arrow type is
signed": it is that `StatisticsCollector` compared the values with a TYPED
comparator, which it does only for BOOLEAN/INT32/INT64/FLOAT/DOUBLE. Every
FIXED_LEN_BYTE_ARRAY column goes through `SequenceCompareTo` -- unsigned
lexicographic. A wrong bound in the footer is a wrong prune, not a cosmetic
defect, so the physical type is now part of the answer.

This one is latent until an Arrow `TimestampType` can map to FLBA, which is
exactly what the FLBA(12) writer will do. There is therefore no end-to-end write
that reaches it yet, and a unit test is the only thing standing between the fix
and a silent regression -- hence `SignedOrderMatchesLogical` becoming internal.

NOT FIXED HERE, deliberately: the same class of mismatch exists for other
annotations (STRING on FLBA, DATE on INT64, the fixed-width INT variants). Those
need a physical-type compatibility table and a decision about how lenient to be
with files that currently "work", which is a bigger change with real regression
risk. parquet-testing#122 adds a fixture for that class; it deserves its own PR.

Verified: reverting the four guards fails 8 of the 16 new tests. Full Parquet
suite 1020/1020 on net10.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(parquet): read back the FLBA columns we write as DELTA_BYTE_ARRAY

This library wrote files it could not itself read. Any FIXED_LEN_BYTE_ARRAY
column written with ByteArrayEncoding.DeltaByteArray and V2 pages came back as a
NullReferenceException -- DECIMAL above precision 18, UUID, FLOAT16 and plain
fixed binary alike.

DELTA_BYTE_ARRAY is legal for both BYTE_ARRAY and FIXED_LEN_BYTE_ARRAY, and
EncodingStrategyResolver emits it for both. But DeltaByteArrayDecoder finished by
calling ColumnBuildState.AddByteArrayValues, which writes through the
data/offsets buffer pair -- and the state allocates that pair only for BYTE_ARRAY
columns. A fixed-width column arrives with both buffers null and dies
dereferencing them. No test covered the combination in either direction, so
nothing caught it.

The reconstruction was already producing exactly the right bytes: when every
value is the same width, the output is the packed layout the fixed-width buffer
wants and the offsets are redundant. So the fix is to copy it straight into
ReserveFixedBytes and skip the byte-array bookkeeping entirely.

The width now has to reach the decoder, because a fixed-width column's value size
is not recoverable from the encoded page -- prefix and suffix lengths are
per-value and a malformed file may disagree with the schema. That is also why the
width is checked per value rather than trusted: the bulk copy would otherwise
shift every later value silently, which is a worse failure than the crash it
replaces.

Found while checking whether DELTA_BYTE_ARRAY was usable for the FLBA(12)
extended-precision timestamp carrier. It is now, but this is a pre-existing bug
on its own and predates that work.

Verified: reverting the fixed-width branch fails 7 of the 9 new tests. Parquet
suite 1029/1029 on net10.0 and 1023/1023 on net472.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(parquet): stop truncating sub-millisecond timestamp statistics bounds

A row-group max bound of 1500 microseconds decoded as 0 milliseconds. A predicate
of `t > 0.5ms` then compares against that bound, concludes the row group cannot
match, and prunes rows that genuinely do. Silent data loss, not a rounding
blemish.

ParquetStatisticsAccessor converted every timestamp bound through
DateTimeOffset.FromUnixTimeMilliseconds, so MICROS and NANOS columns lost
everything below a millisecond -- and lost it by truncating TOWARD ZERO, which
moves a positive max down and a negative min up. Both directions narrow the range
the file claims, which is the unsafe direction.

Bounds now go through TICKS. A DateTimeOffset holds 100 ns, so MILLIS and MICROS
are exact and NANOS is the only unit that has to round at all. Where rounding is
unavoidable the bound moves OUTWARD -- max up, min down -- so the advertised
range can only ever be wider than the data, never narrower. TIME(NANOS) had the
same truncation and is fixed with it.

A bound outside DateTimeOffset's range is now dropped rather than clamped. A
clamped bound is indistinguishable from a real endpoint and would prune on a
value the file never contained; no bound at all just means no pruning. The two
ends are independent, so a representable min still survives a max that is not.

The invariant is stated directly as a test: whatever rounding happens, every
value in the column still falls inside the range the footer advertises.

Found while adding statistics support for the FLBA(12) extended-precision
timestamp carrier -- the same decode path, and the same mistake was about to be
repeated there. This is a pre-existing bug and is fixed on its own.

Verified: restoring the millisecond conversion fails 7 of the 9 new tests.
Parquet suite 1038/1038 on net10.0 and 1032/1032 on net472.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(parquet): reject malformed DELTA_BYTE_ARRAY instead of reconstructing it

From Copilot's review of #215, and correct.

DELTA_BYTE_ARRAY builds each value from the first prefix_length bytes of the
PREVIOUS value plus a suffix. Nothing checked that the prefix actually fit inside
the previous value.

It does not read out of bounds -- the output buffer is sized from the same
lengths -- so it read forward into the zero-filled region reserved for the value
being reconstructed and produced a value that is neither what was encoded nor an
error. A nonzero prefix on the FIRST value is the same bug at index 0, where
there is no previous value at all. Both cases decoded silently.

Verifying the report turned up three more in the same family: a negative prefix
also decoded silently, while a negative suffix and a suffix running past the end
of the page threw ArgumentException and ArgumentOutOfRangeException -- a
malformed file reported as an internal argument error rather than as a malformed
file. One validation pass covers all five.

The total is also accumulated as long now. Prefixes let the described output grow
faster than the page does, so a malformed page can claim more bytes than an int
can hold.

The boundary case is explicitly tested: a prefix exactly the length of the
previous value is LEGAL -- it is what an encoder emits for a repeated value --
and must not be caught by the check.

The payloads are hand-built from two DELTA_BINARY_PACKED blocks, because no
encoder here can produce them.

Parquet suite 966/966 on net10.0 and 960/960 on net472. (The cloud-emulator tests
in this assembly are excluded from those counts: fake-gcs-server is returning
stale content hashes locally after many repeated runs this session. They fail
14-15 at random with and without this change, and CI is green on the branch.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Support extended precision for nanosecond timestamps

4 participants