Skip to content

Commit ca73573

Browse files
committed
address PR comments
1 parent 804feb3 commit ca73573

6 files changed

Lines changed: 102 additions & 55 deletions

File tree

parquet-column/src/main/java/org/apache/parquet/schema/PrimitiveComparator.java

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.io.Serializable;
2222
import java.nio.ByteBuffer;
23+
import java.nio.ByteOrder;
2324
import java.util.Comparator;
2425
import org.apache.parquet.io.api.Binary;
2526

@@ -288,29 +289,16 @@ int compareBinary(Binary b1, Binary b2) {
288289
throw new IllegalArgumentException(
289290
"Timestamp binary length must be 12 bytes, got " + b1.length() + " and " + b2.length());
290291
}
291-
292-
ByteBuffer bb1 = b1.toByteBuffer();
293-
ByteBuffer bb2 = b2.toByteBuffer();
294-
// The buffers may be slices with a non-zero position (e.g. a ByteBuffer-backed Binary), so
295-
// index relative to position() rather than absolute offset 0. Byte 11 (position + 11) is the
296-
// most significant byte and carries the sign; byte 0 (position) is least significant.
297-
int p1 = bb1.position();
298-
int p2 = bb2.position();
299-
300-
// If one value is negative and the other is positive, one is trivially greater.
301-
boolean neg1 = bb1.get(p1 + 11) < 0;
302-
boolean neg2 = bb2.get(p2 + 11) < 0;
303-
if (neg1 != neg2) {
304-
return neg1 ? -1 : 1;
292+
ByteBuffer bb1 = b1.toByteBuffer().slice();
293+
bb1.order(ByteOrder.LITTLE_ENDIAN);
294+
ByteBuffer bb2 = b2.toByteBuffer().slice();
295+
bb2.order(ByteOrder.LITTLE_ENDIAN);
296+
// Signed comparison of the high 4 bytes followed by unsigned comparison of the low 8 bytes.
297+
int hiResult = Integer.compare(bb1.getInt(8), bb2.getInt(8));
298+
if (hiResult != 0) {
299+
return hiResult;
305300
}
306-
307-
for (int i = 11; i >= 0; --i) {
308-
int diff = toUnsigned(bb1.get(p1 + i)) - toUnsigned(bb2.get(p2 + i));
309-
if (diff != 0) {
310-
return diff;
311-
}
312-
}
313-
return 0;
301+
return Long.compareUnsigned(bb1.getLong(0), bb2.getLong(0));
314302
}
315303

316304
@Override

parquet-column/src/main/java/org/apache/parquet/schema/Types.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -563,10 +563,13 @@ public Optional<Boolean> visit(
563563
@Override
564564
public Optional<Boolean> visit(
565565
LogicalTypeAnnotation.TimestampLogicalTypeAnnotation timestampLogicalType) {
566-
if (primitiveType == PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY) {
567-
return checkFixedPrimitiveType(12, timestampLogicalType);
568-
}
569-
return checkInt64PrimitiveType(timestampLogicalType);
566+
Preconditions.checkState(
567+
primitiveType == PrimitiveTypeName.INT64
568+
|| (primitiveType == PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY
569+
&& length == 12),
570+
"%s can only annotate INT64 or FIXED_LEN_BYTE_ARRAY(12)",
571+
timestampLogicalType);
572+
return Optional.of(true);
570573
}
571574

572575
@Override

parquet-column/src/test/java/org/apache/parquet/schema/TestPrimitiveComparator.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,10 @@ public void testBinaryAsSignedIntegerLE12Comparator() {
455455
// an LSB-first comparator would wrongly rank +256 < +1 (seeing byte[0]=0 < 1 first)
456456
byte[] posTwoFiftySix = new byte[12];
457457
posTwoFiftySix[1] = 1;
458+
// +2^63: low 8 bytes have bit 63 set (LE: bytes 0..6=0x00, byte 7=0x80), high 4 bytes=0x00.
459+
// Sits between small positives and largePos; fails if the lower 8 bytes are compared signed.
460+
byte[] posTwo63 = new byte[12];
461+
posTwo63[7] = (byte) 0x80;
458462
// large positive: 0x7F FF..FF (most positive 96-bit value)
459463
byte[] largePos = new byte[12];
460464
for (int i = 0; i < 12; i++) largePos[i] = (byte) 0xFF;
@@ -467,6 +471,7 @@ public void testBinaryAsSignedIntegerLE12Comparator() {
467471
Binary.fromConstantByteArray(zero),
468472
Binary.fromConstantByteArray(posOne),
469473
Binary.fromConstantByteArray(posTwoFiftySix),
474+
Binary.fromConstantByteArray(posTwo63),
470475
Binary.fromConstantByteArray(largePos));
471476
}
472477

parquet-column/src/test/java/org/apache/parquet/schema/TestTypeBuilders.java

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
*/
1919
package org.apache.parquet.schema;
2020

21-
import static org.apache.parquet.schema.LogicalTypeAnnotation.TimeUnit;
2221
import static org.apache.parquet.schema.LogicalTypeAnnotation.TimeUnit.MICROS;
2322
import static org.apache.parquet.schema.LogicalTypeAnnotation.TimeUnit.MILLIS;
2423
import static org.apache.parquet.schema.LogicalTypeAnnotation.timestampType;
@@ -535,7 +534,8 @@ public void testInt32AnnotationsRejectNonInt32() {
535534

536535
@Test
537536
public void testInt64Annotations() {
538-
OriginalType[] types = new OriginalType[] {TIME_MICROS, TIMESTAMP_MILLIS, TIMESTAMP_MICROS, UINT_64, INT_64};
537+
// Test the non-timestamp annotations for INT64. Timestamps are tested separately below.
538+
OriginalType[] types = new OriginalType[] {TIME_MICROS, UINT_64, INT_64};
539539
for (OriginalType logicalType : types) {
540540
PrimitiveType expected = new PrimitiveType(REQUIRED, INT64, "col", logicalType);
541541
PrimitiveType date = Types.required(INT64).as(logicalType).named("col");
@@ -545,7 +545,8 @@ public void testInt64Annotations() {
545545

546546
@Test
547547
public void testInt64AnnotationsRejectNonInt64() {
548-
OriginalType[] types = new OriginalType[] {TIME_MICROS, TIMESTAMP_MILLIS, TIMESTAMP_MICROS, UINT_64, INT_64};
548+
// Test the non-timestamp annotations for INT64. Timestamps are tested separately below.
549+
OriginalType[] types = new OriginalType[] {TIME_MICROS, UINT_64, INT_64};
549550
for (final OriginalType logicalType : types) {
550551
PrimitiveTypeName[] nonInt64 = new PrimitiveTypeName[] {BOOLEAN, INT32, INT96, DOUBLE, FLOAT, BINARY};
551552
for (final PrimitiveTypeName type : nonInt64) {
@@ -554,19 +555,54 @@ public void testInt64AnnotationsRejectNonInt64() {
554555
.hasMessage(
555556
LogicalTypeAnnotation.fromOriginalType(logicalType, null) + " can only annotate INT64");
556557
}
557-
// TIMESTAMP allows FLBA(12); other lengths are still rejected.
558-
// Non-timestamp types still only accept INT64 for any FLBA length.
559-
boolean isTimestamp = logicalType == TIMESTAMP_MILLIS || logicalType == TIMESTAMP_MICROS;
560-
String flbaErrMsg = isTimestamp
561-
? LogicalTypeAnnotation.fromOriginalType(logicalType, null)
562-
+ " can only annotate FIXED_LEN_BYTE_ARRAY(12)"
563-
: LogicalTypeAnnotation.fromOriginalType(logicalType, null) + " can only annotate INT64";
564558
assertThatThrownBy(() -> Types.required(FIXED_LEN_BYTE_ARRAY)
565559
.length(1)
566560
.as(logicalType)
567561
.named("col"))
568562
.isInstanceOf(IllegalStateException.class)
569-
.hasMessage(flbaErrMsg);
563+
.hasMessage(LogicalTypeAnnotation.fromOriginalType(logicalType, null) + " can only annotate INT64");
564+
}
565+
}
566+
567+
@Test
568+
public void testTimestampAnnotations() {
569+
// Test timestamp annotations for both INT64 and FLBA(12).
570+
OriginalType[] types = new OriginalType[] {TIMESTAMP_MILLIS, TIMESTAMP_MICROS};
571+
for (OriginalType logicalType : types) {
572+
PrimitiveType expectedInt64 = new PrimitiveType(REQUIRED, INT64, "col", logicalType);
573+
PrimitiveType dateInt64 = Types.required(INT64).as(logicalType).named("col");
574+
assertThat(dateInt64).isEqualTo(expectedInt64);
575+
576+
PrimitiveType expectedFlba12 = new PrimitiveType(REQUIRED, FIXED_LEN_BYTE_ARRAY, 12, "col", logicalType);
577+
PrimitiveType dateFlba12 = Types.required(FIXED_LEN_BYTE_ARRAY)
578+
.length(12)
579+
.as(logicalType)
580+
.named("col");
581+
assertThat(dateFlba12).isEqualTo(expectedFlba12);
582+
}
583+
}
584+
585+
@Test
586+
public void testTimestampAnnotationsRejectNonTimestamp() {
587+
// Test timestamp annotations for both INT64 and FLBA(12).
588+
OriginalType[] types = new OriginalType[] {TIMESTAMP_MILLIS, TIMESTAMP_MICROS};
589+
for (OriginalType logicalType : types) {
590+
// Invalid primitive types are rejected.
591+
PrimitiveTypeName[] nonTimestamp = new PrimitiveTypeName[] {BOOLEAN, INT32, INT96, DOUBLE, FLOAT, BINARY};
592+
for (PrimitiveTypeName type : nonTimestamp) {
593+
assertThatThrownBy(() -> Types.required(type).as(logicalType).named("col"))
594+
.isInstanceOf(IllegalStateException.class)
595+
.hasMessage(LogicalTypeAnnotation.fromOriginalType(logicalType, null)
596+
+ " can only annotate INT64 or FIXED_LEN_BYTE_ARRAY(12)");
597+
}
598+
// Invalid FLBA lengths are rejected.
599+
assertThatThrownBy(() -> Types.required(FIXED_LEN_BYTE_ARRAY)
600+
.length(11)
601+
.as(logicalType)
602+
.named("col"))
603+
.isInstanceOf(IllegalStateException.class)
604+
.hasMessage(LogicalTypeAnnotation.fromOriginalType(logicalType, null)
605+
+ " can only annotate INT64 or FIXED_LEN_BYTE_ARRAY(12)");
570606
}
571607
}
572608

@@ -1451,23 +1487,6 @@ public void testTimestampLogicalTypeWithUTCParameter() {
14511487
assertThat(nonUtcMicrosActual).isEqualTo(nonUtcMicrosExpected);
14521488
}
14531489

1454-
@Test
1455-
public void testTimestampFlba12LogicalType() {
1456-
for (TimeUnit unit : TimeUnit.values()) {
1457-
// FLBA(12) with TIMESTAMP annotation is valid.
1458-
Types.required(FIXED_LEN_BYTE_ARRAY)
1459-
.length(12)
1460-
.as(timestampType(true, unit))
1461-
.named("ts");
1462-
// Other FLBA lengths must be rejected.
1463-
assertThatThrownBy(() -> Types.required(FIXED_LEN_BYTE_ARRAY)
1464-
.length(11)
1465-
.as(timestampType(true, unit))
1466-
.named("ts"))
1467-
.isInstanceOf(IllegalStateException.class);
1468-
}
1469-
}
1470-
14711490
@Test
14721491
public void testVariantLogicalType() {
14731492
byte specVersion = 1;

parquet-hadoop/src/main/java/org/apache/parquet/format/converter/ParquetMetadataConverter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,14 @@ public void visit(PrimitiveType primitiveType) {
318318
element.setRepetition_type(toParquetRepetition(primitiveType.getRepetition()));
319319
element.setType(getType(primitiveType.getPrimitiveTypeName()));
320320
if (primitiveType.getLogicalTypeAnnotation() != null) {
321-
element.setConverted_type(convertToConvertedType(primitiveType.getLogicalTypeAnnotation()));
321+
// The TimestampType logical type may have a converted type, but only for the INT64
322+
// physical type.
323+
boolean suppressConvertedType = primitiveType.getLogicalTypeAnnotation()
324+
instanceof LogicalTypeAnnotation.TimestampLogicalTypeAnnotation
325+
&& primitiveType.getPrimitiveTypeName() != PrimitiveTypeName.INT64;
326+
if (!suppressConvertedType) {
327+
element.setConverted_type(convertToConvertedType(primitiveType.getLogicalTypeAnnotation()));
328+
}
322329
element.setLogicalType(convertToLogicalType(primitiveType.getLogicalTypeAnnotation()));
323330
}
324331
if (primitiveType.getDecimalMetadata() != null) {

parquet-hadoop/src/test/java/org/apache/parquet/format/converter/TestParquetMetadataConverter.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,18 @@ public void testTimeLogicalTypes() {
449449
.required(PrimitiveTypeName.INT64)
450450
.as(timestampType(true, NANOS))
451451
.named("aTimestampUtcNanos")
452+
.required(PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY)
453+
.length(12)
454+
.as(timestampType(true, MILLIS))
455+
.named("aTimestampFlbaMillis")
456+
.required(PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY)
457+
.length(12)
458+
.as(timestampType(true, MICROS))
459+
.named("aTimestampFlbaMicros")
460+
.required(PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY)
461+
.length(12)
462+
.as(timestampType(true, NANOS))
463+
.named("aTimestampFlbaNanos")
452464
.required(PrimitiveTypeName.INT32)
453465
.as(timeType(false, MILLIS))
454466
.named("aTimeNonUtcMillis")
@@ -469,6 +481,19 @@ public void testTimeLogicalTypes() {
469481
.named("aTimeUtcNanos")
470482
.named("Message");
471483
List<SchemaElement> parquetSchema = parquetMetadataConverter.toParquetSchema(expected);
484+
// FLBA(12) MILLIS/MICROS must not write a legacy converted_type (it is INT64-only).
485+
SchemaElement flbaMillis = parquetSchema.stream()
486+
.filter(e -> "aTimestampFlbaMillis".equals(e.getName()))
487+
.findFirst()
488+
.get();
489+
assertThat(flbaMillis.isSetConverted_type()).isFalse();
490+
assertThat(flbaMillis.isSetLogicalType()).isTrue();
491+
SchemaElement flbaMicros = parquetSchema.stream()
492+
.filter(e -> "aTimestampFlbaMicros".equals(e.getName()))
493+
.findFirst()
494+
.get();
495+
assertThat(flbaMicros.isSetConverted_type()).isFalse();
496+
assertThat(flbaMicros.isSetLogicalType()).isTrue();
472497
MessageType schema = parquetMetadataConverter.fromParquetSchema(parquetSchema, null);
473498
assertThat(schema).isEqualTo(expected);
474499
}

0 commit comments

Comments
 (0)