Encode non-negative nanos for negative ORC timestamps - #23391
Conversation
|
Thanks for working on this. I tested the current PR head against the original values from #19350 and additional near-epoch boundaries. The good news is that the original #19350 values written by libcudf can now be read by Apache ORC without However, the current implementation introduces a libcudf ORC round-trip correctness regression for negative timestamps in [P1] Preserve near-epoch negative timestamps — This focused C++ test can be added after TEST_F(OrcWriterTest, NegativeTimestampWithinOneSecondOfEpoch)
{
test_negative_fractional_timestamp_roundtrip<cudf::timestamp_us>(
{-1L, -500L, -500'000L, -999'000L, -999'001L, -999'999L, -1'000'001L, -5'999'500L});
}With this PR, the test reports the equivalent of: The same case can be reproduced through the Java bindings by adding this to @Test
void testORCNegativeTimestampWithinOneSecondOfEpoch() throws IOException {
long[] values = {
-1L,
-500L,
-500_000L,
-999_000L,
-999_001L,
-999_999L,
-1_000_001L,
-5_999_500L
};
try (TempFile tempFile = TempFile.create("near-epoch", ".orc");
ColumnVector timestamps = ColumnVector.timestampMicroSecondsFromLongs(values);
Table expected = new Table(timestamps)) {
File file = tempFile.getFile();
ORCWriterOptions writeOptions = ORCWriterOptions.builder()
.withNonNullableColumns("ts")
.build();
try (TableWriter writer = Table.writeORCChunked(writeOptions, file)) {
writer.write(expected);
}
ORCOptions readOptions = ORCOptions.builder()
.withTimeUnit(DType.TIMESTAMP_MICROSECONDS)
.build();
try (Table actual = Table.readORC(readOptions, file)) {
assertTablesAreEqual(expected, actual);
}
}
}I also checked the behavior before this PR: the libcudf GPU write/read round trip preserves these near-epoch values exactly, while an Apache ORC reader fails on the old negative-nanos encoding. So this is a regression introduced by the normalization in this PR, rather than pre-existing libcudf reader behavior. A simple |
Express the writer as a direct transcription of the Apache ORC rule and pin the near-epoch behavior that the format cannot represent in a test.
|
Thanks for testing this so thoroughly. The one second shift you found for ORC stores a timestamp as (seconds, nanos) with nanos in if (millis < 0 && newNanos > 999_999) { millis -= TimestampTreeWriter.MILLIS_PER_SECOND; }The Apache writers cancel that borrow on write ( Apache treats this as a known limitation of the format rather than a bug to fix, and asserts it in if (seconds[r] == -1) {
// reproduce the JDK bug of java.sql.Timestamp see ORC-763
// Wrong extra second: 1969-12-31 23.59.59.001 -> 1970-01-01 00.00.00.001
assertEquals(0, timestamps.getTimestampAsLong(r));
}I ran the external-reader check you asked for: wrote your values with this PR and read the file back with the Apache ORC C++ reader (via pyarrow). No exception, and the result matches your list exactly.
So libcudf and Apache now agree on every value here, which I believe is the property that matters for Spark: a file written on the GPU decodes to the same values as a file written by CPU Spark from the same input. The alternative that keeps the libcudf round trip lossless for this range is the old negative-nanos encoding, which is precisely what makes Apache readers throw Note that the affected range is Updated in the latest commit: the writer is now a direct transcription of the Apache rule (floor seconds and non-negative nanos, then give back the second the reader borrows), and your near-epoch values are covered by a new |
|
AI took the liberty to reply with a detailed explanation of the findings, but I haven't checked them myself 🙈 |
|
This PR now matches Apache ORC Java's timestamp encoding exactly. However, please note that it also inherits a bug from Apache ORC Java. For timestamp values in the range [-999 ms, 0), the sign is lost during encoding, and the values are read back one second later:
The original negative value cannot be restored because its encoded (seconds, nanos) representation is identical to that of the corresponding positive timestamp. I am OK with this PR because cudf-spark should match Apache ORC Java's behavior, including this known limitation. |
Description
Fixes #19350.
The ORC writer split negative timestamps with a fractional second into a negative nanos remainder, which was then stored in the unsigned
SECONDARYstream as a large value. The libcudf reader round-tripped this correctly, but Apache ORC readers (e.g. Spark) failed withnanos > 999999999 or < 0.The writer now emits the same (seconds, nanos) pair as the Apache ORC writer: floor seconds with a non-negative nanos remainder, plus the second that Apache readers borrow back when the stored seconds are negative and the stored nanos are at least 1 ms (ORC-306/ORC-763). Existing files remain readable.
One consequence is inherited from the format: timestamps within 999 ms before the epoch are stored with zero seconds, so no reader can tell them apart from the same nanos one second later, and they read back one second too late. Apache ORC has the same limitation and asserts it in its own tests (ORC-763, ORC-771);
OrcWriterTest.NegativeTimestampsNearEpochpins the behavior for libcudf.Checklist