This is an automated email from the ASF dual-hosted git repository.
xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 2050fce9bd0 [UUID 3/8] UUID result rendering (DataSchema, Arrow/JSON
encoders) (#18871)
2050fce9bd0 is described below
commit 2050fce9bd0dd2c4b2e26df83ecf89dfb3f7f844
Author: Xiang Fu <[email protected]>
AuthorDate: Sun Jul 26 13:28:59 2026 -0700
[UUID 3/8] UUID result rendering (DataSchema, Arrow/JSON encoders) (#18871)
Part 3/8 of splitting apache/pinot#18140 (logical UUID type). Rebased onto
master, which now includes the merged #18869 type foundation and #18870
ingest/storage layer.
Renders UUID result columns as canonical lowercase RFC-4122 strings:
- DataSchema.ColumnDataType.UUID / UUID_ARRAY plus the conversion and format
helpers, backed by the UuidUtils class merged in #18869.
- Arrow and JSON broker response encoders.
UUID null placeholder: ColumnDataType.UUID overrides getNullPlaceholder() to
return the nil UUID, matching the default null sentinel FieldSpec already
uses
for UUID columns. Its stored type BYTES supplies a zero-length placeholder,
which is not a valid 16-byte UUID and fails to render, so the three call
sites
that build null-aware blocks (DataBlockBuilder, GroupByResultsBlock,
GroupByDataTableReducer) now resolve the placeholder on the logical type
rather
than the stored type. For every other logical type the two are identical, an
invariant pinned by a new DataSchemaTest case.
DataSchema.fromBytes now reports an unrecognized ColumnDataType token with a
message naming the mixed-version cause instead of a bare
IllegalArgumentException.
Rolling-upgrade note: DataSchema serializes ColumnDataType by name, so an
old
broker/server that receives a UUID token from a new node cannot parse it.
Treat
as an atomic-upgrade feature - UUID columns should only be queried once the
whole cluster is upgraded. No existing (non-UUID) column is affected.
---
.../response/encoder/ArrowResponseEncoder.java | 6 +
.../response/encoder/JsonResponseEncoder.java | 2 +
.../org/apache/pinot/common/utils/DataSchema.java | 172 ++++++++++++++++++++-
.../response/encoder/ArrowResponseEncoderTest.java | 79 +++++++++-
.../response/encoder/JsonResponseEncoderTest.java | 52 ++++++-
.../apache/pinot/common/utils/DataSchemaTest.java | 88 ++++++++++-
.../core/common/datablock/DataBlockBuilder.java | 40 +++--
.../blocks/results/GroupByResultsBlock.java | 4 +-
.../core/query/reduce/GroupByDataTableReducer.java | 4 +-
.../common/datablock/DataBlockBuilderTest.java | 23 +++
.../core/common/datablock/DataBlockTestUtils.java | 13 ++
.../core/common/datatable/DataTableSerDeTest.java | 26 ++++
.../selection/SelectionOperatorUtilsTest.java | 23 +++
13 files changed, 497 insertions(+), 35 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/response/encoder/ArrowResponseEncoder.java
b/pinot-common/src/main/java/org/apache/pinot/common/response/encoder/ArrowResponseEncoder.java
index f96027398c5..efd9098991e 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/response/encoder/ArrowResponseEncoder.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/response/encoder/ArrowResponseEncoder.java
@@ -107,6 +107,7 @@ public class ArrowResponseEncoder implements
ResponseEncoder {
case STRING:
case JSON:
case BYTES:
+ case UUID:
case OBJECT:
field = new Field(colName, FieldType.nullable(new ArrowType.Utf8()),
null);
vector = new VarCharVector(colName, ALLOCATOR);
@@ -181,6 +182,7 @@ public class ArrowResponseEncoder implements
ResponseEncoder {
case TIMESTAMP_ARRAY:
case STRING_ARRAY:
case BYTES_ARRAY:
+ case UUID_ARRAY:
// Define the inner field for a string element.
children = List.of(new Field("element", FieldType.nullable(new
ArrowType.Utf8()), null));
// Define the field for the list column.
@@ -234,6 +236,7 @@ public class ArrowResponseEncoder implements
ResponseEncoder {
case STRING:
case JSON:
case BYTES:
+ case UUID:
case OBJECT:
byte[] bytes = ((String) value).getBytes(StandardCharsets.UTF_8);
((VarCharVector) vector).setSafe(rowIndex, bytes);
@@ -345,6 +348,7 @@ public class ArrowResponseEncoder implements
ResponseEncoder {
case TIMESTAMP_ARRAY:
case STRING_ARRAY:
case BYTES_ARRAY:
+ case UUID_ARRAY:
ListVector listVector = (ListVector) vector;
String[] stringArray = (String[]) value;
// Start a new list entry for the current row.
@@ -411,6 +415,7 @@ public class ArrowResponseEncoder implements
ResponseEncoder {
case STRING:
case JSON:
case BYTES:
+ case UUID:
case OBJECT:
row[col] = new String(((VarCharVector) vector).get(i),
StandardCharsets.UTF_8);
break;
@@ -469,6 +474,7 @@ public class ArrowResponseEncoder implements
ResponseEncoder {
case TIMESTAMP_ARRAY:
case STRING_ARRAY:
case BYTES_ARRAY:
+ case UUID_ARRAY:
ListVector listVector = (ListVector) vector;
List<?> arrayValues = listVector.getObject(i);
String[] array = new String[arrayValues.size()];
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/response/encoder/JsonResponseEncoder.java
b/pinot-common/src/main/java/org/apache/pinot/common/response/encoder/JsonResponseEncoder.java
index 0f82daab9e9..aa86400de85 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/response/encoder/JsonResponseEncoder.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/response/encoder/JsonResponseEncoder.java
@@ -194,6 +194,7 @@ public class JsonResponseEncoder implements ResponseEncoder
{
case TIMESTAMP_ARRAY:
case STRING_ARRAY:
case BYTES_ARRAY:
+ case UUID_ARRAY:
String[] stringArray = new String[jsonValue.size()];
for (int k = 0; k < jsonValue.size(); k++) {
stringArray[k] = jsonValue.get(k).textValue();
@@ -224,6 +225,7 @@ public class JsonResponseEncoder implements ResponseEncoder
{
case STRING:
case JSON:
case BYTES:
+ case UUID:
case OBJECT:
return jsonValue.textValue();
case UNKNOWN:
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/DataSchema.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/DataSchema.java
index 5149195d68c..978b33aed80 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/utils/DataSchema.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/utils/DataSchema.java
@@ -40,6 +40,7 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.sql.type.SqlTypeName;
@@ -51,6 +52,7 @@ import org.apache.pinot.spi.utils.BytesUtils;
import org.apache.pinot.spi.utils.CommonConstants.NullValuePlaceHolder;
import org.apache.pinot.spi.utils.EqualityUtils;
import org.apache.pinot.spi.utils.PinotDataType;
+import org.apache.pinot.spi.utils.UuidUtils;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -135,7 +137,7 @@ public class DataSchema {
// Write the column types.
for (ColumnDataType columnDataType : _columnDataTypes) {
// We don't want to use ordinal of the enum since adding a new data type
will break things if server and broker
- // use different versions of DataType class.
+ // use different versions of DataType class. See parseColumnDataType()
for the mixed-version read side.
byte[] bytes = columnDataType.name().getBytes(UTF_8);
dataOutputStream.writeInt(bytes.length);
dataOutputStream.write(bytes);
@@ -164,7 +166,7 @@ public class DataSchema {
int length = buffer.getInt();
byte[] bytes = new byte[length];
buffer.get(bytes);
- columnDataTypes[i] = ColumnDataType.valueOf(new String(bytes, UTF_8));
+ columnDataTypes[i] = parseColumnDataType(new String(bytes, UTF_8));
}
return new DataSchema(columnNames, columnDataTypes);
}
@@ -187,11 +189,31 @@ public class DataSchema {
int length = buffer.readInt();
byte[] bytes = new byte[length];
buffer.readFully(bytes);
- columnDataTypes[i] = ColumnDataType.valueOf(new String(bytes, UTF_8));
+ columnDataTypes[i] = parseColumnDataType(new String(bytes, UTF_8));
}
return new DataSchema(columnNames, columnDataTypes);
}
+ /// Resolves a [ColumnDataType] token read off the wire, turning the raw
[IllegalArgumentException] from
+ /// [ColumnDataType#valueOf] into a message that names the mixed-version
cause.
+ ///
+ /// Rolling-upgrade limitation: once a node on this build emits a `UUID`
token, an older peer that does not know
+ /// the [ColumnDataType#UUID] constant fails here. There is no
version-negotiation shim or fallback to `BYTES`
+ /// today, so brokers and servers must be upgraded atomically (or UUID
columns kept out of queries) until the
+ /// whole cluster is on this build. Rolling back to a pre-UUID build is
likewise unsafe while UUID-typed query
+ /// results are in flight. No existing (non-UUID) column is affected.
+ private static ColumnDataType parseColumnDataType(String name) {
+ try {
+ return ColumnDataType.valueOf(name);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Unrecognized ColumnDataType '" + name + "' received from a peer
node. This typically means the peer is "
+ + "running a newer build that introduced a data type not yet
known to this node. Upgrade all brokers "
+ + "and servers to the same build before querying columns of that
type, or keep those columns out of "
+ + "queries until the rolling upgrade is complete.", e);
+ }
+ }
+
@SuppressWarnings("MethodDoesntCallSuperMethod")
@Override
public DataSchema clone() {
@@ -297,6 +319,25 @@ public class DataSchema {
return typeFactory.createSqlType(SqlTypeName.VARBINARY);
}
},
+ // UUID is a logical type backed by BYTES; keep it directly after BYTES.
ColumnDataType is serialized by name (not
+ // ordinal) via name()/valueOf(), so enum order does not affect wire
compatibility.
+ UUID(BYTES, null) {
+ @Override
+ public RelDataType toType(RelDataTypeFactory typeFactory) {
+ return typeFactory.createSqlType(SqlTypeName.UUID);
+ }
+
+ /// Returns the nil UUID, matching the default null sentinel that
[FieldSpec#getDefaultNullValue] uses for
+ /// UUID columns. This is the one type whose placeholder differs from
its stored type's: `BYTES` uses a shared
+ /// zero-length [ByteArray], which is not a valid 16-byte UUID and would
fail to render.
+ ///
+ /// A fresh instance is returned per call because, unlike every other
placeholder (all empty or immutable),
+ /// this one wraps a mutable 16-byte array that callers hand out as a
column fill value.
+ @Override
+ public Object getNullPlaceholder() {
+ return new ByteArray(UuidUtils.nullUuidBytes());
+ }
+ },
MAP(NullValuePlaceHolder.MAP) {
@Override
public RelDataType toType(RelDataTypeFactory typeFactory) {
@@ -363,6 +404,12 @@ public class DataSchema {
return typeFactory.createArrayType(BYTES.toType(typeFactory), -1);
}
},
+ UUID_ARRAY(BYTES_ARRAY, NullValuePlaceHolder.INTERNAL_BYTES_ARRAY) {
+ @Override
+ public RelDataType toType(RelDataTypeFactory typeFactory) {
+ return typeFactory.createArrayType(UUID.toType(typeFactory), -1);
+ }
+ },
UNKNOWN(null) {
@Override
public RelDataType toType(RelDataTypeFactory typeFactory) {
@@ -374,7 +421,7 @@ public class DataSchema {
private static final EnumSet<ColumnDataType> INTEGRAL_TYPES =
EnumSet.of(INT, LONG);
private static final EnumSet<ColumnDataType> ARRAY_TYPES =
EnumSet.of(INT_ARRAY, LONG_ARRAY, FLOAT_ARRAY, DOUBLE_ARRAY,
BIG_DECIMAL_ARRAY, BOOLEAN_ARRAY, TIMESTAMP_ARRAY,
- STRING_ARRAY, BYTES_ARRAY);
+ STRING_ARRAY, BYTES_ARRAY, UUID_ARRAY);
private static final EnumSet<ColumnDataType> NUMERIC_ARRAY_TYPES =
EnumSet.of(INT_ARRAY, LONG_ARRAY, FLOAT_ARRAY, DOUBLE_ARRAY,
BIG_DECIMAL_ARRAY);
private static final EnumSet<ColumnDataType> INTEGRAL_ARRAY_TYPES =
EnumSet.of(INT_ARRAY, LONG_ARRAY);
@@ -395,6 +442,10 @@ public class DataSchema {
_nullPlaceholder = nullPlaceHolder;
}
+ /// Returns the value used to fill null entries in the serialized column,
masked on read by the null bitmap.
+ ///
+ /// Callers must resolve this on the *logical* type, not on
[#getStoredType], because [#UUID] overrides it (see
+ /// that constant). For every other logical type the two agree, an
invariant pinned by `DataSchemaTest`.
public Object getNullPlaceholder() {
return _nullPlaceholder;
}
@@ -463,6 +514,9 @@ public class DataSchema {
case BYTES:
case BYTES_ARRAY:
return DataType.BYTES;
+ case UUID:
+ case UUID_ARRAY:
+ return DataType.UUID;
case UNKNOWN:
return DataType.UNKNOWN;
default:
@@ -490,6 +544,8 @@ public class DataSchema {
* <li>BYTES: byte[] -> ByteArray</li>
* <li>BOOLEAN_ARRAY: boolean[] -> int[]</li>
* <li>TIMESTAMP_ARRAY: Timestamp[] -> long[]</li>
+ * <li>UUID: UUID/String/byte[]/ByteArray -> ByteArray</li>
+ * <li>UUID_ARRAY: UUID[]/String[]/byte[][]/ByteArray[] ->
ByteArray[]</li>
* </ul>
*/
public Object toInternal(Object value) {
@@ -500,10 +556,14 @@ public class DataSchema {
return ((Timestamp) value).getTime();
case BYTES:
return new ByteArray((byte[]) value);
+ case UUID:
+ return new ByteArray(UuidUtils.toBytes(value));
case BOOLEAN_ARRAY:
return fromBooleanArray((boolean[]) value);
case TIMESTAMP_ARRAY:
return fromTimestampArray((Timestamp[]) value);
+ case UUID_ARRAY:
+ return fromUuidArray(value);
case OBJECT:
// For OBJECT type, we need to convert based on the actual type of
the value. This can happen when the scalar
// function returns Object type, e.g. cast function.
@@ -516,6 +576,9 @@ public class DataSchema {
if (value instanceof byte[]) {
return new ByteArray((byte[]) value);
}
+ if (value instanceof UUID) {
+ return new ByteArray(UuidUtils.toBytes((UUID) value));
+ }
if (value instanceof boolean[]) {
return fromBooleanArray((boolean[]) value);
}
@@ -539,6 +602,8 @@ public class DataSchema {
* <li>BOOLEAN_ARRAY: int[] -> boolean[]</li>
* <li>TIMESTAMP_ARRAY: long[] -> Timestamp[]</li>
* <li>BYTES_ARRAY: ByteArray[] -> byte[][]</li>
+ * <li>UUID: ByteArray -> UUID</li>
+ * <li>UUID_ARRAY: ByteArray[] -> UUID[]</li>
* </ul>
*/
public Object toExternal(Object value) {
@@ -549,12 +614,16 @@ public class DataSchema {
return new Timestamp((long) value);
case BYTES:
return ((ByteArray) value).getBytes();
+ case UUID:
+ return UuidUtils.toUUID((ByteArray) value);
case BOOLEAN_ARRAY:
return toBooleanArray((int[]) value);
case TIMESTAMP_ARRAY:
return toTimestampArray((long[]) value);
case BYTES_ARRAY:
return toBytesArray(value);
+ case UUID_ARRAY:
+ return toUuidArray(value);
default:
return value;
}
@@ -585,6 +654,8 @@ public class DataSchema {
return value.toString();
case BYTES:
return ((ByteArray) value).getBytes();
+ case UUID:
+ return UuidUtils.toUUID((ByteArray) value);
case INT_ARRAY:
return toIntArray(value);
case LONG_ARRAY:
@@ -603,6 +674,8 @@ public class DataSchema {
return toStringArray(value);
case BYTES_ARRAY:
return toBytesArray(value);
+ case UUID_ARRAY:
+ return toUuidArray(value);
case UNKNOWN: // fall through
case OBJECT:
return (Serializable) value;
@@ -624,12 +697,16 @@ public class DataSchema {
return value.toString();
case BYTES:
return BytesUtils.toHexString((byte[]) value);
+ case UUID:
+ return formatUuid(value);
case BIG_DECIMAL_ARRAY:
return formatBigDecimalArray((BigDecimal[]) value);
case TIMESTAMP_ARRAY:
return formatTimestampArray((Timestamp[]) value);
case BYTES_ARRAY:
return formatBytesArray((byte[][]) value);
+ case UUID_ARRAY:
+ return formatUuidArray(value);
default:
return (Serializable) value;
}
@@ -659,6 +736,8 @@ public class DataSchema {
return value.toString();
case BYTES:
return ((ByteArray) value).toHexString();
+ case UUID:
+ return UuidUtils.toString((ByteArray) value);
case MAP:
return toMap(value);
case INT_ARRAY:
@@ -679,6 +758,8 @@ public class DataSchema {
return (String[]) value;
case BYTES_ARRAY:
return formatBytesArray((ByteArray[]) value);
+ case UUID_ARRAY:
+ return formatUuidArray(value);
default:
throw new IllegalStateException(String.format("Cannot convert and
format: '%s' to type: %s", value, this));
}
@@ -869,6 +950,54 @@ public class DataSchema {
throw new IllegalStateException(String.format("Cannot convert: '%s' to
byte[][]", value));
}
+ /// Converts any supported UUID array representation to `UUID[]`. Elements
may be `UUID`, `byte[]`, [ByteArray]
+ /// or `CharSequence`; per-element dispatch is delegated to
[UuidUtils#toUUID(Object)]. Note that `byte[][]`,
+ /// [ByteArray]`[]`, `String[]` and `UUID[]` are all `Object[]`, so one
branch covers every array form.
+ private static UUID[] toUuidArray(Object value) {
+ if (value instanceof UUID[]) {
+ return (UUID[]) value;
+ }
+ if (value instanceof ObjectArrayList) {
+ ObjectArrayList<?> list = (ObjectArrayList<?>) value;
+ int size = list.size();
+ UUID[] uuidArray = new UUID[size];
+ for (int i = 0; i < size; i++) {
+ uuidArray[i] = UuidUtils.toUUID(list.get(i));
+ }
+ return uuidArray;
+ }
+ Object[] valueArray = (Object[]) value;
+ int length = valueArray.length;
+ UUID[] uuidArray = new UUID[length];
+ for (int i = 0; i < length; i++) {
+ uuidArray[i] = UuidUtils.toUUID(valueArray[i]);
+ }
+ return uuidArray;
+ }
+
+ /// Inverse of [#toUuidArray]: converts any supported UUID array
representation to the internal [ByteArray]`[]`.
+ private static ByteArray[] fromUuidArray(Object value) {
+ if (value instanceof ByteArray[]) {
+ return (ByteArray[]) value;
+ }
+ if (value instanceof ObjectArrayList) {
+ ObjectArrayList<?> list = (ObjectArrayList<?>) value;
+ int size = list.size();
+ ByteArray[] wrapped = new ByteArray[size];
+ for (int i = 0; i < size; i++) {
+ wrapped[i] = new ByteArray(UuidUtils.toBytes(list.get(i)));
+ }
+ return wrapped;
+ }
+ Object[] valueArray = (Object[]) value;
+ int length = valueArray.length;
+ ByteArray[] wrapped = new ByteArray[length];
+ for (int i = 0; i < length; i++) {
+ wrapped[i] = new ByteArray(UuidUtils.toBytes(valueArray[i]));
+ }
+ return wrapped;
+ }
+
private static String[] formatBytesArray(byte[][] bytesArray) {
int length = bytesArray.length;
String[] formattedBytesArray = new String[length];
@@ -887,6 +1016,28 @@ public class DataSchema {
return formattedBytesArray;
}
+ /// Renders any supported UUID array representation as canonical lowercase
RFC 4122 strings. Per-element
+ /// dispatch is delegated to [#formatUuid], so `byte[][]`,
[ByteArray]`[]`, `UUID[]` and `String[]` are all
+ /// handled by the single `Object[]` branch.
+ private static String[] formatUuidArray(Object value) {
+ if (value instanceof ObjectArrayList) {
+ ObjectArrayList<?> list = (ObjectArrayList<?>) value;
+ int size = list.size();
+ String[] formattedUuidArray = new String[size];
+ for (int i = 0; i < size; i++) {
+ formattedUuidArray[i] = formatUuid(list.get(i));
+ }
+ return formattedUuidArray;
+ }
+ Object[] valueArray = (Object[]) value;
+ int length = valueArray.length;
+ String[] formattedUuidArray = new String[length];
+ for (int i = 0; i < length; i++) {
+ formattedUuidArray[i] = formatUuid(valueArray[i]);
+ }
+ return formattedUuidArray;
+ }
+
public static ColumnDataType fromDataType(DataType dataType, boolean
isSingleValue) {
return isSingleValue ? fromDataTypeSV(dataType) :
fromDataTypeMV(dataType);
}
@@ -913,6 +1064,8 @@ public class DataSchema {
return JSON;
case BYTES:
return BYTES;
+ case UUID:
+ return UUID;
case MAP:
return MAP;
case OPEN_STRUCT:
@@ -944,6 +1097,8 @@ public class DataSchema {
return STRING_ARRAY;
case BYTES:
return BYTES_ARRAY;
+ case UUID:
+ return UUID_ARRAY;
default:
throw new IllegalStateException("Unsupported data type: " +
dataType);
}
@@ -973,6 +1128,8 @@ public class DataSchema {
return PinotDataType.JSON;
case BYTES:
return PinotDataType.BYTES;
+ case UUID:
+ return PinotDataType.UUID;
case MAP:
return PinotDataType.MAP;
case OBJECT:
@@ -1000,6 +1157,13 @@ public class DataSchema {
}
}
+ /// Renders a single UUID as its canonical lowercase RFC 4122 string.
Accepts every representation
+ /// [UuidUtils#toUUID(Object)] does: `UUID`, `byte[]`, [ByteArray] and
`CharSequence`. A non-canonical (e.g.
+ /// upper-case) string input is re-canonicalized rather than passed
through.
+ private static String formatUuid(Object value) {
+ return UuidUtils.toUUID(value).toString();
+ }
+
public abstract RelDataType toType(RelDataTypeFactory typeFactory);
}
}
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/response/encoder/ArrowResponseEncoderTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/response/encoder/ArrowResponseEncoderTest.java
index 0d65e3e9f0b..ea141a29398 100644
---
a/pinot-common/src/test/java/org/apache/pinot/common/response/encoder/ArrowResponseEncoderTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/common/response/encoder/ArrowResponseEncoderTest.java
@@ -23,15 +23,19 @@ import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import org.apache.pinot.common.response.broker.ResultTable;
import org.apache.pinot.common.utils.DataSchema;
import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
import static org.testng.Assert.fail;
@@ -114,6 +118,72 @@ public class ArrowResponseEncoderTest {
}
}
+ @Test
+ public void testEncodeDecodeUuidColumn()
+ throws IOException {
+ DataSchema schema = new DataSchema(new String[]{"uuidCol"}, new
ColumnDataType[]{ColumnDataType.UUID});
+ List<Object[]> rows = Arrays.asList(
+ new Object[]{"550e8400-e29b-41d4-a716-446655440000"},
+ new Object[]{"f81d4fae-7dec-11d0-a765-00a0c91e6bf6"}
+ );
+
+ ResultTable resultTable = new ResultTable(schema, rows);
+ ArrowResponseEncoder encoder = new ArrowResponseEncoder();
+ byte[] encodedBytes = encoder.encodeResultTable(resultTable, 0,
rows.size());
+ ResultTable decodedTable = encoder.decodeResultTable(encodedBytes,
rows.size(), schema);
+
+ assertEquals(decodedTable.getRows().size(), rows.size(), "Row count should
match");
+ for (int i = 0; i < rows.size(); i++) {
+ assertEquals(decodedTable.getRows().get(i)[0], rows.get(i)[0], "UUID row
" + i + " should match");
+ }
+ }
+
+ /// Mirrors the real broker path: a UUID column is rendered to its canonical
string *before* it reaches the
+ /// encoder -- via `convertAndFormat` in the single-stage engine, and
`format(toExternal(..))` in the multi-stage
+ /// engine (see QueryDispatcher#toExternalList). The encoder therefore only
ever sees Strings in this group.
+ @Test
+ public void testEncodeDecodeUuidColumnRenderedFromInternalValue()
+ throws IOException {
+ String uuidValue = "550e8400-e29b-41d4-a716-446655440000";
+ DataSchema schema = new DataSchema(new String[]{"uuidCol", "cnt"},
+ new ColumnDataType[]{ColumnDataType.UUID, ColumnDataType.LONG});
+ Object internalValue =
ColumnDataType.UUID.toInternal(UUID.fromString(uuidValue));
+ List<Object[]> rows =
+ Collections.singletonList(new
Object[]{ColumnDataType.UUID.convertAndFormat(internalValue), 3L});
+
+ ResultTable resultTable = new ResultTable(schema, rows);
+ ArrowResponseEncoder encoder = new ArrowResponseEncoder();
+ byte[] encodedBytes = encoder.encodeResultTable(resultTable, 0,
rows.size());
+ ResultTable decodedTable = encoder.decodeResultTable(encodedBytes,
rows.size(), schema);
+
+ assertEquals(decodedTable.getRows().size(), 1, "Row count should match");
+ assertEquals(decodedTable.getRows().get(0)[0], uuidValue, "UUID value
should round-trip as canonical string");
+ assertEquals(decodedTable.getRows().get(0)[1], 3L, "Non-UUID columns
should be preserved");
+ }
+
+ @Test
+ public void testEncodeDecodeUuidColumnWithNulls()
+ throws IOException {
+ String uuidValue = "550e8400-e29b-41d4-a716-446655440000";
+ DataSchema schema = new DataSchema(new String[]{"uuidCol", "uuidArrayCol"},
+ new ColumnDataType[]{ColumnDataType.UUID, ColumnDataType.UUID_ARRAY});
+ List<Object[]> rows = Arrays.asList(
+ new Object[]{uuidValue, new String[]{uuidValue}},
+ new Object[]{null, null}
+ );
+
+ ResultTable resultTable = new ResultTable(schema, rows);
+ ArrowResponseEncoder encoder = new ArrowResponseEncoder();
+ byte[] encodedBytes = encoder.encodeResultTable(resultTable, 0,
rows.size());
+ ResultTable decodedTable = encoder.decodeResultTable(encodedBytes,
rows.size(), schema);
+
+ assertEquals(decodedTable.getRows().size(), 2, "Row count should match");
+ assertEquals(decodedTable.getRows().get(0)[0], uuidValue, "Non-null UUID
should round-trip");
+ assertEquals(decodedTable.getRows().get(0)[1], new String[]{uuidValue},
"Non-null UUID array should round-trip");
+ assertNull(decodedTable.getRows().get(1)[0], "Null UUID should round-trip
as null");
+ assertNull(decodedTable.getRows().get(1)[1], "Null UUID array should
round-trip as null");
+ }
+
@Test
public void testEncodeDecodeAllDataTypes()
throws IOException {
@@ -122,7 +192,7 @@ public class ArrowResponseEncoderTest {
"intCol", "longCol", "floatCol", "doubleCol", "bigDecimalCol",
"booleanCol", "timestampCol",
"stringCol", "jsonCol", "mapCol", "bytesCol", "objectCol",
"intArrayCol", "longArrayCol",
"floatArrayCol", "doubleArrayCol", "booleanArrayCol",
"timestampArrayCol", "stringArrayCol",
- "bytesArrayCol", "unknownCol"
+ "bytesArrayCol", "uuidArrayCol", "unknownCol"
};
DataSchema.ColumnDataType[] columnTypes = {
@@ -146,6 +216,7 @@ public class ArrowResponseEncoderTest {
ColumnDataType.TIMESTAMP_ARRAY,
ColumnDataType.STRING_ARRAY,
ColumnDataType.BYTES_ARRAY,
+ ColumnDataType.UUID_ARRAY,
ColumnDataType.UNKNOWN
};
@@ -178,6 +249,10 @@ public class ArrowResponseEncoderTest {
byte[][] bytesArrayVal = new byte[][]{
new byte[]{1, 2}, new byte[]{3, 4}
};
+ byte[][] uuidArrayVal = new byte[][]{
+ UuidUtils.toBytes("550e8400-e29b-41d4-a716-446655440000"),
+ UuidUtils.toBytes("550e8400-e29b-41d4-a716-446655440001")
+ };
Object unknownVal = null; // UNKNOWN is represented as null in this
example.
// Build a single row that contains all the above values.
@@ -185,7 +260,7 @@ public class ArrowResponseEncoderTest {
Object[] row = new Object[]{
intVal, longVal, floatVal, doubleVal, bigDecimalVal, booleanVal,
timestampVal, stringVal,
jsonVal, mapVal, bytesVal, objectVal, intArrayVal, longArrayVal,
floatArrayVal, doubleArrayVal,
- booleanArrayVal, timestampArrayVal, stringArrayVal, bytesArrayVal,
unknownVal
+ booleanArrayVal, timestampArrayVal, stringArrayVal, bytesArrayVal,
uuidArrayVal, unknownVal
};
for (int i = 0; i < row.length; i++) {
row[i] = columnTypes[i].format(row[i]); // Convert to internal
representation.
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/response/encoder/JsonResponseEncoderTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/response/encoder/JsonResponseEncoderTest.java
index f23088fca01..4625d618be1 100644
---
a/pinot-common/src/test/java/org/apache/pinot/common/response/encoder/JsonResponseEncoderTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/common/response/encoder/JsonResponseEncoderTest.java
@@ -26,12 +26,15 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import org.apache.pinot.common.response.broker.ResultTable;
import org.apache.pinot.common.utils.DataSchema;
import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
import static org.testng.Assert.fail;
public class JsonResponseEncoderTest {
@@ -110,6 +113,46 @@ public class JsonResponseEncoderTest {
}
}
+ @Test
+ public void testEncodeDecodeUuidColumn() throws IOException {
+ DataSchema schema = new DataSchema(
+ new String[] {"uuidCol"},
+ new ColumnDataType[] {ColumnDataType.UUID});
+ String uuidValue = "550e8400-e29b-41d4-a716-446655440000";
+
+ List<Object[]> rows = new ArrayList<>();
+ rows.add(new Object[]
{ColumnDataType.UUID.format(UUID.fromString(uuidValue))});
+
+ ResultTable resultTable = new ResultTable(schema, rows);
+ JsonResponseEncoder encoder = new JsonResponseEncoder();
+
+ byte[] encodedBytes = encoder.encodeResultTable(resultTable, 0,
rows.size());
+ ResultTable decodedTable = encoder.decodeResultTable(encodedBytes,
rows.size(), schema);
+
+ assertEquals(decodedTable.getRows().size(), 1, "Row count should be 1");
+ assertEquals(decodedTable.getRows().get(0)[0], uuidValue, "UUID value
should round-trip as canonical string");
+ }
+
+ @Test
+ public void testEncodeDecodeUuidColumnWithNulls() throws IOException {
+ String uuidValue = "550e8400-e29b-41d4-a716-446655440000";
+ DataSchema schema = new DataSchema(new String[] {"uuidCol"}, new
ColumnDataType[] {ColumnDataType.UUID});
+
+ List<Object[]> rows = new ArrayList<>();
+ rows.add(new Object[] {uuidValue});
+ rows.add(new Object[] {null});
+
+ ResultTable resultTable = new ResultTable(schema, rows);
+ JsonResponseEncoder encoder = new JsonResponseEncoder();
+
+ byte[] encodedBytes = encoder.encodeResultTable(resultTable, 0,
rows.size());
+ ResultTable decodedTable = encoder.decodeResultTable(encodedBytes,
rows.size(), schema);
+
+ assertEquals(decodedTable.getRows().size(), 2, "Row count should be 2");
+ assertEquals(decodedTable.getRows().get(0)[0], uuidValue, "Non-null UUID
should round-trip");
+ assertNull(decodedTable.getRows().get(1)[0], "Null UUID should round-trip
as null");
+ }
+
@Test
public void testEncodeDecodeAllDataTypes() throws IOException {
// Define the column names and corresponding data types.
@@ -117,7 +160,7 @@ public class JsonResponseEncoderTest {
"intCol", "longCol", "floatCol", "doubleCol", "bigDecimalCol",
"booleanCol", "timestampCol",
"stringCol", "jsonCol", "mapCol", "bytesCol", "objectCol",
"intArrayCol", "longArrayCol",
"floatArrayCol", "doubleArrayCol", "booleanArrayCol",
"timestampArrayCol", "stringArrayCol",
- "bytesArrayCol", "unknownCol"
+ "bytesArrayCol", "uuidArrayCol", "unknownCol"
};
DataSchema.ColumnDataType[] columnTypes = {
@@ -141,6 +184,7 @@ public class JsonResponseEncoderTest {
ColumnDataType.TIMESTAMP_ARRAY,
ColumnDataType.STRING_ARRAY,
ColumnDataType.BYTES_ARRAY,
+ ColumnDataType.UUID_ARRAY,
ColumnDataType.UNKNOWN
};
@@ -173,6 +217,10 @@ public class JsonResponseEncoderTest {
byte[][] bytesArrayVal = new byte[][] {
new byte[] {1, 2}, new byte[] {3, 4}
};
+ byte[][] uuidArrayVal = new byte[][] {
+ UuidUtils.toBytes("550e8400-e29b-41d4-a716-446655440000"),
+ UuidUtils.toBytes("550e8400-e29b-41d4-a716-446655440001")
+ };
Object unknownVal = null; // UNKNOWN is represented as null in this
example.
// Build a single row that contains all the above values.
@@ -180,7 +228,7 @@ public class JsonResponseEncoderTest {
Object[] row = new Object[] {
intVal, longVal, floatVal, doubleVal, bigDecimalVal, booleanVal,
timestampVal, stringVal,
jsonVal, mapVal, bytesVal, objectVal, intArrayVal, longArrayVal,
floatArrayVal, doubleArrayVal,
- booleanArrayVal, timestampArrayVal, stringArrayVal, bytesArrayVal,
unknownVal
+ booleanArrayVal, timestampArrayVal, stringArrayVal, bytesArrayVal,
uuidArrayVal, unknownVal
};
// Convert each value using the schema's formatting (if needed).
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/utils/DataSchemaTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/utils/DataSchemaTest.java
index 3a22b1d30cc..ba3bdceef4e 100644
---
a/pinot-common/src/test/java/org/apache/pinot/common/utils/DataSchemaTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/common/utils/DataSchemaTest.java
@@ -21,8 +21,11 @@ package org.apache.pinot.common.utils;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.sql.Timestamp;
+import java.util.Locale;
import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.utils.ByteArray;
import org.apache.pinot.spi.utils.BytesUtils;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -31,14 +34,19 @@ import static
org.apache.pinot.common.utils.DataSchema.ColumnDataType.*;
public class DataSchemaTest {
private static final String[] COLUMN_NAMES = {
- "int", "long", "float", "double", "string", "object", "int_array",
"long_array", "float_array", "double_array",
- "string_array", "boolean_array", "timestamp_array", "bytes_array"
+ "int", "long", "float", "double", "string", "uuid", "object",
"int_array", "long_array", "float_array",
+ "double_array", "string_array", "boolean_array", "timestamp_array",
"bytes_array", "uuid_array"
};
private static final int NUM_COLUMNS = COLUMN_NAMES.length;
private static final DataSchema.ColumnDataType[] COLUMN_DATA_TYPES = {
- INT, LONG, FLOAT, DOUBLE, STRING, OBJECT, INT_ARRAY, LONG_ARRAY,
FLOAT_ARRAY, DOUBLE_ARRAY, STRING_ARRAY,
- BOOLEAN_ARRAY, TIMESTAMP_ARRAY, BYTES_ARRAY
+ INT, LONG, FLOAT, DOUBLE, STRING, UUID, OBJECT, INT_ARRAY, LONG_ARRAY,
FLOAT_ARRAY, DOUBLE_ARRAY, STRING_ARRAY,
+ BOOLEAN_ARRAY, TIMESTAMP_ARRAY, BYTES_ARRAY, UUID_ARRAY
};
+ private static final String UUID_VALUE =
"550e8400-e29b-41d4-a716-446655440000";
+ private static final String UUID_VALUE_2 =
"550e8400-e29b-41d4-a716-446655440001";
+ // Fully qualified: the static ColumnDataType.* import below binds the
simple name UUID to the enum constant.
+ private static final java.util.UUID JAVA_UUID =
java.util.UUID.fromString(UUID_VALUE);
+ private static final java.util.UUID JAVA_UUID_2 =
java.util.UUID.fromString(UUID_VALUE_2);
@Test
public void testGetters() {
@@ -71,9 +79,10 @@ public class DataSchemaTest {
public void testToString() {
DataSchema dataSchema = new DataSchema(COLUMN_NAMES, COLUMN_DATA_TYPES);
Assert.assertEquals(dataSchema.toString(),
-
"[int(INT),long(LONG),float(FLOAT),double(DOUBLE),string(STRING),object(OBJECT),int_array(INT_ARRAY),"
- +
"long_array(LONG_ARRAY),float_array(FLOAT_ARRAY),double_array(DOUBLE_ARRAY),string_array(STRING_ARRAY),"
- +
"boolean_array(BOOLEAN_ARRAY),timestamp_array(TIMESTAMP_ARRAY),bytes_array(BYTES_ARRAY)]");
+
"[int(INT),long(LONG),float(FLOAT),double(DOUBLE),string(STRING),uuid(UUID),object(OBJECT),"
+ +
"int_array(INT_ARRAY),long_array(LONG_ARRAY),float_array(FLOAT_ARRAY),double_array(DOUBLE_ARRAY),"
+ +
"string_array(STRING_ARRAY),boolean_array(BOOLEAN_ARRAY),timestamp_array(TIMESTAMP_ARRAY),"
+ + "bytes_array(BYTES_ARRAY),uuid_array(UUID_ARRAY)]");
}
@Test
@@ -115,6 +124,16 @@ public class DataSchemaTest {
Assert.assertFalse(STRING.isCompatible(STRING_ARRAY));
Assert.assertFalse(STRING.isCompatible(BYTES_ARRAY));
+ Assert.assertFalse(UUID.isNumber());
+ Assert.assertFalse(UUID.isWholeNumber());
+ Assert.assertFalse(UUID.isArray());
+ Assert.assertFalse(UUID.isNumberArray());
+ Assert.assertFalse(UUID.isWholeNumberArray());
+ Assert.assertFalse(UUID.isCompatible(DOUBLE));
+ Assert.assertTrue(UUID.isCompatible(UUID));
+ Assert.assertFalse(UUID.isCompatible(BYTES));
+ Assert.assertFalse(UUID.isCompatible(STRING));
+
Assert.assertFalse(OBJECT.isNumber());
Assert.assertFalse(OBJECT.isWholeNumber());
Assert.assertFalse(OBJECT.isArray());
@@ -154,7 +173,7 @@ public class DataSchemaTest {
}
for (DataSchema.ColumnDataType columnDataType : new
DataSchema.ColumnDataType[]{
- STRING_ARRAY, BOOLEAN_ARRAY, TIMESTAMP_ARRAY, BYTES_ARRAY
+ STRING_ARRAY, BOOLEAN_ARRAY, TIMESTAMP_ARRAY, BYTES_ARRAY, UUID_ARRAY
}) {
Assert.assertFalse(columnDataType.isNumber());
Assert.assertFalse(columnDataType.isWholeNumber());
@@ -178,6 +197,8 @@ public class DataSchemaTest {
Assert.assertEquals(fromDataType(FieldSpec.DataType.DOUBLE, false),
DOUBLE_ARRAY);
Assert.assertEquals(fromDataType(FieldSpec.DataType.STRING, true), STRING);
Assert.assertEquals(fromDataType(FieldSpec.DataType.STRING, false),
STRING_ARRAY);
+ Assert.assertEquals(fromDataType(FieldSpec.DataType.UUID, true), UUID);
+ Assert.assertEquals(fromDataType(FieldSpec.DataType.UUID, false),
UUID_ARRAY);
Assert.assertEquals(fromDataType(FieldSpec.DataType.BOOLEAN, false),
BOOLEAN_ARRAY);
Assert.assertEquals(fromDataType(FieldSpec.DataType.TIMESTAMP, false),
TIMESTAMP_ARRAY);
Assert.assertEquals(fromDataType(FieldSpec.DataType.BYTES, false),
BYTES_ARRAY);
@@ -186,7 +207,58 @@ public class DataSchemaTest {
Assert.assertEquals(BIG_DECIMAL.format(bigDecimalValue),
bigDecimalValue.toPlainString());
Timestamp timestampValue = new Timestamp(1234567890123L);
Assert.assertEquals(TIMESTAMP.format(timestampValue),
timestampValue.toString());
+ ByteArray uuidValue = new ByteArray(UuidUtils.toBytes(UUID_VALUE));
+ Assert.assertEquals(UUID.convert(uuidValue), JAVA_UUID);
+ Assert.assertEquals(UUID.format(uuidValue), UUID_VALUE);
+ Assert.assertEquals(UUID.convertAndFormat(uuidValue), UUID_VALUE);
+ // format() also accepts the external form and re-canonicalizes
non-canonical strings.
+ Assert.assertEquals(UUID.format(JAVA_UUID), UUID_VALUE);
+ Assert.assertEquals(UUID.format(UUID_VALUE.toUpperCase(Locale.ROOT)),
UUID_VALUE);
+ byte[][] uuidArrayBytesValue = {UuidUtils.toBytes(UUID_VALUE),
UuidUtils.toBytes(UUID_VALUE_2)};
+ ByteArray[] uuidArrayValue = (ByteArray[]) UUID_ARRAY.toInternal(new
String[]{UUID_VALUE, UUID_VALUE_2});
+ java.util.UUID[] expectedUuidArray = {JAVA_UUID, JAVA_UUID_2};
+ String[] expectedFormatted = {UUID_VALUE, UUID_VALUE_2};
+ Assert.assertEquals(UUID_ARRAY.toExternal(uuidArrayValue),
expectedUuidArray);
+ Assert.assertEquals(UUID_ARRAY.toExternal(uuidArrayBytesValue),
expectedUuidArray);
+ Assert.assertEquals(UUID_ARRAY.convert(uuidArrayValue), expectedUuidArray);
+ Assert.assertEquals(UUID_ARRAY.toInternal(expectedUuidArray),
uuidArrayValue);
+ Assert.assertEquals(UUID_ARRAY.toInternal(uuidArrayBytesValue),
uuidArrayValue);
+ Assert.assertEquals(UUID_ARRAY.format(uuidArrayBytesValue),
expectedFormatted);
+ Assert.assertEquals(UUID_ARRAY.format(expectedUuidArray),
expectedFormatted);
+ Assert.assertEquals(UUID_ARRAY.format(uuidArrayValue), expectedFormatted);
+ Assert.assertEquals(UUID_ARRAY.convertAndFormat(uuidArrayValue),
expectedFormatted);
+ Assert.assertEquals(UUID_ARRAY.convertAndFormat(uuidArrayBytesValue),
expectedFormatted);
byte[] bytesValue = {12, 34, 56};
Assert.assertEquals(BYTES.format(bytesValue),
BytesUtils.toHexString(bytesValue));
}
+
+ /// The null placeholder must be resolved on the *logical* type. UUID is the
only type whose placeholder differs
+ /// from its stored type's: it needs the 16-byte nil UUID, while BYTES
supplies a zero-length one. Every other
+ /// logical type must agree with its stored type, otherwise callers that
resolve the stored type (DataBlockBuilder,
+ /// GroupByResultsBlock, GroupByDataTableReducer) would silently write the
wrong placeholder.
+ @Test
+ public void testNullPlaceholderMatchesStoredTypeExceptUuid() {
+ for (DataSchema.ColumnDataType columnDataType :
DataSchema.ColumnDataType.values()) {
+ DataSchema.ColumnDataType storedType = columnDataType.getStoredType();
+ if (columnDataType == UUID) {
+ Assert.assertEquals(storedType, BYTES);
+ Assert.assertEquals(columnDataType.getNullPlaceholder(), new
ByteArray(UuidUtils.nullUuidBytes()));
+ Assert.assertNotEquals(columnDataType.getNullPlaceholder(),
storedType.getNullPlaceholder());
+ } else {
+ Assert.assertEquals(columnDataType.getNullPlaceholder(),
storedType.getNullPlaceholder(),
+ "Null placeholder mismatch between " + columnDataType + " and its
stored type " + storedType);
+ }
+ }
+ }
+
+ /// The nil-UUID placeholder wraps a mutable 16-byte array, unlike every
other placeholder (all empty or
+ /// immutable), so each call must hand back a fresh instance.
+ @Test
+ public void testUuidNullPlaceholderIsNotShared() {
+ ByteArray first = (ByteArray) UUID.getNullPlaceholder();
+ ByteArray second = (ByteArray) UUID.getNullPlaceholder();
+ Assert.assertNotSame(first, second);
+ first.getBytes()[0] = 1;
+ Assert.assertEquals(second, new ByteArray(UuidUtils.nullUuidBytes()));
+ }
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/common/datablock/DataBlockBuilder.java
b/pinot-core/src/main/java/org/apache/pinot/core/common/datablock/DataBlockBuilder.java
index 3a15b30184e..6e418e572c4 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/common/datablock/DataBlockBuilder.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/common/datablock/DataBlockBuilder.java
@@ -81,7 +81,9 @@ public class DataBlockBuilder {
Object[] nullPlaceholders = new Object[numColumns];
for (int colId = 0; colId < numColumns; colId++) {
nullBitmaps[colId] = new RoaringBitmap();
- nullPlaceholders[colId] = storedTypes[colId].getNullPlaceholder();
+ // Resolved on the logical type, not the stored type: UUID overrides
getNullPlaceholder() to return the nil
+ // UUID, whereas its stored type BYTES would yield a zero-length
placeholder that is not a valid UUID.
+ nullPlaceholders[colId] =
dataSchema.getColumnDataType(colId).getNullPlaceholder();
}
int nullFixedBytes = numColumns * Integer.BYTES * 2;
int rowSizeInBytes = calculateBytesPerRow(dataSchema);
@@ -254,7 +256,11 @@ public class DataBlockBuilder {
ByteBuffer fixedSize, PagedPinotOutputStream varSize, RoaringBitmap
nullBitmap,
Object2IntOpenHashMap<String> dictionary, @Nullable AggregationFunction
aggFunction)
throws IOException {
- ColumnDataType storedType =
dataSchema.getColumnDataType(colId).getStoredType();
+ // Dispatch on the stored type, but read null placeholders off the logical
type: UUID overrides
+ // getNullPlaceholder() to return the nil UUID, whereas its stored type
BYTES would yield a zero-length
+ // placeholder that is not a valid UUID. The two agree for every other
type (see DataSchemaTest).
+ ColumnDataType columnDataType = dataSchema.getColumnDataType(colId);
+ ColumnDataType storedType = columnDataType.getStoredType();
int numRows = columns.get(colId).length;
Object[] column = columns.get(colId);
@@ -266,7 +272,7 @@ public class DataBlockBuilder {
switch (storedType) {
// Single-value column
case INT: {
- int nullPlaceholder = (int) storedType.getNullPlaceholder();
+ int nullPlaceholder = (int) columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -281,7 +287,7 @@ public class DataBlockBuilder {
break;
}
case LONG: {
- long nullPlaceholder = (long) storedType.getNullPlaceholder();
+ long nullPlaceholder = (long) columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -296,7 +302,7 @@ public class DataBlockBuilder {
break;
}
case FLOAT: {
- float nullPlaceholder = (float) storedType.getNullPlaceholder();
+ float nullPlaceholder = (float) columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -311,7 +317,7 @@ public class DataBlockBuilder {
break;
}
case DOUBLE: {
- double nullPlaceholder = (double) storedType.getNullPlaceholder();
+ double nullPlaceholder = (double) columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -326,7 +332,7 @@ public class DataBlockBuilder {
break;
}
case BIG_DECIMAL: {
- BigDecimal nullPlaceholder = (BigDecimal)
storedType.getNullPlaceholder();
+ BigDecimal nullPlaceholder = (BigDecimal)
columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -342,7 +348,7 @@ public class DataBlockBuilder {
}
case STRING: {
ToIntFunction<String> didSupplier = k -> dictionary.size();
- int nullPlaceHolder = dictionary.computeIfAbsent((String)
storedType.getNullPlaceholder(), didSupplier);
+ int nullPlaceHolder = dictionary.computeIfAbsent((String)
columnDataType.getNullPlaceholder(), didSupplier);
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -358,7 +364,7 @@ public class DataBlockBuilder {
break;
}
case BYTES: {
- ByteArray nullPlaceholder = (ByteArray)
storedType.getNullPlaceholder();
+ ByteArray nullPlaceholder = (ByteArray)
columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -373,7 +379,7 @@ public class DataBlockBuilder {
break;
}
case MAP: {
- Map nullPlaceholder = (Map) storedType.getNullPlaceholder();
+ Map nullPlaceholder = (Map) columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -389,7 +395,7 @@ public class DataBlockBuilder {
}
// Multi-value column
case INT_ARRAY: {
- int[] nullPlaceholder = (int[]) storedType.getNullPlaceholder();
+ int[] nullPlaceholder = (int[]) columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -404,7 +410,7 @@ public class DataBlockBuilder {
break;
}
case LONG_ARRAY: {
- long[] nullPlaceholder = (long[]) storedType.getNullPlaceholder();
+ long[] nullPlaceholder = (long[]) columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -419,7 +425,7 @@ public class DataBlockBuilder {
break;
}
case FLOAT_ARRAY: {
- float[] nullPlaceholder = (float[]) storedType.getNullPlaceholder();
+ float[] nullPlaceholder = (float[])
columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -434,7 +440,7 @@ public class DataBlockBuilder {
break;
}
case DOUBLE_ARRAY: {
- double[] nullPlaceholder = (double[]) storedType.getNullPlaceholder();
+ double[] nullPlaceholder = (double[])
columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -449,7 +455,7 @@ public class DataBlockBuilder {
break;
}
case BIG_DECIMAL_ARRAY: {
- BigDecimal[] nullPlaceholder = (BigDecimal[])
storedType.getNullPlaceholder();
+ BigDecimal[] nullPlaceholder = (BigDecimal[])
columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -464,7 +470,7 @@ public class DataBlockBuilder {
break;
}
case STRING_ARRAY: {
- String[] nullPlaceholder = (String[]) storedType.getNullPlaceholder();
+ String[] nullPlaceholder = (String[])
columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
@@ -479,7 +485,7 @@ public class DataBlockBuilder {
break;
}
case BYTES_ARRAY: {
- ByteArray[] nullPlaceholder = (ByteArray[])
storedType.getNullPlaceholder();
+ ByteArray[] nullPlaceholder = (ByteArray[])
columnDataType.getNullPlaceholder();
interruptableLoop(0, numRows, interruptableLoopStep, (start, end) -> {
for (int rowId = start; rowId < end; rowId++) {
Object value = column[rowId];
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/GroupByResultsBlock.java
b/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/GroupByResultsBlock.java
index 6cfc8511818..0a5565be7b9 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/GroupByResultsBlock.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/GroupByResultsBlock.java
@@ -220,7 +220,9 @@ public class GroupByResultsBlock extends BaseResultsBlock {
Object[] nullPlaceholders = new Object[numColumns];
for (int colId = 0; colId < numColumns; colId++) {
nullBitmaps[colId] = new RoaringBitmap();
- nullPlaceholders[colId] =
storedColumnDataTypes[colId].getNullPlaceholder();
+ // Resolved on the logical type, not the stored type: UUID overrides
getNullPlaceholder() to return the nil
+ // UUID, whereas its stored type BYTES would yield a zero-length
placeholder that is not a valid UUID.
+ nullPlaceholders[colId] =
_dataSchema.getColumnDataType(colId).getNullPlaceholder();
}
int rowId = 0;
while (iterator.hasNext()) {
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/reduce/GroupByDataTableReducer.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/reduce/GroupByDataTableReducer.java
index bfbd8bb8486..85b8f614f3d 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/reduce/GroupByDataTableReducer.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/reduce/GroupByDataTableReducer.java
@@ -564,7 +564,9 @@ public class GroupByDataTableReducer implements
DataTableReducer {
Object[] nullPlaceholders = new Object[_numColumns];
for (int colId = 0; colId < _numColumns; colId++) {
nullBitmaps[colId] = new RoaringBitmap();
- nullPlaceholders[colId] =
storedColumnDataTypes[colId].getNullPlaceholder();
+ // Resolved on the logical type, not the stored type: UUID overrides
getNullPlaceholder() to return the nil
+ // UUID, whereas its stored type BYTES would yield a zero-length
placeholder that is not a valid UUID.
+ nullPlaceholders[colId] =
dataSchema.getColumnDataType(colId).getNullPlaceholder();
}
int rowId = 0;
while (iterator.hasNext()) {
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/common/datablock/DataBlockBuilderTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/common/datablock/DataBlockBuilderTest.java
index f7206f59189..24e4c03b11c 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/common/datablock/DataBlockBuilderTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/common/datablock/DataBlockBuilderTest.java
@@ -32,6 +32,7 @@ import org.apache.pinot.common.utils.DataSchema;
import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
import org.apache.pinot.spi.utils.ByteArray;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.roaringbitmap.RoaringBitmap;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
@@ -194,6 +195,28 @@ public class DataBlockBuilderTest {
runColumnBlockTest(type, 25_000);
}
+ /// A null in a UUID column must serialize as the nil UUID, not as the
zero-length placeholder its stored type
+ /// (BYTES) supplies. The value is normally masked by the null bitmap, but
it must still decode as a valid 16-byte
+ /// UUID for any consumer that renders the raw column, and
[UuidUtils#toString] rejects any other width.
+ @Test
+ void testUuidNullPlaceholderIsNilUuid()
+ throws IOException {
+ ByteArray uuid = new
ByteArray(UuidUtils.toBytes("550e8400-e29b-41d4-a716-446655440000"));
+ DataSchema dataSchema = new DataSchema(new String[]{"uuidCol"}, new
ColumnDataType[]{ColumnDataType.UUID});
+ Object[] column = {uuid, null};
+ List<Object[]> rows = List.of(new Object[]{uuid}, new Object[]{null});
+
+ List<DataBlock> blocks = List.of(DataBlockBuilder.buildFromRows(rows,
dataSchema),
+ DataBlockBuilder.buildFromColumns(List.<Object[]>of(column),
dataSchema));
+ for (DataBlock block : blocks) {
+ assertEquals(block.getNumberOfRows(), 2);
+ assertEquals(new ByteArray(block.getBytes(0, 0).getBytes()), uuid);
+ // Row 1 is null: the bitmap flags it, and the placeholder underneath
still renders as the nil UUID.
+ assertEquals(block.getNullRowIds(0), RoaringBitmap.bitmapOf(1));
+ assertEquals(UuidUtils.toString(block.getBytes(1, 0).getBytes()),
"00000000-0000-0000-0000-000000000000");
+ }
+ }
+
private void runColumnBlockTest(ColumnDataType type, int numRows)
throws IOException {
Object[] column = generateColumns(type, numRows);
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/common/datablock/DataBlockTestUtils.java
b/pinot-core/src/test/java/org/apache/pinot/core/common/datablock/DataBlockTestUtils.java
index c96a21b5e86..a9fd19f0900 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/common/datablock/DataBlockTestUtils.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/common/datablock/DataBlockTestUtils.java
@@ -24,11 +24,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
+import java.util.UUID;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.pinot.common.datablock.DataBlock;
import org.apache.pinot.common.utils.DataSchema;
import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
import org.apache.pinot.spi.utils.ByteArray;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.roaringbitmap.RoaringBitmap;
@@ -73,6 +75,9 @@ public class DataBlockTestUtils {
case BYTES:
row[colId] = new
ByteArray(RandomStringUtils.secure().next(RANDOM.nextInt(20)).getBytes());
break;
+ case UUID:
+ row[colId] = new ByteArray(UuidUtils.toBytes(new
UUID(RANDOM.nextLong(), RANDOM.nextLong())));
+ break;
case INT_ARRAY:
int length = RANDOM.nextInt(ARRAY_SIZE);
int[] intArray = new int[length];
@@ -147,6 +152,14 @@ public class DataBlockTestUtils {
}
row[colId] = bytesArray;
break;
+ case UUID_ARRAY:
+ length = RANDOM.nextInt(ARRAY_SIZE);
+ ByteArray[] uuidArray = new ByteArray[length];
+ for (int i = 0; i < length; i++) {
+ uuidArray[i] = new ByteArray(UuidUtils.toBytes(new
UUID(RANDOM.nextLong(), RANDOM.nextLong())));
+ }
+ row[colId] = uuidArray;
+ break;
case MAP:
length = RANDOM.nextInt(ARRAY_SIZE);
Map<String, Object> map = new HashMap<>();
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
index 7f416c9f414..f2ac8811035 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/common/datatable/DataTableSerDeTest.java
@@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
+import java.util.UUID;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.pinot.common.datatable.DataTable;
@@ -34,6 +35,7 @@ import org.apache.pinot.common.utils.DataSchema;
import org.apache.pinot.spi.accounting.ThreadResourceUsageProvider;
import org.apache.pinot.spi.exception.QueryErrorCode;
import org.apache.pinot.spi.utils.ByteArray;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.roaringbitmap.RoaringBitmap;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
@@ -60,6 +62,7 @@ public class DataTableSerDeTest {
private static final String[] STRINGS = new String[NUM_ROWS];
private static final String[] JSONS = new String[NUM_ROWS];
private static final byte[][] BYTES = new byte[NUM_ROWS][];
+ private static final byte[][] UUIDS = new byte[NUM_ROWS][];
private static final Object[] OBJECTS = new Object[NUM_ROWS];
private static final int[][] INT_ARRAYS = new int[NUM_ROWS][];
private static final long[][] LONG_ARRAYS = new long[NUM_ROWS][];
@@ -69,6 +72,7 @@ public class DataTableSerDeTest {
private static final long[][] TIMESTAMP_ARRAYS = new long[NUM_ROWS][];
private static final String[][] STRING_ARRAYS = new String[NUM_ROWS][];
private static final ByteArray[][] BYTES_ARRAYS = new ByteArray[NUM_ROWS][];
+ private static final ByteArray[][] UUID_ARRAYS = new ByteArray[NUM_ROWS][];
private static final BigDecimal[][] BIG_DECIMAL_ARRAYS = new
BigDecimal[NUM_ROWS][];
private static final Map<String, Object>[] MAPS = new Map[NUM_ROWS];
@@ -367,6 +371,11 @@ public class DataTableSerDeTest {
BYTES[rowId] = isNull ? new byte[0] :
RandomStringUtils.secure().next(RANDOM.nextInt(20)).getBytes();
dataTableBuilder.setColumn(colId, new ByteArray(BYTES[rowId]));
break;
+ case UUID:
+ UUIDS[rowId] = isNull ? UuidUtils.nullUuidBytes()
+ : UuidUtils.toBytes(new UUID(RANDOM.nextLong(),
RANDOM.nextLong()));
+ dataTableBuilder.setColumn(colId, new ByteArray(UUIDS[rowId]));
+ break;
case INT_ARRAY:
int length = RANDOM.nextInt(20);
int[] intArray = new int[length];
@@ -440,6 +449,15 @@ public class DataTableSerDeTest {
BYTES_ARRAYS[rowId] = bytesArray;
dataTableBuilder.setColumn(colId, bytesArray);
break;
+ case UUID_ARRAY:
+ length = RANDOM.nextInt(20);
+ ByteArray[] uuidArray = new ByteArray[length];
+ for (int i = 0; i < length; i++) {
+ uuidArray[i] = new ByteArray(UuidUtils.toBytes(new
UUID(RANDOM.nextLong(), RANDOM.nextLong())));
+ }
+ UUID_ARRAYS[rowId] = uuidArray;
+ dataTableBuilder.setColumn(colId, uuidArray);
+ break;
case STRING_ARRAY:
length = RANDOM.nextInt(20);
String[] stringArray = new String[length];
@@ -520,6 +538,10 @@ public class DataTableSerDeTest {
Assert.assertEquals(newDataTable.getBytes(rowId,
colId).getBytes(), isNull ? new byte[0] : BYTES[rowId],
ERROR_MESSAGE);
break;
+ case UUID:
+ Assert.assertEquals(newDataTable.getBytes(rowId, colId).getBytes(),
+ isNull ? UuidUtils.nullUuidBytes() : UUIDS[rowId],
ERROR_MESSAGE);
+ break;
case INT_ARRAY:
Assert.assertTrue(Arrays.equals(newDataTable.getIntArray(rowId,
colId), INT_ARRAYS[rowId]), ERROR_MESSAGE);
break;
@@ -551,6 +573,10 @@ public class DataTableSerDeTest {
Assert.assertTrue(Arrays.equals(newDataTable.getBytesArray(rowId,
colId), BYTES_ARRAYS[rowId]),
ERROR_MESSAGE);
break;
+ case UUID_ARRAY:
+ Assert.assertTrue(Arrays.equals(newDataTable.getBytesArray(rowId,
colId), UUID_ARRAYS[rowId]),
+ ERROR_MESSAGE);
+ break;
case STRING_ARRAY:
Assert.assertTrue(Arrays.equals(newDataTable.getStringArray(rowId,
colId), STRING_ARRAYS[rowId]),
ERROR_MESSAGE);
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/selection/SelectionOperatorUtilsTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/selection/SelectionOperatorUtilsTest.java
index 196f4dd168e..1df11e8cf86 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/selection/SelectionOperatorUtilsTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/selection/SelectionOperatorUtilsTest.java
@@ -18,17 +18,23 @@
*/
package org.apache.pinot.core.query.selection;
+import java.util.Collections;
import org.apache.commons.lang3.tuple.Pair;
+import org.apache.pinot.common.response.broker.ResultTable;
import org.apache.pinot.common.utils.DataSchema;
import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
import org.apache.pinot.core.query.request.context.QueryContext;
import
org.apache.pinot.core.query.request.context.utils.QueryContextConverterUtils;
+import org.apache.pinot.spi.utils.ByteArray;
+import org.apache.pinot.spi.utils.BytesUtils;
+import org.apache.pinot.spi.utils.UuidUtils;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
public class SelectionOperatorUtilsTest {
+ private static final String UUID_VALUE =
"550e8400-e29b-41d4-a716-446655440000";
@Test
public void testGetResultTableColumnIndices() {
@@ -207,4 +213,21 @@ public class SelectionOperatorUtilsTest {
ColumnDataType.STRING, ColumnDataType.STRING, ColumnDataType.STRING
}));
}
+
+ @Test
+ public void testRenderResultTableWithoutOrderingFormatsUUIDAndBytes() {
+ byte[] bytesValue = new byte[]{0x01, 0x23, 0x45};
+ DataSchema dataSchema = new DataSchema(new String[]{"uuidCol", "bytesCol"},
+ new ColumnDataType[]{ColumnDataType.UUID, ColumnDataType.BYTES});
+
+ ResultTable resultTable =
SelectionOperatorUtils.renderResultTableWithoutOrdering(
+ Collections.singletonList(
+ new Object[]{new ByteArray(UuidUtils.toBytes(UUID_VALUE)), new
ByteArray(bytesValue)}),
+ dataSchema,
+ new int[]{0, 1});
+
+ Object[] row = resultTable.getRows().get(0);
+ assertEquals(row[0], UUID_VALUE);
+ assertEquals(row[1], BytesUtils.toHexString(bytesValue));
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]