@@ -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