-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[GH-3609]: Add new sort order for int96 timestamps #3610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
|
|
||
| import java.io.Serializable; | ||
| import java.nio.ByteBuffer; | ||
| import java.nio.ByteOrder; | ||
| import java.util.Comparator; | ||
| import org.apache.parquet.io.api.Binary; | ||
|
|
||
|
|
@@ -354,4 +355,47 @@ public String toString() { | |
| return "BINARY_AS_FLOAT16_IEEE_754_TOTAL_ORDER_COMPARATOR"; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Comparator for two timestamps encoded as INT96 (12-byte little-endian) binary. | ||
| * Layout: first 8 bytes = nanoseconds within the day, last 4 bytes = Julian day. | ||
| * | ||
| * Two-level comparison, matching the INT96 timestamp sort order: | ||
| * 1. Compare the last 4 bytes (Julian day) as a signed little-endian int32. | ||
| * 2. If equal, compare the first 8 bytes (nanos) as a signed little-endian int64. | ||
| */ | ||
| static final PrimitiveComparator<Binary> BINARY_AS_INT96_TIMESTAMP_COMPARATOR = new BinaryComparator() { | ||
| private static final long NANOSECONDS_PER_DAY = 86_400_000_000_000L; | ||
|
|
||
| @Override | ||
| int compareBinary(Binary b1, Binary b2) { | ||
| if (b1.length() != 12 || b2.length() != 12) { | ||
| throw new IllegalArgumentException( | ||
| "INT96 binary length must be 12, got " + b1.length() + " and " + b2.length()); | ||
| } | ||
|
|
||
| ByteBuffer bb1 = b1.toByteBuffer().slice(); | ||
| ByteBuffer bb2 = b2.toByteBuffer().slice(); | ||
| bb1.order(ByteOrder.LITTLE_ENDIAN); | ||
| bb2.order(ByteOrder.LITTLE_ENDIAN); | ||
|
|
||
| int result = Integer.compare(bb1.getInt(8), bb2.getInt(8)); | ||
| if (result != 0) return result; | ||
|
|
||
| long nanos1 = bb1.getLong(0); | ||
| long nanos2 = bb2.getLong(0); | ||
| if (nanos1 < 0 || nanos1 > NANOSECONDS_PER_DAY) { | ||
| throw new IllegalArgumentException("Invalid nanos value: " + nanos1); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that this is clear and direct, but we usually want to also state why the value is invalid. In this case, it is beyond the number of nanos per day. I'd change this to |
||
| } | ||
| if (nanos2 < 0 || nanos2 > NANOSECONDS_PER_DAY) { | ||
| throw new IllegalArgumentException("Invalid nanos value: " + nanos2); | ||
| } | ||
| return Long.compare(nanos1, nanos2); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "BINARY_AS_INT96_TIMESTAMP_COMPARATOR"; | ||
| } | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
|
|
||
| import static org.apache.parquet.schema.PrimitiveComparator.BINARY_AS_FLOAT16_COMPARATOR; | ||
| import static org.apache.parquet.schema.PrimitiveComparator.BINARY_AS_FLOAT16_IEEE_754_TOTAL_ORDER_COMPARATOR; | ||
| import static org.apache.parquet.schema.PrimitiveComparator.BINARY_AS_INT96_TIMESTAMP_COMPARATOR; | ||
| import static org.apache.parquet.schema.PrimitiveComparator.BINARY_AS_SIGNED_INTEGER_COMPARATOR; | ||
| import static org.apache.parquet.schema.PrimitiveComparator.BOOLEAN_COMPARATOR; | ||
| import static org.apache.parquet.schema.PrimitiveComparator.DOUBLE_COMPARATOR; | ||
|
|
@@ -38,6 +39,7 @@ | |
| import java.nio.ByteBuffer; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import org.apache.parquet.example.data.simple.NanoTime; | ||
| import org.apache.parquet.io.api.Binary; | ||
| import org.junit.Test; | ||
|
|
||
|
|
@@ -354,6 +356,51 @@ public void testBinaryAsSignedIntegerComparatorWithEquals() { | |
| } | ||
| } | ||
|
|
||
| private static Binary int96(int julianDay, long nanosOfDay) { | ||
| return new NanoTime(julianDay, nanosOfDay).toBinary(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testInt96TimestampComparator() { | ||
|
divjotarora marked this conversation as resolved.
|
||
| Binary[] valuesInAscendingOrder = { | ||
| int96(Integer.MIN_VALUE, 0), // most negative julian day | ||
| int96(-1, 86_399_999_999_999L), // negative julian days sort before day 0 | ||
| int96(0, 0), // start of the julian period | ||
| int96(0, 86_399_999_999_999L), // same day, later time of day | ||
| int96(2440000, 123L), // 1968-05-23T00:00:00.000000123, pre-epoch but positive julian day | ||
| int96(2458850, 43_200_000_000_000L), // 2020-01-01T12:00:00 | ||
| int96(2458881, 39_600_000_000_000L), // 2020-02-01T11:00:00, later day even though earlier time of day | ||
| int96(2458881, 39_600_000_000_001L), // 2020-02-01T11:00:00.000000001, nanos tie-break | ||
| int96(Integer.MAX_VALUE, 86_399_999_999_999L) | ||
| }; | ||
|
|
||
| for (int i = 0; i < valuesInAscendingOrder.length; ++i) { | ||
| for (int j = 0; j < valuesInAscendingOrder.length; ++j) { | ||
| assertEquals( | ||
| "comparing value " + i + " to value " + j, | ||
| Integer.signum(Integer.compare(i, j)), | ||
| Integer.signum(BINARY_AS_INT96_TIMESTAMP_COMPARATOR.compare( | ||
| valuesInAscendingOrder[i], valuesInAscendingOrder[j]))); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testInt96TimestampComparatorRejectsInvalidNanos() { | ||
| // Same Julian day so the comparator reaches the nanos validation instead of | ||
| // returning early on the day comparison. | ||
| Binary valid = int96(0, 0); | ||
| for (long invalidNanos : new long[] {-1L, Long.MIN_VALUE, 86_400_000_000_001L, Long.MAX_VALUE}) { | ||
| Binary invalid = int96(0, invalidNanos); | ||
| try { | ||
| BINARY_AS_INT96_TIMESTAMP_COMPARATOR.compare(valid, invalid); | ||
| fail("Expected IllegalArgumentException for nanos=" + invalidNanos); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use |
||
| } catch (IllegalArgumentException e) { | ||
| // expected | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testFloat16Comparator() { | ||
| Binary[] valuesInAscendingOrder = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,6 +88,7 @@ | |
| import org.apache.parquet.format.GeometryType; | ||
| import org.apache.parquet.format.GeospatialStatistics; | ||
| import org.apache.parquet.format.IEEE754TotalOrder; | ||
| import org.apache.parquet.format.Int96TimestampOrder; | ||
| import org.apache.parquet.format.IntType; | ||
| import org.apache.parquet.format.KeyValue; | ||
| import org.apache.parquet.format.LogicalType; | ||
|
|
@@ -146,6 +147,7 @@ public class ParquetMetadataConverter { | |
|
|
||
| private static final TypeDefinedOrder TYPE_DEFINED_ORDER = new TypeDefinedOrder(); | ||
| private static final IEEE754TotalOrder IEEE_754_TOTAL_ORDER = new IEEE754TotalOrder(); | ||
| private static final Int96TimestampOrder INT96_TIMESTAMP_ORDER = new Int96TimestampOrder(); | ||
| public static final MetadataFilter NO_FILTER = new NoFilter(); | ||
| public static final MetadataFilter SKIP_ROW_GROUPS = new SkipMetadataFilter(); | ||
| public static final long MAX_STATS_SIZE = 4096; // limit stats to 4k | ||
|
|
@@ -290,6 +292,9 @@ private List<ColumnOrder> getColumnOrders(MessageType schema) { | |
| case IEEE_754_TOTAL_ORDER: | ||
| columnOrder.setIEEE_754_TOTAL_ORDER(IEEE_754_TOTAL_ORDER); | ||
| break; | ||
| case INT96_TIMESTAMP_ORDER: | ||
| columnOrder.setINT96_TIMESTAMP_ORDER(INT96_TIMESTAMP_ORDER); | ||
| break; | ||
| case UNDEFINED: | ||
| // Use TypeDefinedOrder if some types (e.g. INT96) have undefined column orders. | ||
| columnOrder.setTYPE_ORDER(TYPE_DEFINED_ORDER); | ||
|
|
@@ -911,8 +916,16 @@ private static byte[] tuncateMax(BinaryTruncator truncator, int truncateLength, | |
| } | ||
|
|
||
| private static boolean isMinMaxStatsSupported(PrimitiveType type) { | ||
| return type.columnOrder().getColumnOrderName() == ColumnOrderName.TYPE_DEFINED_ORDER | ||
| || type.columnOrder().getColumnOrderName() == ColumnOrderName.IEEE_754_TOTAL_ORDER; | ||
| switch (type.columnOrder().getColumnOrderName()) { | ||
| case TYPE_DEFINED_ORDER: | ||
| case IEEE_754_TOTAL_ORDER: | ||
| case INT96_TIMESTAMP_ORDER: | ||
| return true; | ||
| case UNDEFINED: | ||
| return false; | ||
| default: | ||
| throw new IllegalArgumentException("Unknown column order: " + type.columnOrder()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think we would normal throw |
||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -2057,7 +2070,17 @@ private void buildChildren( | |
| || schemaElement.converted_type == ConvertedType.INTERVAL)) { | ||
| columnOrder = org.apache.parquet.schema.ColumnOrder.undefined(); | ||
| } | ||
| // INT96_TIMESTAMP_ORDER is only valid for INT96 columns, ignore it anywhere else. | ||
| if (columnOrder.getColumnOrderName() == ColumnOrderName.INT96_TIMESTAMP_ORDER | ||
| && schemaElement.type != Type.INT96) { | ||
| columnOrder = org.apache.parquet.schema.ColumnOrder.undefined(); | ||
| } | ||
| primitiveBuilder.columnOrder(columnOrder); | ||
| } else if (schemaElement.type == Type.INT96) { | ||
| // A footer without column orders predates INT96_TIMESTAMP_ORDER, so an INT96 column here | ||
| // must not inherit the (chronological) construction-time default: its stats, if any, were | ||
| // written under the legacy order and must be ignored. | ||
| primitiveBuilder.columnOrder(org.apache.parquet.schema.ColumnOrder.undefined()); | ||
|
divjotarora marked this conversation as resolved.
|
||
| } | ||
| childBuilder = primitiveBuilder; | ||
| } else { | ||
|
|
@@ -2110,6 +2133,9 @@ private static org.apache.parquet.schema.ColumnOrder fromParquetColumnOrder(Colu | |
| if (columnOrder.isSetIEEE_754_TOTAL_ORDER()) { | ||
| return org.apache.parquet.schema.ColumnOrder.ieee754TotalOrder(); | ||
| } | ||
| if (columnOrder.isSetINT96_TIMESTAMP_ORDER()) { | ||
| return org.apache.parquet.schema.ColumnOrder.int96TimestampOrder(); | ||
| } | ||
| // The column order is not yet supported by this API | ||
| return org.apache.parquet.schema.ColumnOrder.undefined(); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.