From 653f20ccfb201a452d76cc872956357439c31360 Mon Sep 17 00:00:00 2001 From: Xiang Fu Date: Thu, 6 Aug 2026 23:09:03 -0700 Subject: [PATCH 1/2] [UUID 4a] CAST to and from the logical UUID type Adds CAST support for the logical UUID type, in both directions and for single- and multi-value columns: - CAST( AS UUID) parses the value into the 16-byte stored form - a UUID result read as a string renders the canonical dashed form rather than the hex a BYTES column would produce The conversions live in ArrayCopyUtils as copyToUuid / copyFromUuid, next to copyFromTimestamp which they mirror -- a logical type parsed into, or rendered from, its stored representation. Note the two directions are distinct and both are needed: #19165 handles a UUID *source* cast to STRING, while this handles rendering a UUID *result*, e.g. reading CAST(x AS UUID) as a string. Split out of #18872 to keep that PR to predicate evaluation. Depends only on UuidUtils (#18869) and the UUID stored type (#18870), both already on master. --- .../function/BaseTransformFunction.java | 4 + .../function/CastTransformFunction.java | 80 +++++++++++++++++++ .../function/BaseTransformFunctionTest.java | 16 +++- .../function/CastTransformFunctionTest.java | 80 +++++++++++++++++++ .../pinot/spi/utils/ArrayCopyUtils.java | 60 ++++++++++++++ 5 files changed, 239 insertions(+), 1 deletion(-) diff --git a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/BaseTransformFunction.java b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/BaseTransformFunction.java index 9f01c30492ee..b05599e54712 100644 --- a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/BaseTransformFunction.java +++ b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/BaseTransformFunction.java @@ -55,6 +55,8 @@ public abstract class BaseTransformFunction implements TransformFunction { new TransformResultMetadata(DataType.JSON, true, false); protected static final TransformResultMetadata BYTES_SV_NO_DICTIONARY_METADATA = new TransformResultMetadata(DataType.BYTES, true, false); + protected static final TransformResultMetadata UUID_SV_NO_DICTIONARY_METADATA = + new TransformResultMetadata(DataType.UUID, true, false); protected static final TransformResultMetadata INT_MV_NO_DICTIONARY_METADATA = new TransformResultMetadata(DataType.INT, false, false); @@ -76,6 +78,8 @@ public abstract class BaseTransformFunction implements TransformFunction { new TransformResultMetadata(DataType.JSON, false, false); protected static final TransformResultMetadata BYTES_MV_NO_DICTIONARY_METADATA = new TransformResultMetadata(DataType.BYTES, false, false); + protected static final TransformResultMetadata UUID_MV_NO_DICTIONARY_METADATA = + new TransformResultMetadata(DataType.UUID, false, false); protected static final TransformResultMetadata UNKNOWN_METADATA = new TransformResultMetadata(DataType.UNKNOWN, true, false); diff --git a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CastTransformFunction.java b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CastTransformFunction.java index efef5b2b65c6..82edb956f061 100644 --- a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CastTransformFunction.java +++ b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CastTransformFunction.java @@ -18,6 +18,7 @@ */ package org.apache.pinot.core.operator.transform.function; +import com.google.common.base.Preconditions; import java.math.BigDecimal; import java.util.List; import java.util.Map; @@ -97,6 +98,14 @@ public void init(List arguments, Map c case "VARBINARY": _resultMetadata = sourceSV ? BYTES_SV_NO_DICTIONARY_METADATA : BYTES_MV_NO_DICTIONARY_METADATA; break; + case "UUID": + Preconditions.checkState(sourceSV, "Cannot cast from MV to UUID"); + _resultMetadata = UUID_SV_NO_DICTIONARY_METADATA; + break; + case "UUID_ARRAY": + Preconditions.checkState(!sourceSV, "Cannot cast from SV to UUID_ARRAY"); + _resultMetadata = UUID_MV_NO_DICTIONARY_METADATA; + break; case "INT_ARRAY": case "INTEGER_ARRAY": _resultMetadata = INT_MV_NO_DICTIONARY_METADATA; @@ -319,6 +328,11 @@ public String[] transformToStringValuesSV(ValueBlock valueBlock) { byte[][] bytesValues = transformToBytesValuesSV(valueBlock); ArrayCopyUtils.copy(bytesValues, _stringValuesSV, length); break; + // Renders a UUID *result* (e.g. CAST(x AS UUID) read as a string). The switch above handles the other + // direction, a UUID *source* cast to STRING. + case UUID: + ArrayCopyUtils.copyFromUuid(transformToBytesValuesSV(valueBlock), _stringValuesSV, length); + break; default: throw new IllegalStateException(String.format("Cannot cast from SV %s to STRING", resultDataType)); } @@ -326,6 +340,68 @@ public String[] transformToStringValuesSV(ValueBlock valueBlock) { return _stringValuesSV; } + @Override + public byte[][] transformToBytesValuesSV(ValueBlock valueBlock) { + switch (_resultMetadata.getDataType()) { + case BYTES: + return _transformFunction.transformToBytesValuesSV(valueBlock); + case UUID: + return transformToUuidValuesSV(valueBlock); + default: + return super.transformToBytesValuesSV(valueBlock); + } + } + + // TODO: Add it to the interface + private byte[][] transformToUuidValuesSV(ValueBlock valueBlock) { + int length = valueBlock.getNumDocs(); + initBytesValuesSV(length); + switch (_sourceDataType.getStoredType()) { + case STRING: + String[] stringValues = _transformFunction.transformToStringValuesSV(valueBlock); + ArrayCopyUtils.copyToUuid(stringValues, _bytesValuesSV, length); + break; + case BYTES: + byte[][] bytesValues = _transformFunction.transformToBytesValuesSV(valueBlock); + ArrayCopyUtils.copyToUuid(bytesValues, _bytesValuesSV, length); + break; + default: + throw new IllegalStateException(String.format("Cannot cast from SV %s to UUID", _sourceDataType)); + } + return _bytesValuesSV; + } + + @Override + public byte[][][] transformToBytesValuesMV(ValueBlock valueBlock) { + switch (_resultMetadata.getDataType()) { + case BYTES: + return _transformFunction.transformToBytesValuesMV(valueBlock); + case UUID: + return transformToUuidValuesMV(valueBlock); + default: + return super.transformToBytesValuesMV(valueBlock); + } + } + + // TODO: Add it to the interface + private byte[][][] transformToUuidValuesMV(ValueBlock valueBlock) { + int length = valueBlock.getNumDocs(); + initBytesValuesMV(length); + switch (_sourceDataType.getStoredType()) { + case STRING: + String[][] stringValuesMV = _transformFunction.transformToStringValuesMV(valueBlock); + ArrayCopyUtils.copyToUuid(stringValuesMV, _bytesValuesMV, length); + break; + case BYTES: + byte[][][] bytesValuesMV = _transformFunction.transformToBytesValuesMV(valueBlock); + ArrayCopyUtils.copyToUuid(bytesValuesMV, _bytesValuesMV, length); + break; + default: + throw new IllegalStateException(String.format("Cannot cast from MV %s to UUID", _sourceDataType)); + } + return _bytesValuesMV; + } + @Override public int[][] transformToIntValuesMV(ValueBlock valueBlock) { switch (_resultMetadata.getDataType()) { @@ -491,6 +567,10 @@ public String[][] transformToStringValuesMV(ValueBlock valueBlock) { longValuesMV = transformToTimestampValuesMV(valueBlock); ArrayCopyUtils.copyFromTimestamp(longValuesMV, _stringValuesMV, length); break; + // See the SV variant: this renders a UUID result, not a UUID source. + case UUID: + ArrayCopyUtils.copyFromUuid(transformToBytesValuesMV(valueBlock), _stringValuesMV, length); + break; default: throw new IllegalStateException(String.format("Cannot cast from MV %s to STRING", resultDataType)); } diff --git a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/BaseTransformFunctionTest.java b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/BaseTransformFunctionTest.java index 4eb284986eab..ccc4804e0ac3 100644 --- a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/BaseTransformFunctionTest.java +++ b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/BaseTransformFunctionTest.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.ArrayUtils; @@ -59,6 +60,7 @@ import org.apache.pinot.spi.utils.BytesUtils; import org.apache.pinot.spi.utils.JsonUtils; import org.apache.pinot.spi.utils.ReadMode; +import org.apache.pinot.spi.utils.UuidUtils; import org.apache.pinot.spi.utils.builder.TableConfigBuilder; import org.roaringbitmap.RoaringBitmap; import org.testng.annotations.AfterClass; @@ -83,6 +85,7 @@ public abstract class BaseTransformFunctionTest { protected static final String JSON_STRING_SV_COLUMN = "jsonSV"; protected static final String STRING_SV_NULL_COLUMN = "stringSVNull"; protected static final String BYTES_SV_COLUMN = "bytesSV"; + protected static final String UUID_SV_COLUMN = "uuidSV"; protected static final String VECTOR_1_COLUMN = "vector1"; protected static final String VECTOR_2_COLUMN = "vector2"; protected static final String ZERO_VECTOR_COLUMN = "zeroVector"; @@ -132,6 +135,7 @@ public abstract class BaseTransformFunctionTest { protected final String[] _jsonArrayValues = new String[NUM_ROWS]; protected final String[] _stringAlphaNumericSVValues = new String[NUM_ROWS]; protected final byte[][] _bytesSVValues = new byte[NUM_ROWS][]; + protected final byte[][] _uuidSVValues = new byte[NUM_ROWS][]; protected final int[][] _intMVValues = new int[NUM_ROWS][]; protected final long[][] _longMVValues = new long[NUM_ROWS][]; protected final float[][] _floatMVValues = new float[NUM_ROWS][]; @@ -181,6 +185,9 @@ public void setUp() df.format(RANDOM.nextInt() * RANDOM.nextDouble())); _stringAlphaNumericSVValues[i] = RandomStringUtils.secure().nextAlphanumeric(26); _bytesSVValues[i] = RandomStringUtils.secure().nextAlphanumeric(26).getBytes(); + long mostSignificantBits = (i % 2 == 0) ? Long.MIN_VALUE + i : Long.MAX_VALUE - i; + long leastSignificantBits = ((long) i << Integer.SIZE) | i; + _uuidSVValues[i] = UuidUtils.toBytes(new UUID(mostSignificantBits, leastSignificantBits)); int numValues = 1 + RANDOM.nextInt(MAX_NUM_MULTI_VALUES); _intMVValues[i] = new int[numValues]; @@ -251,6 +258,7 @@ public void setUp() map.put(STRING_ALPHANUM_NULL_SV_COLUMN, _stringAlphaNumericSVValues[i]); } map.put(BYTES_SV_COLUMN, _bytesSVValues[i]); + map.put(UUID_SV_COLUMN, _uuidSVValues[i]); map.put(INT_MV_COLUMN, ArrayUtils.toObject(_intMVValues[i])); // Same values as INT_MV_COLUMN so callers can compare results against the dict-encoded baseline and @@ -304,6 +312,7 @@ public void setUp() .addSingleValueDimension(STRING_ALPHANUM_SV_COLUMN, FieldSpec.DataType.STRING) .addSingleValueDimension(STRING_ALPHANUM_NULL_SV_COLUMN, FieldSpec.DataType.STRING) .addSingleValueDimension(BYTES_SV_COLUMN, FieldSpec.DataType.BYTES) + .addSingleValueDimension(UUID_SV_COLUMN, FieldSpec.DataType.UUID) .addSingleValueDimension(JSON_COLUMN, FieldSpec.DataType.JSON) .addSingleValueDimension(DEFAULT_JSON_COLUMN, FieldSpec.DataType.JSON) .addMultiValueDimension(INT_MV_COLUMN, FieldSpec.DataType.INT) @@ -652,8 +661,13 @@ protected void testTransformFunctionWithNull(TransformFunction transformFunction protected void testTransformFunction(TransformFunction transformFunction, byte[][] expectedValues) { String[] stringValues = transformFunction.transformToStringValuesSV(_projectionBlock); byte[][] bytesValues = transformFunction.transformToBytesValuesSV(_projectionBlock); + FieldSpec.DataType resultDataType = transformFunction.getResultMetadata().getDataType(); for (int i = 0; i < NUM_ROWS; i++) { - assertEquals(bytesValues[i], BytesUtils.toBytes(stringValues[i])); + if (resultDataType == FieldSpec.DataType.UUID) { + assertEquals(bytesValues[i], UuidUtils.toBytes(stringValues[i])); + } else { + assertEquals(bytesValues[i], BytesUtils.toBytes(stringValues[i])); + } assertEquals(bytesValues[i], expectedValues[i]); } testNullBitmap(transformFunction, null); diff --git a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/CastTransformFunctionTest.java b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/CastTransformFunctionTest.java index db5fc9d0cfec..f6c0912dfd13 100644 --- a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/CastTransformFunctionTest.java +++ b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/CastTransformFunctionTest.java @@ -28,7 +28,9 @@ import org.apache.pinot.common.request.context.RequestContextUtils; import org.apache.pinot.core.operator.transform.TransformResultMetadata; import org.apache.pinot.spi.data.FieldSpec; +import org.apache.pinot.spi.exception.BadQueryRequestException; import org.apache.pinot.spi.utils.ArrayCopyUtils; +import org.apache.pinot.spi.utils.BytesUtils; import org.apache.pinot.spi.utils.CommonConstants.NullValuePlaceHolder; import org.apache.pinot.spi.utils.UuidUtils; import org.roaringbitmap.RoaringBitmap; @@ -43,6 +45,7 @@ public class CastTransformFunctionTest extends BaseTransformFunctionTest { + private static final String UUID_VALUE = "550e8400-e29b-41d4-a716-446655440000"; @Test public void testCastUuidToString() { String uuid = "550e8400-e29b-41d4-a716-446655440000"; @@ -361,4 +364,81 @@ public void testCastNullColumn() { } testTransformFunctionWithNull(transformFunction, expectedValues, roaringBitmap); } + + @Test + public void testCastTransformFunctionUUID() { + ExpressionContext expression = + RequestContextUtils.getExpression(String.format("CAST('%s' AS UUID)", UUID_VALUE.toUpperCase())); + TransformFunction transformFunction = TransformFunctionFactory.get(expression, _dataSourceMap); + assertTrue(transformFunction instanceof CastTransformFunction); + assertEquals(transformFunction.getResultMetadata().getDataType(), FieldSpec.DataType.UUID); + + byte[][] bytesValues = transformFunction.transformToBytesValuesSV(_projectionBlock); + String[] stringValues = transformFunction.transformToStringValuesSV(_projectionBlock); + byte[] expectedBytes = UuidUtils.toBytes(UUID_VALUE); + for (int i = 0; i < NUM_ROWS; i++) { + assertEquals(bytesValues[i], expectedBytes); + assertEquals(stringValues[i], UUID_VALUE); + } + } + + @Test + public void testCastTransformFunctionUuidRoundTrips() { + String bytesHex = BytesUtils.toHexString(UuidUtils.toBytes(UUID_VALUE)); + + ExpressionContext expression = RequestContextUtils.getExpression( + String.format("CAST(CAST('%s' AS BYTES) AS UUID)", bytesHex)); + TransformFunction transformFunction = TransformFunctionFactory.get(expression, _dataSourceMap); + byte[] expectedBytes = UuidUtils.toBytes(UUID_VALUE); + byte[][] expectedUuidValues = new byte[NUM_ROWS][]; + for (int i = 0; i < NUM_ROWS; i++) { + expectedUuidValues[i] = expectedBytes; + } + testTransformFunction(transformFunction, expectedUuidValues); + + expression = RequestContextUtils.getExpression(String.format("CAST(CAST('%s' AS UUID) AS STRING)", UUID_VALUE)); + transformFunction = TransformFunctionFactory.get(expression, _dataSourceMap); + String[] expectedStringValues = new String[NUM_ROWS]; + Arrays.fill(expectedStringValues, UUID_VALUE); + testTransformFunction(transformFunction, expectedStringValues); + + expression = RequestContextUtils.getExpression(String.format("CAST(CAST('%s' AS UUID) AS BYTES)", UUID_VALUE)); + transformFunction = TransformFunctionFactory.get(expression, _dataSourceMap); + byte[][] expectedBytesValues = new byte[NUM_ROWS][]; + for (int i = 0; i < NUM_ROWS; i++) { + expectedBytesValues[i] = expectedBytes; + } + testTransformFunction(transformFunction, expectedBytesValues); + } + + @Test + public void testCastTransformFunctionUUIDRejectsInvalidLiteral() { + ExpressionContext expression = RequestContextUtils.getExpression("CAST('not-a-uuid' AS UUID)"); + TransformFunction transformFunction = TransformFunctionFactory.get(expression, _dataSourceMap); + assertTrue(transformFunction instanceof CastTransformFunction); + + IllegalArgumentException exception = Assert.expectThrows(IllegalArgumentException.class, + () -> transformFunction.transformToBytesValuesSV(_projectionBlock)); + assertTrue(exception.getMessage().contains("Invalid UUID")); + } + + @Test + public void testCastTransformFunctionUUIDRejectsInvalidBytesLiteral() { + ExpressionContext expression = RequestContextUtils.getExpression("CAST(CAST('0011' AS BYTES) AS UUID)"); + TransformFunction transformFunction = TransformFunctionFactory.get(expression, _dataSourceMap); + assertTrue(transformFunction instanceof CastTransformFunction); + + IllegalArgumentException exception = Assert.expectThrows(IllegalArgumentException.class, + () -> transformFunction.transformToBytesValuesSV(_projectionBlock)); + assertTrue(exception.getMessage().contains("Invalid UUID byte length")); + } + + @Test + public void testCastTransformFunctionUUIDRejectsMVSource() { + ExpressionContext expression = + RequestContextUtils.getExpression(String.format("CAST(%s AS UUID)", STRING_MV_COLUMN)); + BadQueryRequestException exception = Assert.expectThrows(BadQueryRequestException.class, + () -> TransformFunctionFactory.get(expression, _dataSourceMap)); + assertTrue(exception.getMessage().contains("Cannot cast from MV to UUID")); + } } diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/ArrayCopyUtils.java b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/ArrayCopyUtils.java index f72808eb3e76..5cefa54a0059 100644 --- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/ArrayCopyUtils.java +++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/ArrayCopyUtils.java @@ -276,6 +276,14 @@ public static void copy(String[] src, byte[][] dest, int length) { } } + /// Parses canonical UUID strings into the 16-byte stored form. Inverse of + /// [#copyFromUuid(byte[][], String[], int)]. + public static void copyToUuid(String[] src, byte[][] dest, int length) { + for (int i = 0; i < length; i++) { + dest[i] = UuidUtils.toBytes(src[i]); + } + } + public static void copy(String[][] src, byte[][][] dest, int length) { for (int i = 0; i < length; i++) { String[] stringValues = src[i]; @@ -286,6 +294,42 @@ public static void copy(String[][] src, byte[][][] dest, int length) { } } + /// Multi-value variant of [#copyToUuid(String[], byte[][], int)]. + public static void copyToUuid(String[][] src, byte[][][] dest, int length) { + for (int i = 0; i < length; i++) { + int rowLength = src[i].length; + byte[][] row = new byte[rowLength][]; + for (int j = 0; j < rowLength; j++) { + row[j] = UuidUtils.toBytes(src[i][j]); + } + dest[i] = row; + } + } + + /// Multi-value variant of [#copyFromUuid(byte[][], String[], int)]. + public static void copyFromUuid(byte[][][] src, String[][] dest, int length) { + for (int i = 0; i < length; i++) { + int rowLength = src[i].length; + String[] row = new String[rowLength]; + for (int j = 0; j < rowLength; j++) { + row[j] = UuidUtils.toString(src[i][j]); + } + dest[i] = row; + } + } + + /// Multi-value variant of [#copyToUuid(byte[][], byte[][], int)]. + public static void copyToUuid(byte[][][] src, byte[][][] dest, int length) { + for (int i = 0; i < length; i++) { + int rowLength = src[i].length; + byte[][] row = new byte[rowLength][]; + for (int j = 0; j < rowLength; j++) { + row[j] = UuidUtils.toBytes(src[i][j]); + } + dest[i] = row; + } + } + public static void copy(byte[][] src, BigDecimal[] dest, int length) { for (int i = 0; i < length; i++) { dest[i] = BigDecimalUtils.deserialize(src[i]); @@ -298,6 +342,22 @@ public static void copy(byte[][] src, String[] dest, int length) { } } + /// Renders UUID values, stored as 16 raw bytes, into their canonical dashed form. Mirrors + /// [#copyFromTimestamp(long[], String[], int)], which likewise renders a logical type from its stored form. + public static void copyFromUuid(byte[][] src, String[] dest, int length) { + for (int i = 0; i < length; i++) { + dest[i] = UuidUtils.toString(src[i]); + } + } + + /// Normalises UUID values already in byte form, validating the 16-byte width. Counterpart of + /// [#copyToUuid(String[], byte[][], int)] for a BYTES source. + public static void copyToUuid(byte[][] src, byte[][] dest, int length) { + for (int i = 0; i < length; i++) { + dest[i] = UuidUtils.toBytes(src[i]); + } + } + public static void copy(int[][] src, long[][] dest, int length) { for (int i = 0; i < length; i++) { int rowLength = src[i].length; From 1ab6eaf2440784bf43cd2b87db8a724452126f4a Mon Sep 17 00:00:00 2001 From: Xiang Fu Date: Fri, 7 Aug 2026 12:38:16 -0700 Subject: [PATCH 2/2] Address UUID cast review comments --- .../function/CastTransformFunction.java | 17 ++++--- .../function/CastTransformFunctionTest.java | 16 +++++++ .../pinot/spi/utils/ArrayCopyUtils.java | 48 +++++++++---------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CastTransformFunction.java b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CastTransformFunction.java index 82edb956f061..16d4533c51a7 100644 --- a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CastTransformFunction.java +++ b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/CastTransformFunction.java @@ -102,10 +102,6 @@ public void init(List arguments, Map c Preconditions.checkState(sourceSV, "Cannot cast from MV to UUID"); _resultMetadata = UUID_SV_NO_DICTIONARY_METADATA; break; - case "UUID_ARRAY": - Preconditions.checkState(!sourceSV, "Cannot cast from SV to UUID_ARRAY"); - _resultMetadata = UUID_MV_NO_DICTIONARY_METADATA; - break; case "INT_ARRAY": case "INTEGER_ARRAY": _resultMetadata = INT_MV_NO_DICTIONARY_METADATA; @@ -128,6 +124,13 @@ public void init(List arguments, Map c case "VARCHAR_ARRAY": _resultMetadata = STRING_MV_NO_DICTIONARY_METADATA; break; + case "BYTES_ARRAY": + _resultMetadata = BYTES_MV_NO_DICTIONARY_METADATA; + break; + case "UUID_ARRAY": + Preconditions.checkState(!sourceSV, "Cannot cast from SV to UUID_ARRAY"); + _resultMetadata = UUID_MV_NO_DICTIONARY_METADATA; + break; default: throw new IllegalArgumentException("Unable to cast expression to type - " + targetType); } @@ -331,7 +334,8 @@ public String[] transformToStringValuesSV(ValueBlock valueBlock) { // Renders a UUID *result* (e.g. CAST(x AS UUID) read as a string). The switch above handles the other // direction, a UUID *source* cast to STRING. case UUID: - ArrayCopyUtils.copyFromUuid(transformToBytesValuesSV(valueBlock), _stringValuesSV, length); + byte[][] uuidValues = transformToBytesValuesSV(valueBlock); + ArrayCopyUtils.copyFromUuid(uuidValues, _stringValuesSV, length); break; default: throw new IllegalStateException(String.format("Cannot cast from SV %s to STRING", resultDataType)); @@ -569,7 +573,8 @@ public String[][] transformToStringValuesMV(ValueBlock valueBlock) { break; // See the SV variant: this renders a UUID result, not a UUID source. case UUID: - ArrayCopyUtils.copyFromUuid(transformToBytesValuesMV(valueBlock), _stringValuesMV, length); + byte[][][] uuidValuesMV = transformToBytesValuesMV(valueBlock); + ArrayCopyUtils.copyFromUuid(uuidValuesMV, _stringValuesMV, length); break; default: throw new IllegalStateException(String.format("Cannot cast from MV %s to STRING", resultDataType)); diff --git a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/CastTransformFunctionTest.java b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/CastTransformFunctionTest.java index f6c0912dfd13..7b5cba7e8b4b 100644 --- a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/CastTransformFunctionTest.java +++ b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/CastTransformFunctionTest.java @@ -41,6 +41,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; @@ -334,6 +335,21 @@ public void testCastTransformFunctionMV() { testTransformFunction(transformFunction, expectedArraySums); } + @Test + public void testCastTransformFunctionBytesArray() { + ExpressionContext expression = + RequestContextUtils.getExpression(String.format("CAST(%s AS BYTES_ARRAY)", STRING_MV_COLUMN)); + TransformFunction transformFunction = TransformFunctionFactory.get(expression, _dataSourceMap); + assertTrue(transformFunction instanceof CastTransformFunction); + TransformResultMetadata resultMetadata = transformFunction.getResultMetadata(); + assertEquals(resultMetadata.getDataType(), FieldSpec.DataType.BYTES); + assertFalse(resultMetadata.isSingleValue()); + + byte[][][] expectedBytesValues = new byte[NUM_ROWS][][]; + ArrayCopyUtils.copy(_stringMVValues, expectedBytesValues, NUM_ROWS); + assertEquals(transformFunction.transformToBytesValuesMV(_projectionBlock), expectedBytesValues); + } + @Test public void testCastNullLiteral() { ExpressionContext expression = RequestContextUtils.getExpression("cast(null AS INT)"); diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/ArrayCopyUtils.java b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/ArrayCopyUtils.java index 5cefa54a0059..c6acf032f3c8 100644 --- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/ArrayCopyUtils.java +++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/ArrayCopyUtils.java @@ -306,30 +306,6 @@ public static void copyToUuid(String[][] src, byte[][][] dest, int length) { } } - /// Multi-value variant of [#copyFromUuid(byte[][], String[], int)]. - public static void copyFromUuid(byte[][][] src, String[][] dest, int length) { - for (int i = 0; i < length; i++) { - int rowLength = src[i].length; - String[] row = new String[rowLength]; - for (int j = 0; j < rowLength; j++) { - row[j] = UuidUtils.toString(src[i][j]); - } - dest[i] = row; - } - } - - /// Multi-value variant of [#copyToUuid(byte[][], byte[][], int)]. - public static void copyToUuid(byte[][][] src, byte[][][] dest, int length) { - for (int i = 0; i < length; i++) { - int rowLength = src[i].length; - byte[][] row = new byte[rowLength][]; - for (int j = 0; j < rowLength; j++) { - row[j] = UuidUtils.toBytes(src[i][j]); - } - dest[i] = row; - } - } - public static void copy(byte[][] src, BigDecimal[] dest, int length) { for (int i = 0; i < length; i++) { dest[i] = BigDecimalUtils.deserialize(src[i]); @@ -358,6 +334,30 @@ public static void copyToUuid(byte[][] src, byte[][] dest, int length) { } } + /// Multi-value variant of [#copyFromUuid(byte[][], String[], int)]. + public static void copyFromUuid(byte[][][] src, String[][] dest, int length) { + for (int i = 0; i < length; i++) { + int rowLength = src[i].length; + String[] row = new String[rowLength]; + for (int j = 0; j < rowLength; j++) { + row[j] = UuidUtils.toString(src[i][j]); + } + dest[i] = row; + } + } + + /// Multi-value variant of [#copyToUuid(byte[][], byte[][], int)]. + public static void copyToUuid(byte[][][] src, byte[][][] dest, int length) { + for (int i = 0; i < length; i++) { + int rowLength = src[i].length; + byte[][] row = new byte[rowLength][]; + for (int j = 0; j < rowLength; j++) { + row[j] = UuidUtils.toBytes(src[i][j]); + } + dest[i] = row; + } + } + public static void copy(int[][] src, long[][] dest, int length) { for (int i = 0; i < length; i++) { int rowLength = src[i].length;