Skip to content

Commit 50e30f3

Browse files
committed
Merge branch 'main' into minhash-perf
2 parents d0b17c1 + ad2eccf commit 50e30f3

4 files changed

Lines changed: 345 additions & 19 deletions

File tree

‎java/src/main/java/ai/rapids/cudf/VariantUtils.java‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public class VariantUtils {
2323
DType.DTypeEnum.STRING, DType.DTypeEnum.INT8, DType.DTypeEnum.INT16,
2424
DType.DTypeEnum.INT32, DType.DTypeEnum.INT64, DType.DTypeEnum.FLOAT32,
2525
DType.DTypeEnum.FLOAT64, DType.DTypeEnum.BOOL8, DType.DTypeEnum.DECIMAL32,
26-
DType.DTypeEnum.DECIMAL64, DType.DTypeEnum.DECIMAL128);
26+
DType.DTypeEnum.DECIMAL64, DType.DTypeEnum.DECIMAL128,
27+
DType.DTypeEnum.TIMESTAMP_MICROSECONDS, DType.DTypeEnum.TIMESTAMP_NANOSECONDS);
2728

2829
private VariantUtils() {}
2930

@@ -61,6 +62,8 @@ public static ColumnVector getVariantFieldValue(ColumnView variantStruct, String
6162
* the value blob is empty, or its header is unrecognized. An encoded Variant null is represented
6263
* by a valid {@link VariantLogicalType#NULL_VALUE} row. Only the header byte is inspected, so a
6364
* recognized header is classified even if the remaining payload is truncated.
65+
* Timestamp logical IDs distinguish timezone-adjusted from NTZ encodings, but do not
66+
* distinguish microsecond from nanosecond units.
6467
*
6568
* <p>This API mirrors an experimental libcudf API and is subject to change.
6669
*
@@ -76,18 +79,24 @@ public static ColumnVector getVariantTypeId(ColumnView valueBytes) {
7679
* Decode raw Variant-encoded value bytes into {@code targetType}. Supported target types are
7780
* {@link DType#STRING}, {@link DType#INT8}, {@link DType#INT16}, {@link DType#INT32},
7881
* {@link DType#INT64}, {@link DType#FLOAT32}, {@link DType#FLOAT64}, {@link DType#BOOL8},
82+
* {@link DType#TIMESTAMP_MICROSECONDS}, {@link DType#TIMESTAMP_NANOSECONDS},
7983
* and decimal types created with {@link DType#create(DType.DTypeEnum, int)} using
8084
* {@link DType.DTypeEnum#DECIMAL32}, {@link DType.DTypeEnum#DECIMAL64}, or
81-
* {@link DType.DTypeEnum#DECIMAL128}. Non-decimal decoding requires the encoded physical type
82-
* to exactly match {@code targetType}; no numeric conversions are performed. Any encoded
83-
* decimal width can be decoded to any supported decimal width. Decimal values are rescaled to
84-
* the target scale, truncating toward zero when fractional digits are discarded. Input nulls,
85+
* {@link DType.DTypeEnum#DECIMAL128}. Except for the timestamp encodings described below,
86+
* non-decimal decoding requires the encoded physical type to exactly match {@code targetType};
87+
* no numeric conversions are performed. Any encoded decimal width can be decoded to any
88+
* supported decimal width. Decimal values are rescaled to the target scale, truncating toward
89+
* zero when fractional digits are discarded. Input nulls,
8590
* encoded Variant nulls, physical-type mismatches, malformed values, and decimal overflows
8691
* produce null output rows.
8792
*
8893
* <p>The target decimal scale uses cuDF's base-10 exponent convention: scale {@code -2}
8994
* represents hundredths, while scale {@code 2} represents hundreds. Overflow is checked against
9095
* the target's integer storage width; no declared decimal precision is enforced.
96+
*
97+
* <p>Timestamp targets accept both timezone-adjusted and NTZ encodings of the same unit
98+
* and preserve the signed 64-bit payload unchanged. No timezone or unit conversion is
99+
* performed. Mismatched units and truncated timestamp payloads produce null rows.
91100
*/
92101
public static ColumnVector castVariantValue(ColumnView valueBytes, DType targetType) {
93102
Objects.requireNonNull(valueBytes, "valueBytes");
@@ -100,11 +109,14 @@ public static ColumnVector castVariantValue(ColumnView valueBytes, DType targetT
100109
* Extract a Variant field and decode it into {@code targetType} in one native call.
101110
* Supported target types are {@link DType#STRING}, {@link DType#INT8}, {@link DType#INT16},
102111
* {@link DType#INT32}, {@link DType#INT64}, {@link DType#FLOAT32}, {@link DType#FLOAT64}, and
103-
* {@link DType#BOOL8}, plus decimal types created with
112+
* {@link DType#BOOL8}, {@link DType#TIMESTAMP_MICROSECONDS},
113+
* {@link DType#TIMESTAMP_NANOSECONDS}, plus decimal types created with
104114
* {@link DType#create(DType.DTypeEnum, int)} using {@link DType.DTypeEnum#DECIMAL32},
105-
* {@link DType.DTypeEnum#DECIMAL64}, or {@link DType.DTypeEnum#DECIMAL128}. Non-decimal decoding
106-
* requires the encoded physical type to exactly match {@code targetType}; no numeric conversions
107-
* are performed. Any encoded decimal width can be decoded to any supported decimal width.
115+
* {@link DType.DTypeEnum#DECIMAL64}, or {@link DType.DTypeEnum#DECIMAL128}.
116+
* Except for the timestamp encodings described below, non-decimal decoding requires the encoded
117+
* physical type to exactly
118+
* match {@code targetType}; no numeric conversions are performed. Any encoded decimal width can
119+
* be decoded to any supported decimal width.
108120
* Decimal values are rescaled to the target scale, truncating toward zero when fractional digits
109121
* are discarded. Missing fields, input nulls, encoded Variant nulls, physical-type mismatches,
110122
* malformed values, and decimal overflows produce null output rows.
@@ -113,6 +125,10 @@ public static ColumnVector castVariantValue(ColumnView valueBytes, DType targetT
113125
* represents hundredths, while scale {@code 2} represents hundreds. Overflow is checked against
114126
* the target's integer storage width; no declared decimal precision is enforced.
115127
*
128+
* <p>Timestamp targets accept both timezone-adjusted and NTZ encodings of the same unit
129+
* and preserve the signed 64-bit payload unchanged. No timezone or unit conversion is
130+
* performed. Mismatched units and truncated timestamp payloads produce null rows.
131+
*
116132
* @param variantStruct Variant materialization: STRUCT(metadata LIST&lt;UINT8&gt;,
117133
* value LIST&lt;UINT8&gt;, optional shredded children...)
118134
* @param path JSONPath-like path accepted by cuDF's Variant extractor. Object-field steps use

‎java/src/test/java/ai/rapids/cudf/VariantUtilsTest.java‎

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,20 @@ private static List<Byte> float32(float value) {
237237
(bits >>> 24) & 0xff);
238238
}
239239

240+
private static List<Byte> timestamp(long value, boolean nanoseconds, boolean ntz) {
241+
// Variant primitive type IDs: TIMESTAMP_MICROS=12, TIMESTAMP_NTZ_MICROS=13,
242+
// TIMESTAMP_NANOS=18, TIMESTAMP_NTZ_NANOS=19. All have an int64 payload.
243+
int primitiveType;
244+
if (nanoseconds) {
245+
primitiveType = ntz ? 19 : 18;
246+
} else {
247+
primitiveType = ntz ? 13 : 12;
248+
}
249+
List<Byte> encoded = int64(value);
250+
encoded.set(0, oneByte(simple(primitiveType), "timestamp header"));
251+
return encoded;
252+
}
253+
240254
private static List<Byte> float64(double value) {
241255
long bits = Double.doubleToRawLongBits(value);
242256
return bytes(simple(0x07),
@@ -916,6 +930,233 @@ void emptyInputSupportsEveryDecimalOutputWidthAndScale() {
916930
}
917931
}
918932

933+
@Test
934+
void castTimestampValuesPreservesPayloadForBothUnitsAndEncodings() {
935+
for (boolean nanoseconds : new boolean[] {false, true}) {
936+
DType target = nanoseconds ? DType.TIMESTAMP_NANOSECONDS : DType.TIMESTAMP_MICROSECONDS;
937+
for (boolean ntz : new boolean[] {false, true}) {
938+
try (ColumnVector values = ColumnVector.fromLists(
939+
BINARY_TYPE,
940+
VariantEncoder.timestamp(Long.MIN_VALUE, nanoseconds, ntz),
941+
VariantEncoder.timestamp(-1700000000000123L, nanoseconds, ntz),
942+
VariantEncoder.timestamp(-1, nanoseconds, ntz),
943+
VariantEncoder.timestamp(0, nanoseconds, ntz),
944+
VariantEncoder.timestamp(1, nanoseconds, ntz),
945+
VariantEncoder.timestamp(1700000000000123L, nanoseconds, ntz),
946+
VariantEncoder.timestamp(Long.MAX_VALUE, nanoseconds, ntz));
947+
ColumnVector result = VariantUtils.castVariantValue(values, target);
948+
ColumnVector ticks = ColumnVector.fromLongs(
949+
Long.MIN_VALUE, -1700000000000123L, -1, 0, 1, 1700000000000123L, Long.MAX_VALUE);
950+
ColumnVector expected = ticks.castTo(target)) {
951+
assertColumnsAreEqual(expected, result);
952+
}
953+
}
954+
}
955+
}
956+
957+
@Test
958+
void extractTimestampValuesPreservesPayloadForBothUnitsAndEncodings() {
959+
VariantEncoder encoder = new VariantEncoder("time");
960+
for (boolean nanoseconds : new boolean[] {false, true}) {
961+
DType target = nanoseconds ? DType.TIMESTAMP_NANOSECONDS : DType.TIMESTAMP_MICROSECONDS;
962+
for (boolean ntz : new boolean[] {false, true}) {
963+
List<StructData> rows = new ArrayList<>();
964+
for (long ticks : new long[] {
965+
Long.MIN_VALUE, -1700000000000123L, -1, 0, 1, 1700000000000123L, Long.MAX_VALUE}) {
966+
rows.add(variant(encoder.metadata(), encoder.object(fields(
967+
field("time", VariantEncoder.timestamp(ticks, nanoseconds, ntz))))));
968+
}
969+
try (ColumnVector values = ColumnVector.fromStructs(
970+
VARIANT_TYPE, rows.toArray(new StructData[0]));
971+
ColumnVector result = VariantUtils.extractVariantField(values, "$.time", target);
972+
ColumnVector ticks = ColumnVector.fromLongs(
973+
Long.MIN_VALUE, -1700000000000123L, -1, 0, 1, 1700000000000123L, Long.MAX_VALUE);
974+
ColumnVector expected = ticks.castTo(target)) {
975+
assertColumnsAreEqual(expected, result);
976+
}
977+
}
978+
}
979+
}
980+
981+
@Test
982+
void castTimestampSliceRejectsOtherUnitsTypesAndTruncatedPayloads() {
983+
for (boolean nanoseconds : new boolean[] {false, true}) {
984+
DType target = nanoseconds ? DType.TIMESTAMP_NANOSECONDS : DType.TIMESTAMP_MICROSECONDS;
985+
List<Byte> truncated = new ArrayList<>(VariantEncoder.timestamp(123, nanoseconds, false));
986+
truncated.remove(truncated.size() - 1);
987+
try (ColumnVector values = ColumnVector.fromLists(
988+
BINARY_TYPE,
989+
VariantEncoder.int8(0),
990+
VariantEncoder.timestamp(7, nanoseconds, false),
991+
VariantEncoder.timestamp(-7, nanoseconds, true),
992+
VariantEncoder.timestamp(7, !nanoseconds, false),
993+
VariantEncoder.timestamp(-7, !nanoseconds, true),
994+
VariantEncoder.int64(7),
995+
VariantEncoder.string("2026-10-23"),
996+
VariantEncoder.nullValue(),
997+
VariantEncoder.primitiveHeader(nanoseconds ? 18 : 12),
998+
truncated,
999+
null);
1000+
ColumnVector sliced = values.subVector(1, 11);
1001+
ColumnVector result = VariantUtils.castVariantValue(sliced, target);
1002+
ColumnVector ticks = ColumnVector.fromBoxedLongs(
1003+
7L, -7L, null, null, null, null, null, null, null, null);
1004+
ColumnVector expected = ticks.castTo(target)) {
1005+
assertColumnsAreEqual(expected, result);
1006+
}
1007+
}
1008+
}
1009+
1010+
@Test
1011+
void extractTimestampSlicePreservesNullsAndRejectsMismatches() {
1012+
VariantEncoder encoder = new VariantEncoder("time");
1013+
for (boolean nanoseconds : new boolean[] {false, true}) {
1014+
DType target = nanoseconds ? DType.TIMESTAMP_NANOSECONDS : DType.TIMESTAMP_MICROSECONDS;
1015+
List<Byte> truncated = new ArrayList<>(VariantEncoder.timestamp(123, nanoseconds, true));
1016+
truncated.remove(truncated.size() - 1);
1017+
try (ColumnVector values = ColumnVector.fromStructs(
1018+
VARIANT_TYPE,
1019+
variant(encoder.metadata(), encoder.object(fields())),
1020+
variant(encoder.metadata(), encoder.object(fields(
1021+
field("time", VariantEncoder.timestamp(7, nanoseconds, false))))),
1022+
variant(encoder.metadata(), encoder.object(fields(
1023+
field("time", VariantEncoder.timestamp(-7, nanoseconds, true))))),
1024+
variant(encoder.metadata(), encoder.object(fields(
1025+
field("time", VariantEncoder.timestamp(7, !nanoseconds, false))))),
1026+
variant(encoder.metadata(), encoder.object(fields(
1027+
field("time", VariantEncoder.timestamp(-7, !nanoseconds, true))))),
1028+
variant(encoder.metadata(), encoder.object(fields(
1029+
field("time", VariantEncoder.int64(7))))),
1030+
variant(encoder.metadata(), encoder.object(fields(
1031+
field("time", VariantEncoder.nullValue())))),
1032+
variant(encoder.metadata(), encoder.object(fields(
1033+
field("time", VariantEncoder.primitiveHeader(nanoseconds ? 19 : 13))))),
1034+
variant(encoder.metadata(), encoder.object(fields(field("time", truncated)))),
1035+
variant(encoder.metadata(), encoder.object(fields())),
1036+
null);
1037+
ColumnVector sliced = values.subVector(1, 11);
1038+
ColumnVector result = VariantUtils.extractVariantField(sliced, "time", target);
1039+
ColumnVector ticks = ColumnVector.fromBoxedLongs(
1040+
7L, -7L, null, null, null, null, null, null, null, null);
1041+
ColumnVector expected = ticks.castTo(target)) {
1042+
assertColumnsAreEqual(expected, result);
1043+
}
1044+
}
1045+
}
1046+
1047+
@Test
1048+
void timestampMixedRowsCrossValidityMaskBoundaries() {
1049+
VariantEncoder encoder = new VariantEncoder("time");
1050+
for (boolean nanoseconds : new boolean[] {false, true}) {
1051+
DType target = nanoseconds ? DType.TIMESTAMP_NANOSECONDS : DType.TIMESTAMP_MICROSECONDS;
1052+
List<List<Byte>> rawRows = new ArrayList<>();
1053+
List<StructData> variantRows = new ArrayList<>();
1054+
// Slice away three leading nulls to exercise an unaligned input offset as well as
1055+
// multiple 32-bit validity words, including a partially populated final word.
1056+
for (int i = 0; i < 3; i++) {
1057+
rawRows.add(null);
1058+
variantRows.add(null);
1059+
}
1060+
Long[] expectedTicks = new Long[161];
1061+
for (int i = 0; i < expectedTicks.length; i++) {
1062+
List<Byte> value;
1063+
switch (i % 8) {
1064+
case 0:
1065+
expectedTicks[i] = (long) i + 1;
1066+
value = VariantEncoder.timestamp(expectedTicks[i], nanoseconds, false);
1067+
break;
1068+
case 1:
1069+
expectedTicks[i] = -(long) i - 1;
1070+
value = VariantEncoder.timestamp(expectedTicks[i], nanoseconds, true);
1071+
break;
1072+
case 2:
1073+
value = VariantEncoder.timestamp(i, !nanoseconds, false);
1074+
break;
1075+
case 3:
1076+
value = VariantEncoder.timestamp(i, !nanoseconds, true);
1077+
break;
1078+
case 4:
1079+
rawRows.add(null);
1080+
variantRows.add(null);
1081+
continue;
1082+
case 5:
1083+
value = VariantEncoder.nullValue();
1084+
break;
1085+
case 6:
1086+
value = new ArrayList<>(VariantEncoder.timestamp(i, nanoseconds, true));
1087+
value.remove(value.size() - 1);
1088+
break;
1089+
default:
1090+
value = VariantEncoder.int64(i);
1091+
break;
1092+
}
1093+
rawRows.add(value);
1094+
variantRows.add(variant(encoder.metadata(), encoder.object(fields(field("time", value)))));
1095+
}
1096+
@SuppressWarnings("unchecked")
1097+
List<Byte>[] encodedRows = (List<Byte>[]) rawRows.toArray(new List<?>[0]);
1098+
try (ColumnVector raw = ColumnVector.fromLists(BINARY_TYPE, encodedRows);
1099+
ColumnVector structs = ColumnVector.fromStructs(
1100+
VARIANT_TYPE, variantRows.toArray(new StructData[0]));
1101+
CloseableArray<ColumnView> rawViews = CloseableArray.wrap(raw.splitAsViews(3));
1102+
CloseableArray<ColumnView> structViews = CloseableArray.wrap(structs.splitAsViews(3));
1103+
ColumnVector cast = VariantUtils.castVariantValue(rawViews.get(1), target);
1104+
ColumnVector extracted =
1105+
VariantUtils.extractVariantField(structViews.get(1), "time", target);
1106+
ColumnVector ticks = ColumnVector.fromBoxedLongs(expectedTicks);
1107+
ColumnVector expected = ticks.castTo(target)) {
1108+
assertColumnsAreEqual(expected, cast);
1109+
assertColumnsAreEqual(expected, extracted);
1110+
}
1111+
}
1112+
}
1113+
1114+
@Test
1115+
void timestampTargetsSupportEmptyAndAllNullInputs() {
1116+
for (DType target : new DType[] {
1117+
DType.TIMESTAMP_MICROSECONDS, DType.TIMESTAMP_NANOSECONDS}) {
1118+
try (ColumnVector emptyValues = ColumnVector.fromLists(BINARY_TYPE);
1119+
ColumnVector emptyVariant = ColumnVector.fromStructs(VARIANT_TYPE);
1120+
ColumnVector emptyCast = VariantUtils.castVariantValue(emptyValues, target);
1121+
ColumnVector emptyExtract =
1122+
VariantUtils.extractVariantField(emptyVariant, "time", target);
1123+
ColumnVector nullValues = ColumnVector.fromLists(BINARY_TYPE, null, null);
1124+
ColumnVector nullVariant = ColumnVector.fromStructs(VARIANT_TYPE, null, null);
1125+
ColumnVector nullCast = VariantUtils.castVariantValue(nullValues, target);
1126+
ColumnVector nullExtract = VariantUtils.extractVariantField(nullVariant, "time", target);
1127+
ColumnVector nullTicks = ColumnVector.fromBoxedLongs(null, null);
1128+
ColumnVector expectedNulls = nullTicks.castTo(target)) {
1129+
assertEquals(target, emptyCast.getType());
1130+
assertEquals(target, emptyExtract.getType());
1131+
assertEquals(0, emptyCast.getRowCount());
1132+
assertEquals(0, emptyExtract.getRowCount());
1133+
assertColumnsAreEqual(expectedNulls, nullCast);
1134+
assertColumnsAreEqual(expectedNulls, nullExtract);
1135+
}
1136+
}
1137+
}
1138+
1139+
@Test
1140+
void timestampTargetsRejectUnsupportedUnits() {
1141+
try (ColumnVector values = ColumnVector.fromLists(
1142+
BINARY_TYPE, VariantEncoder.timestamp(7, false, false));
1143+
ColumnVector variant = makeXyzVariantColumn();
1144+
ColumnVector emptyValues = ColumnVector.fromLists(BINARY_TYPE);
1145+
ColumnVector emptyVariant = ColumnVector.fromStructs(VARIANT_TYPE)) {
1146+
for (DType target : new DType[] {
1147+
DType.TIMESTAMP_SECONDS, DType.TIMESTAMP_MILLISECONDS, DType.TIMESTAMP_DAYS}) {
1148+
assertThrows(IllegalArgumentException.class,
1149+
() -> VariantUtils.castVariantValue(values, target));
1150+
assertThrows(IllegalArgumentException.class,
1151+
() -> VariantUtils.extractVariantField(variant, "x", target));
1152+
assertThrows(IllegalArgumentException.class,
1153+
() -> VariantUtils.castVariantValue(emptyValues, target));
1154+
assertThrows(IllegalArgumentException.class,
1155+
() -> VariantUtils.extractVariantField(emptyVariant, "x", target));
1156+
}
1157+
}
1158+
}
1159+
9191160
@Test
9201161
void variantLogicalTypeIdsMatchNativeValues() {
9211162
VariantLogicalType[] types = {

0 commit comments

Comments
 (0)