Uh oh!
There was an error while loading. Please reload this page.
PARQUET-2249: Add IEEE-754 total order and nan count for floating types - #3393
Conversation
c01b3f3 to
4b7e86bCompare133fb4b to
07a4d77CompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
etseidl
commented
Apr 2, 2026
@wgtmac thank you for adding the interop test! 🙏 In arrow-rs we've made the decision to only write the new column order for floats, so I can't reproduce the total order columns. Some things I think need to be added are negative NaNs, as well as examples where the min and/or max are 0. The latter is to make sure that the old rules regarding 0 min being set to -0 and 0 max set +0 are no longer followed with the new ordering. |
Did you mean arrow-rs will no longer write floats with the legacy TypeDefinedOrder? From the perspective of interoperability test, I think this is fine if it does not fail when reading files produced by other writers.
That's a good suggestion! I have updated the floating-point interop coverage to add explicit ZERO_MIN and ZERO_MAX cases, so we now verify that IEEE-754 total order no longer rewrites +0 min to -0 or -0 max to +0. I also expanded the NaN coverage to include both negative and positive NaN patterns. While debugging the test, I found that the Java implementation uses |
etseidl
commented
Apr 3, 2026
Thank you. I hope to have the rust tests done today. |
wgtmac
commented
Jun 15, 2026
@shangxinli@etseidl Do you want to take a look again? I think now everything is ready on my end. cc @gszadovszky@Fokko |
gszadovszky
left a comment
There was a problem hiding this comment.
Added a couple of comments.
Also, it would be nice to add coverage for predicate filtering with signed NaN values. Since NaN value are excluded from the statistics, this is only for the value level (ValueInspector). Can be either covered unit or integration level.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
wgtmac
commented
Jun 17, 2026
Thanks @gszadovszky for the review! I think I've addressed all your comments. Let me know what you think. |
gszadovszky
commented
Jun 17, 2026
Thank you, @wgtmac. One more thing. This PR also changes how If we move to that direction, we also need to fix the dictionary filter. We use a boxed |
@gszadovszky That's a good question! I think the main behavior change is that now original NaN bits are preserved in not only dictionary but also encoded values. I would regard this as a benign bug fix. For dictionary filter and bloom filter on the read path, they are not aware of column order. Introducing IEEE754 total order is anyway a breaking change to them because raw bits of NaNs must be preserved as is. Update: I've sent https://lists.apache.org/thread/m6j8lzc09ytyd45wt6pdcyn5qy95f0vt to discuss this. |
wgtmac
commented
Jun 24, 2026
I updated the patch so the pruning filters and record-level filters have separate semantics:
I still think that preserving raw bits of NaN values is a bug fix and benign breaking change. Let me know what you think. @gszadovszky |
gszadovszky
commented
Jun 24, 2026
Thanks @wgtmac for the updates. I think one issue remains with |
wgtmac
commented
Jun 25, 2026
That's my oversight and thanks @gszadovszky for catching this! Now it should be fixed as well. |
gszadovszky
left a comment
There was a problem hiding this comment.
Thank you, @wgtmac. LGTM.
Uh oh!
There was an error while loading. Please reload this page.
wgtmac
commented
Jun 25, 2026
Merged. Thanks @etseidl@shangxinli@gszadovszky for the review! |
Jiayi-Wang-db
commented
Jul 28, 2026
@wgtmac Sorry for a late question about this pr, why are we still using the old |
wgtmac
commented
Jul 28, 2026
@Jiayi-Wang-db That's a great question. I was supposed not to break any legacy readers. But I agree that legacy readers should just ignore unknown column order if they are implemented correctly. |
Jiayi-Wang-db
commented
Jul 28, 2026
@wgtmac My concern is that the new column order cannot even be enabled without a code change. |
wgtmac
commented
Jul 28, 2026
Why? Couldn't we explicitly create a Node with a specific column order just like the new unit test does? |
Jiayi-Wang-db
commented
Jul 28, 2026
Yes, but it needs a writer-side code change. |
Jiayi-Wang-db
commented
Jul 28, 2026
Actually, the risker breaking change is reusing |
wgtmac
commented
Jul 29, 2026
I don't think so. NaNs are not written to the min/max for
This is exactly the concern that I have. Readers may have just hard-coded |
Jiayi-Wang-db
commented
Jul 29, 2026
Correct. However, the scenario I’m describing is the following: An older version of parquet-java that predates
As a result, rows containing NaN values can be incorrectly pruned. |
Fokko
commented
Jul 29, 2026
Joining the party late here. I just checked, and for example 1.12.x would ignore the min/max statistics when it encounters an unknown sort order (according to Claude Opus):
The tricky part is that we don't know how other readers in the wild are handling this, and we want to avoid triggering NPE's (or any other language equivalent). |
wgtmac
commented
Jul 30, 2026
@Jiayi-Wang-db Sorry I don't understand your concern. Old readers are not aware of So for columns containing NaN values, either their stats have finite bounds (safe to be used by filter), or contain NaN bounds (should be ignored by filter). If they are implemented correctly then we are safe, if not then that's the problem of old readers not this new version. Please correct me if I was wrong, this change only emits new |
Digging into this further, I found that old parquet-java writers did write NaN into the bounds, despite the spec guidance. Old readers then discarded the min/max whenever either bound was NaN, which made this safe in practice. The new writer changes that behavior for Strictly speaking, the old readers have a latent bug because they are not conservative enough for spec-conforming |
Jiayi-Wang-db
commented
Jul 30, 2026
Yes, this is exactly my concern. Since old readers would simply ignore statistics with an unknown sort order (thanks, @Fokko for checking that) and remaining the old sort order could cause correctness issues for old readers, I think moving forward with writing the new sort order is the right approach. |
wgtmac
commented
Jul 30, 2026
@Jiayi-Wang-db Do you want to submit a PR on this? That said, I think old parquet-java readers still have the problem reading a spec-conforming type_defined_order with mixed NaN values written by other implementations. |
…t columns Follow-up to apache#3393, which added IEEE_754_TOTAL_ORDER support but kept TYPE_DEFINED_ORDER as the default for FLOAT, DOUBLE and FLOAT16 columns. Keeping the type-defined order as the default is a latent backward-compat hazard: the writer now computes finite min/max over the non-NaN subset and records nan_count, but an old reader that predates nan_count ignores it, accepts the finite bounds, and can incorrectly prune row groups that contain NaN. Readers instead ignore statistics written under an unknown sort order, so writing IEEE 754 total order by default is the safer behavior. See the discussion on apache#3393. This makes FLOAT, DOUBLE and FLOAT16 columns built without an explicit column order default to IEEE_754_TOTAL_ORDER (mirroring how apache#3610 defaults INT96 to INT96_TIMESTAMP_ORDER). Columns with a logical annotation that does not accept IEEE 754 total order (e.g. an unknown annotation) fall back to type-defined order so they remain constructible. To stay backward compatible on read, a footer that carries no column_orders list predates IEEE_754_TOTAL_ORDER, so floating-point columns read from such a footer are given type-defined order rather than inheriting the new construction-time default; their legacy statistics are thus not reinterpreted under IEEE 754 total order. Tests that exercise the legacy type-defined NaN / +-0 semantics are pinned to TYPE_DEFINED_ORDER explicitly, and new converter tests cover the default serialization and the column-order-less read path. Co-authored-by: Isaac
…t columns Follow-up to apache#3393, which added IEEE_754_TOTAL_ORDER support but kept TYPE_DEFINED_ORDER as the default for FLOAT, DOUBLE and FLOAT16 columns. Keeping the type-defined order as the default is a latent backward-compat hazard: the writer now computes finite min/max over the non-NaN subset and records nan_count, but an old reader that predates nan_count ignores it, accepts the finite bounds, and can incorrectly prune row groups that contain NaN. Readers instead ignore statistics written under an unknown sort order, so writing IEEE 754 total order by default is the safer behavior. See the discussion on apache#3393. This makes FLOAT, DOUBLE and FLOAT16 columns built without an explicit column order default to IEEE_754_TOTAL_ORDER (mirroring how apache#3610 defaults INT96 to INT96_TIMESTAMP_ORDER). Columns with a logical annotation that does not accept IEEE 754 total order (e.g. an unknown annotation) fall back to type-defined order so they remain constructible. To stay backward compatible on read, a footer that carries no column_orders list predates IEEE_754_TOTAL_ORDER, so floating-point columns read from such a footer are given type-defined order rather than inheriting the new construction-time default; their legacy statistics are thus not reinterpreted under IEEE 754 total order. Tests that exercise the legacy type-defined NaN / +-0 semantics are pinned to TYPE_DEFINED_ORDER explicitly, and new converter tests cover the default serialization and the column-order-less read path. Co-authored-by: Isaac
…t columns Follow-up to apache#3393, which added IEEE_754_TOTAL_ORDER support but kept TYPE_DEFINED_ORDER as the default for FLOAT, DOUBLE and FLOAT16 columns. Keeping the type-defined order as the default is a latent backward-compat hazard: the writer now computes finite min/max over the non-NaN subset and records nan_count, but an old reader that predates nan_count ignores it, accepts the finite bounds, and can incorrectly prune row groups that contain NaN. Readers instead ignore statistics written under an unknown sort order, so writing IEEE 754 total order by default is the safer behavior. See the discussion on apache#3393. This makes FLOAT, DOUBLE and FLOAT16 columns built without an explicit column order default to IEEE_754_TOTAL_ORDER (mirroring how apache#3610 defaults INT96 to INT96_TIMESTAMP_ORDER). Columns with a logical annotation that does not accept IEEE 754 total order (e.g. an unknown annotation) fall back to type-defined order so they remain constructible. The default-order selection is unified in PrimitiveType.defaultColumnOrder so construction and text serialization agree. To stay backward compatible on read, a footer that carries no column_orders list predates IEEE_754_TOTAL_ORDER, so floating-point columns read from such a footer are given type-defined order rather than inheriting the new construction-time default; their legacy statistics are thus not reinterpreted under IEEE 754 total order. The text schema representation now carries a non-default column order (columnorder(...) after the type/annotation) and MessageTypeParser parses it, so a column order set explicitly survives toString()/parse round-trips such as the one GroupWriteSupport performs. Columns left at their default emit no token, keeping existing schema strings unchanged. Tests that exercise the legacy type-defined NaN / +-0 semantics set TYPE_DEFINED_ORDER explicitly, and new tests cover the default serialization, the column-order-less read path, and the text round-trip. Co-authored-by: Isaac
…t columns Follow-up to apache#3393, which added IEEE_754_TOTAL_ORDER support but kept TYPE_DEFINED_ORDER as the default for FLOAT, DOUBLE and FLOAT16 columns. Keeping the type-defined order as the default is a latent backward-compat hazard: the writer now computes finite min/max over the non-NaN subset and records nan_count, but an old reader that predates nan_count ignores it, accepts the finite bounds, and can incorrectly prune row groups that contain NaN. Readers instead ignore statistics written under an unknown sort order, so writing IEEE 754 total order by default is the safer behavior. See the discussion on apache#3393. This makes FLOAT, DOUBLE and FLOAT16 columns built without an explicit column order default to IEEE_754_TOTAL_ORDER (mirroring how apache#3610 defaults INT96 to INT96_TIMESTAMP_ORDER). Columns with a logical annotation that does not accept IEEE 754 total order (e.g. an unknown annotation) fall back to type-defined order so they remain constructible. The default-order selection is unified in PrimitiveType.defaultColumnOrder so construction and text serialization agree. To stay backward compatible on read, a footer that carries no column_orders list predates IEEE_754_TOTAL_ORDER, so floating-point columns read from such a footer are given type-defined order rather than inheriting the new construction-time default; their legacy statistics are thus not reinterpreted under IEEE 754 total order. The text schema representation now carries a non-default column order (columnorder(...) after the type/annotation) and MessageTypeParser parses it, so a column order set explicitly survives toString()/parse round-trips such as the one GroupWriteSupport performs. Columns left at their default emit no token, keeping existing schema strings unchanged. Tests that exercise the legacy type-defined NaN / +-0 semantics set TYPE_DEFINED_ORDER explicitly, and new tests cover the default serialization, the column-order-less read path, and the text round-trip. Co-authored-by: Isaac
* PARQUET-2249: Write IEEE 754 total order by default for floating-point columns Follow-up to #3393, which added IEEE_754_TOTAL_ORDER support but kept TYPE_DEFINED_ORDER as the default for FLOAT, DOUBLE and FLOAT16 columns. Keeping the type-defined order as the default is a latent backward-compat hazard: the writer now computes finite min/max over the non-NaN subset and records nan_count, but an old reader that predates nan_count ignores it, accepts the finite bounds, and can incorrectly prune row groups that contain NaN. Readers instead ignore statistics written under an unknown sort order, so writing IEEE 754 total order by default is the safer behavior. See the discussion on #3393. This makes FLOAT, DOUBLE and FLOAT16 columns built without an explicit column order default to IEEE_754_TOTAL_ORDER (mirroring how #3610 defaults INT96 to INT96_TIMESTAMP_ORDER). Columns with a logical annotation that does not accept IEEE 754 total order (e.g. an unknown annotation) fall back to type-defined order so they remain constructible. The default-order selection is unified in PrimitiveType.defaultColumnOrder so construction and text serialization agree. To stay backward compatible on read, a footer that carries no column_orders list predates IEEE_754_TOTAL_ORDER, so floating-point columns read from such a footer are given type-defined order rather than inheriting the new construction-time default; their legacy statistics are thus not reinterpreted under IEEE 754 total order. The text schema representation now carries a non-default column order (columnorder(...) after the type/annotation) and MessageTypeParser parses it, so a column order set explicitly survives toString()/parse round-trips such as the one GroupWriteSupport performs. Columns left at their default emit no token, keeping existing schema strings unchanged. Tests that exercise the legacy type-defined NaN / +-0 semantics set TYPE_DEFINED_ORDER explicitly, and new tests cover the default serialization, the column-order-less read path, and the text round-trip. Co-authored-by: Isaac * Degrade unrecognized text-schema column order to UNDEFINED Address review feedback: MessageTypeParser.parseColumnOrder now returns ColumnOrder.undefined() for a columnorder(...) token it does not recognize, instead of throwing. This matches ParquetMetadataConverter.fromParquetColumnOrder ("not yet supported by this API") so a schema string written by a newer API with an order this version does not know stays parseable; statistics under an unknown order are ignored by readers anyway. UNDEFINED is a valid order for all primitive types, so it is safe to feed into the builder. Co-authored-by: Isaac * Reconcile differing column orders as UNDEFINED when merging schemas Address review follow-up (codex via @gszadovszky): with floats defaulting to IEEE_754_TOTAL_ORDER and legacy footers read as TYPE_DEFINED_ORDER, aggregating footers over a directory that spans the upgrade (e.g. ParquetInputFormat split planning / getGlobalMetaData, or the deprecated summary-file merge) threw IncompatibleSchemaModificationException on the otherwise-identical float column. PrimitiveType.union now reconciles a column-order-only difference to UNDEFINED instead of failing. At that point type, logical type and length already match, so the order is the only difference and the columns are otherwise mergeable. This is safe because per-file statistics are still read under each file's own column order (from its own footer); only the merged schema's ambiguous ordering claim is dropped. Added TestMessageType.testMergeMixedFloatingColumnOrder and updated testMergeSchemaWithColumnOrder, which previously asserted the merge threw. Co-authored-by: Isaac
Rationale for this change
This implements the parquet-format IEEE 754 total order column-order work from apache/parquet-format#514 in parquet-java. The existing type-defined order remains the default.
While adding the new order, this also fixes floating-point NaN handling so parquet-java preserves the exact FLOAT, DOUBLE, and FLOAT16 bit patterns supplied by applications, including NaN sign and payload bits. Filters are updated to avoid false negatives when NaN semantics cannot be answered safely from metadata.
What changes are included in this PR?
Are these changes tested?
Yes. This PR adds focused unit and end-to-end tests for the new column order, NaN counts, NaN filtering behavior, raw-bit preservation, and interop with parquet-testing files.
Are there any user-facing changes?
Yes. Users can opt into IEEE_754_TOTAL_ORDER for floating columns. The default column order is unchanged, and existing files remain readable. Filtering around NaN values may be more conservative to avoid dropping data that can still match.
Closes#406