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 a8b207e7815 Generate Avro schemas from Pinot logical data types
(#19071) (#19073)
a8b207e7815 is described below
commit a8b207e78156296f686eea3a312a9cb68d08ac6a
Author: Xiang Fu <[email protected]>
AuthorDate: Mon Jul 27 17:10:31 2026 -0700
Generate Avro schemas from Pinot logical data types (#19071) (#19073)
---
.../pinot/core/util/SegmentProcessorAvroUtils.java | 215 ++++++++++-------
.../core/util/SegmentProcessorAvroUtilsTest.java | 258 +++++++++++++++++++++
.../plugin/inputformat/avro/AvroSchemaUtil.java | 147 ++++++------
.../pinot/plugin/inputformat/avro/AvroUtils.java | 76 +-----
.../inputformat/avro/AvroSchemaUtilTest.java | 67 ++++--
.../plugin/inputformat/avro/AvroUtilsTest.java | 84 +++++++
.../filebased/FileBasedSegmentWriterTest.java | 46 ++++
.../converter/PinotSegmentConverterTest.java | 75 ++++++
8 files changed, 738 insertions(+), 230 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/util/SegmentProcessorAvroUtils.java
b/pinot-core/src/main/java/org/apache/pinot/core/util/SegmentProcessorAvroUtils.java
index 0c7f5b5e2a7..5eb58a5e3c3 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/util/SegmentProcessorAvroUtils.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/util/SegmentProcessorAvroUtils.java
@@ -19,12 +19,15 @@
package org.apache.pinot.core.util;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
+import javax.annotation.Nullable;
import org.apache.avro.Conversion;
+import org.apache.avro.Conversions;
import org.apache.avro.LogicalType;
import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema;
@@ -51,43 +54,103 @@ public final class SegmentProcessorAvroUtils {
return convertGenericRowToAvroRecord(genericRow, reusableRecord,
genericRow.getFieldToValueMap().keySet());
}
- /// Convert a GenericRow to an avro GenericRecord
+ /// Convert a GenericRow to an avro GenericRecord.
+ ///
+ /// Values arrive in Pinot's internal (stored) representation and are
coordinated with the Avro field type produced
+ /// by `AvroSchemaUtil.toAvroSchema`: whatever a registered logical-type
[Conversion] can handle is left untouched
+ /// for the writer, and only the two cases Avro cannot resolve on its own
are fixed up here (see
+ /// [#convertValue(Schema, Object)]).
public static GenericData.Record convertGenericRowToAvroRecord(GenericRow
genericRow,
GenericData.Record reusableRecord, Set<String> fields) {
Schema avroSchema = reusableRecord.getSchema();
for (String field : fields) {
Object value = genericRow.getValue(field);
- if (value instanceof Object[]) {
- // Array elements are written as-is. For MV UUID
(array<string{logicalType:uuid}>) the elements are the raw
- // 16-byte values; the uuid Conversion registered on the writer's data
model (getAvroDataModel) renders each
- // element to its canonical string at write time.
- reusableRecord.put(field, Arrays.asList((Object[]) value));
- } else if (value instanceof byte[]) {
- // A byte[] bound for a plain BYTES field must be wrapped as
ByteBuffer (GenericDatumWriter requires it for the
- // bytes type). A byte[] bound for a UUID field
(string{logicalType:uuid}) is left raw so the uuid Conversion
- // registered on the writer's data model (getAvroDataModel) renders it
to a canonical string at write time.
- Schema.Field avroField = avroSchema.getField(field);
- if (avroField != null && avroField.schema().getType() ==
Schema.Type.BYTES) {
- reusableRecord.put(field, ByteBuffer.wrap((byte[]) value));
- } else {
- reusableRecord.put(field, value);
- }
- } else {
+ Schema.Field avroField = avroSchema.getField(field);
+ if (avroField == null) {
+ // Let Avro raise its own "Not a valid schema field" error for a
column missing from the Avro schema.
reusableRecord.put(field, value);
+ } else {
+ reusableRecord.put(avroField.pos(), convertValue(avroField.schema(),
value));
}
}
return reusableRecord;
}
- /// Shared Avro data model with [UuidConversion] registered. Populated once
at class initialization and never
- /// mutated afterward (effectively immutable), so it is safe to share across
writers.
+ /// Adapts a Pinot value to the representation the Avro writer expects for
the given field schema, recursing into
+ /// array elements for multi-value columns.
+ @Nullable
+ private static Object convertValue(Schema fieldSchema, @Nullable Object
value) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Object[]) {
+ Object[] values = (Object[]) value;
+ Schema elementSchema =
+ fieldSchema.getType() == Schema.Type.ARRAY ?
fieldSchema.getElementType() : fieldSchema;
+ // Only BOOLEAN (stored Integer -> Boolean) and plain BYTES (byte[] ->
ByteBuffer) element schemas require a
+ // per-element transform. Every other MV element type is written as-is —
including BIG_DECIMAL, whose logical
+ // BYTES schema is handled by the registered Conversion — so hand the
writer a zero-copy view over the existing
+ // array; allocating and copying a fresh list per row would be pure
overhead on the segment-write hot path.
+ Schema.Type elementType = elementSchema.getType();
+ if (elementType != Schema.Type.BOOLEAN
+ && (elementType != Schema.Type.BYTES ||
isBigDecimalSchema(elementSchema))) {
+ return Arrays.asList(values);
+ }
+ List<Object> converted = new ArrayList<>(values.length);
+ for (Object singleValue : values) {
+ converted.add(convertSingleValue(elementSchema, singleValue));
+ }
+ return converted;
+ }
+ return convertSingleValue(fieldSchema, value);
+ }
+
+ /// Adapts a single (non-array) Pinot value to what `GenericDatumWriter`
expects for `valueSchema`.
+ ///
+ /// Only two cases need fixing up; everything else is written as-is, either
because the Pinot representation already
+ /// *is* the Avro representation (`Integer` for `int`, `Long` for
`long{timestamp-millis}`, `String` for `string`)
+ /// or because a [Conversion] registered on [#getAvroDataModel] handles it
(`byte[]` for `string{uuid}`,
+ /// [java.math.BigDecimal] for `bytes{big-decimal}`).
+ @Nullable
+ private static Object convertSingleValue(Schema valueSchema, @Nullable
Object value) {
+ if (value == null) {
+ return null;
+ }
+ switch (valueSchema.getType()) {
+ case BOOLEAN:
+ // BOOLEAN is the one Pinot logical type with no Avro logical type to
carry a Conversion, so its stored int
+ // 0/1 has to be coerced here. All production callers provide either a
transformed row or a segment-read row,
+ // so a BOOLEAN value is always in Pinot's stored Integer form.
+ return (Integer) value != 0;
+ case BYTES:
+ // A plain BYTES value is always byte[] and must be wrapped as
ByteBuffer (GenericDatumWriter requires it).
+ // BIG_DECIMAL's logical BYTES value must stay untouched for its
registered Conversion.
+ return isBigDecimalSchema(valueSchema) ? value :
ByteBuffer.wrap((byte[]) value);
+ default:
+ return value;
+ }
+ }
+
+ private static boolean isBigDecimalSchema(Schema schema) {
+ LogicalType logicalType = schema.getLogicalType();
+ return logicalType != null &&
LogicalTypes.bigDecimal().getName().equals(logicalType.getName());
+ }
+
+ /// Shared Avro data model with the logical-type conversions registered.
Populated once at class initialization and
+ /// never mutated afterward (effectively immutable), so it is safe to share
across writers.
private static final GenericData AVRO_DATA_MODEL = createAvroDataModel();
/// Returns the shared Avro data model that a `GenericDatumWriter` (or
`AvroParquetWriter`) must be constructed with
- /// to serialize UUID columns produced by [#convertGenericRowToAvroRecord]:
it registers [UuidConversion] so the
- /// internal 16-byte UUID form is rendered as the canonical string required
by `string{logicalType:uuid}` fields.
- /// The UUID column's field schema must be `string{logicalType:uuid}` — as
emitted by
- /// [#convertPinotSchemaToAvroSchema] and
`AvroUtils.getAvroSchemaFromPinotSchema` — for the conversion to apply.
+ /// to serialize the logical-type columns produced by
[#convertGenericRowToAvroRecord]:
+ /// - [UuidConversion] renders Pinot's internal 16-byte UUID form as the
canonical string required by
+ /// `string{logicalType:uuid}` fields.
+ /// - Avro's `BigDecimalConversion` encodes a [java.math.BigDecimal] —
unscaled value plus its own scale — into the
+ /// `bytes{logicalType:big-decimal}` fields Pinot emits for BIG_DECIMAL
columns.
+ ///
+ /// The column's field schema must be the one emitted by
[#convertPinotSchemaToAvroSchema] /
+ /// `AvroUtils.getAvroSchemaFromPinotSchema` for the conversions to apply.
BOOLEAN and TIMESTAMP need no entry
+ /// here: Avro has no `boolean` logical type (the coercion happens in
[#convertGenericRowToAvroRecord]), and a
+ /// `Long` already *is* the base representation of
`long{logicalType:timestamp-millis}`.
///
/// The returned instance is shared and must be treated as read-only: do not
call its mutators
/// (`addLogicalTypeConversion`, `setStringType`, ...), which are not
thread-safe against concurrent writer reads.
@@ -98,6 +161,7 @@ public final class SegmentProcessorAvroUtils {
private static GenericData createAvroDataModel() {
GenericData model = new GenericData();
model.addLogicalTypeConversion(new UuidConversion());
+ model.addLogicalTypeConversion(new Conversions.BigDecimalConversion());
return model;
}
@@ -133,74 +197,57 @@ public final class SegmentProcessorAvroUtils {
}
}
- /// Converts a Pinot schema to an Avro schema
+ /// Converts a Pinot schema to an Avro schema, with the fields ordered by
column name.
+ ///
+ /// Field types are derived from the **original (logical)** Pinot data type,
so BOOLEAN becomes Avro `boolean`,
+ /// TIMESTAMP a `timestamp-millis` long, BIG_DECIMAL a `big-decimal` bytes
and UUID a `uuid` string, instead of all
+ /// four collapsing to their physical storage type. This must stay identical
to
+ /// `AvroSchemaUtil.toAvroSchema(FieldSpec)` in `pinot-avro-base` — the two
live in different modules (neither can
+ /// depend on the other) but feed the same writers, so
`SegmentProcessorAvroUtilsTest` pins them together. See that
+ /// method for the full mapping table and the value representation each Avro
type expects.
public static Schema
convertPinotSchemaToAvroSchema(org.apache.pinot.spi.data.Schema pinotSchema) {
SchemaBuilder.FieldAssembler<org.apache.avro.Schema> fieldAssembler =
SchemaBuilder.record("record").fields();
-
List<FieldSpec> orderedFieldSpecs = pinotSchema.getAllFieldSpecs().stream()
.sorted(Comparator.comparing(FieldSpec::getName))
.collect(Collectors.toList());
for (FieldSpec fieldSpec : orderedFieldSpecs) {
- String name = fieldSpec.getName();
- // Emit UUID columns as Avro string{logicalType:uuid} (matching
AvroUtils.getAvroSchemaFromPinotSchema)
- // so the runtime byte[] → canonical-string conversion in
convertGenericRowToAvroRecord lines up with
- // the field schema. Without this branch SV UUID would fall through to
BYTES (losing UUID semantics) and
- // MV UUID would throw at this point (MV switch below has no BYTES case).
- if (fieldSpec.getDataType() == DataType.UUID) {
- Schema uuidSchema =
LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING));
- if (fieldSpec.isSingleValueField()) {
- fieldAssembler =
fieldAssembler.name(name).type(uuidSchema).noDefault();
- } else {
- fieldAssembler =
fieldAssembler.name(name).type().array().items(uuidSchema).noDefault();
- }
- continue;
- }
- DataType storedType = fieldSpec.getDataType().getStoredType();
- if (fieldSpec.isSingleValueField()) {
- switch (storedType) {
- case INT:
- fieldAssembler =
fieldAssembler.name(name).type().intType().noDefault();
- break;
- case LONG:
- fieldAssembler =
fieldAssembler.name(name).type().longType().noDefault();
- break;
- case FLOAT:
- fieldAssembler =
fieldAssembler.name(name).type().floatType().noDefault();
- break;
- case DOUBLE:
- fieldAssembler =
fieldAssembler.name(name).type().doubleType().noDefault();
- break;
- case STRING:
- fieldAssembler =
fieldAssembler.name(name).type().stringType().noDefault();
- break;
- case BYTES:
- fieldAssembler =
fieldAssembler.name(name).type().bytesType().noDefault();
- break;
- default:
- throw new RuntimeException("Unsupported data type: " + storedType);
- }
- } else {
- switch (storedType) {
- case INT:
- fieldAssembler =
fieldAssembler.name(name).type().array().items().intType().noDefault();
- break;
- case LONG:
- fieldAssembler =
fieldAssembler.name(name).type().array().items().longType().noDefault();
- break;
- case FLOAT:
- fieldAssembler =
fieldAssembler.name(name).type().array().items().floatType().noDefault();
- break;
- case DOUBLE:
- fieldAssembler =
fieldAssembler.name(name).type().array().items().doubleType().noDefault();
- break;
- case STRING:
- fieldAssembler =
fieldAssembler.name(name).type().array().items().stringType().noDefault();
- break;
- default:
- throw new RuntimeException("Unsupported data type: " + storedType);
- }
- }
+ fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type(toAvroSchema(fieldSpec)).noDefault();
}
return fieldAssembler.endRecord();
}
+
+ /// Returns the Avro schema for a whole Pinot column: the single-value type
from [#toAvroSchema(DataType)], or an
+ /// array of it for a multi-value column.
+ private static Schema toAvroSchema(FieldSpec fieldSpec) {
+ Schema valueSchema = toAvroSchema(fieldSpec.getDataType());
+ return fieldSpec.isSingleValueField() ? valueSchema :
Schema.createArray(valueSchema);
+ }
+
+ private static Schema toAvroSchema(DataType dataType) {
+ switch (dataType) {
+ case INT:
+ return Schema.create(Schema.Type.INT);
+ case LONG:
+ return Schema.create(Schema.Type.LONG);
+ case FLOAT:
+ return Schema.create(Schema.Type.FLOAT);
+ case DOUBLE:
+ return Schema.create(Schema.Type.DOUBLE);
+ case BOOLEAN:
+ return Schema.create(Schema.Type.BOOLEAN);
+ case TIMESTAMP:
+ return
LogicalTypes.timestampMillis().addToSchema(Schema.create(Schema.Type.LONG));
+ case BIG_DECIMAL:
+ return
LogicalTypes.bigDecimal().addToSchema(Schema.create(Schema.Type.BYTES));
+ case STRING:
+ case JSON:
+ return Schema.create(Schema.Type.STRING);
+ case BYTES:
+ return Schema.create(Schema.Type.BYTES);
+ case UUID:
+ return
LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING));
+ default:
+ throw new UnsupportedOperationException("Unsupported data type: " +
dataType);
+ }
+ }
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/util/SegmentProcessorAvroUtilsTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/util/SegmentProcessorAvroUtilsTest.java
index 2f3012a2c66..b595ae2bfd2 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/util/SegmentProcessorAvroUtilsTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/util/SegmentProcessorAvroUtilsTest.java
@@ -19,8 +19,12 @@
package org.apache.pinot.core.util;
import java.io.File;
+import java.math.BigDecimal;
import java.nio.ByteBuffer;
+import java.nio.file.Files;
import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.avro.LogicalType;
import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema;
import org.apache.avro.SchemaBuilder;
@@ -31,12 +35,20 @@ import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.commons.io.FileUtils;
+import org.apache.pinot.plugin.inputformat.avro.AvroRecordReader;
+import org.apache.pinot.plugin.inputformat.avro.AvroUtils;
+import org.apache.pinot.segment.local.utils.DataTypeTransformerUtils;
+import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.utils.PinotDataType;
import org.apache.pinot.spi.utils.UuidUtils;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
@@ -67,6 +79,30 @@ public class SegmentProcessorAvroUtilsTest {
assertEquals(record.get("bytesCol"), ByteBuffer.wrap(rawBytes), "BYTES
byte[] must be wrapped as ByteBuffer");
}
+ /// A logical BYTES array such as `bytes{logicalType:big-decimal}` is
handled by its registered Conversion and must
+ /// not take the plain-BYTES per-element copy path.
+ @Test
+ public void
testConvertGenericRowToAvroRecordKeepsLogicalBytesArrayZeroCopy() {
+ Schema bigDecimalSchema =
LogicalTypes.bigDecimal().addToSchema(Schema.create(Schema.Type.BYTES));
+ Schema recordSchema = SchemaBuilder.record("record").fields()
+
.name("bigDecimalCol").type().array().items(bigDecimalSchema).noDefault()
+ .endRecord();
+
+ BigDecimal original = new BigDecimal("123.45");
+ BigDecimal replacement = new BigDecimal("678.90");
+ Object[] values = {original};
+ GenericRow row = new GenericRow();
+ row.putValue("bigDecimalCol", values);
+
+ GenericData.Record record = new GenericData.Record(recordSchema);
+ SegmentProcessorAvroUtils.convertGenericRowToAvroRecord(row, record);
+
+ List<?> converted = (List<?>) record.get("bigDecimalCol");
+ assertSame(converted.get(0), original);
+ values[0] = replacement;
+ assertSame(converted.get(0), replacement, "logical BYTES arrays must
retain the zero-copy array view");
+ }
+
/// End-to-end: a GenericDatumWriter built with getAvroDataModel()
serializes the raw 16-byte UUID values as their
/// canonical string (via the registered uuid Conversion) for both SV and
MV, and the on-disk value reads back as
/// that string with a vanilla reader. Without the registered Conversion the
write would fail (a ByteBuffer/byte[]
@@ -120,6 +156,168 @@ public class SegmentProcessorAvroUtilsTest {
}
}
+ /// The generated Avro schema must describe the *logical* Pinot type.
Switching on the stored type instead emitted
+ /// `int` for BOOLEAN, a bare `long` for TIMESTAMP, and rejected BIG_DECIMAL
outright.
+ @Test
+ public void testConvertPinotSchemaToAvroSchemaUsesLogicalTypes() {
+ Schema avroSchema =
SegmentProcessorAvroUtils.convertPinotSchemaToAvroSchema(allTypesPinotSchema());
+
+ assertFieldType(avroSchema, "intSv", Schema.Type.INT, null);
+ assertFieldType(avroSchema, "longSv", Schema.Type.LONG, null);
+ assertFieldType(avroSchema, "floatSv", Schema.Type.FLOAT, null);
+ assertFieldType(avroSchema, "doubleSv", Schema.Type.DOUBLE, null);
+ assertFieldType(avroSchema, "boolSv", Schema.Type.BOOLEAN, null);
+ assertFieldType(avroSchema, "tsSv", Schema.Type.LONG, "timestamp-millis");
+ assertFieldType(avroSchema, "bigDecimalSv", Schema.Type.BYTES,
"big-decimal");
+ assertFieldType(avroSchema, "stringSv", Schema.Type.STRING, null);
+ assertFieldType(avroSchema, "jsonSv", Schema.Type.STRING, null);
+ assertFieldType(avroSchema, "bytesSv", Schema.Type.BYTES, null);
+ assertFieldType(avroSchema, "uuidSv", Schema.Type.STRING, "uuid");
+
+ assertElementType(avroSchema, "intMv", Schema.Type.INT, null);
+ assertElementType(avroSchema, "boolMv", Schema.Type.BOOLEAN, null);
+ assertElementType(avroSchema, "tsMv", Schema.Type.LONG,
"timestamp-millis");
+ assertElementType(avroSchema, "bigDecimalMv", Schema.Type.BYTES,
"big-decimal");
+ assertElementType(avroSchema, "stringMv", Schema.Type.STRING, null);
+ assertElementType(avroSchema, "bytesMv", Schema.Type.BYTES, null);
+ assertElementType(avroSchema, "uuidMv", Schema.Type.STRING, "uuid");
+ }
+
+ /// This mapping is duplicated in `AvroUtils.getAvroSchemaFromPinotSchema`
(pinot-avro-base), because neither module
+ /// can depend on the other, yet both feed the same writers and the same
shared data model — `PinotSegmentToAvro
+ /// Converter` even pairs one class's schema with the other's data model.
Any divergence would silently produce
+ /// records the writer cannot serialize, so pin the two together here.
+ @Test
+ public void testConvertPinotSchemaToAvroSchemaMatchesAvroUtils() {
+ org.apache.pinot.spi.data.Schema pinotSchema = allTypesPinotSchema();
+
+ Schema avroSchema =
SegmentProcessorAvroUtils.convertPinotSchemaToAvroSchema(pinotSchema);
+ Schema avroUtilsSchema =
AvroUtils.getAvroSchemaFromPinotSchema(pinotSchema);
+
+ assertEquals(avroSchema.getFields().size(),
avroUtilsSchema.getFields().size());
+ for (Schema.Field field : avroSchema.getFields()) {
+ Schema.Field avroUtilsField = avroUtilsSchema.getField(field.name());
+ assertNotNull(avroUtilsField, "AvroUtils is missing field: " +
field.name());
+ assertEquals(field.schema(), avroUtilsField.schema(), "schema mismatch
for field: " + field.name());
+ }
+ }
+
+ /// Full ingest round trip for every supported type, SV and MV: Pinot's
internal (stored) values are written through
+ /// the shared data model, read back with the production `AvroRecordReader`,
run through the same data-type
+ /// transformation ingestion applies, and must land on the values we started
with.
+ @Test
+ public void testAllTypesRoundTripThroughAvroDataModel()
+ throws Exception {
+ org.apache.pinot.spi.data.Schema pinotSchema = allTypesPinotSchema();
+ Schema avroSchema =
SegmentProcessorAvroUtils.convertPinotSchemaToAvroSchema(pinotSchema);
+
+ byte[] uuidBytes =
UuidUtils.toBytes("12345678-1234-1234-1234-1234567890ab");
+ byte[] otherUuidBytes =
UuidUtils.toBytes("550e8400-e29b-41d4-a716-446655440000");
+ byte[] rawBytes = {1, 2, 3, 4};
+ // A scale the value's own magnitude does not imply, to prove
`big-decimal` carries the scale per value rather
+ // than pinning one for the column the way `decimal(precision, scale)`
would.
+ BigDecimal bigDecimal = new BigDecimal("123.45000");
+ BigDecimal hugeBigDecimal = new
BigDecimal("-9999999999999999999999.12345");
+
+ GenericRow row = new GenericRow();
+ row.putValue("intSv", 1);
+ row.putValue("longSv", 2L);
+ row.putValue("floatSv", 3.0f);
+ row.putValue("doubleSv", 4.0);
+ // BOOLEAN and TIMESTAMP arrive in Pinot's stored form: int 0/1 and epoch
millis.
+ row.putValue("boolSv", 1);
+ row.putValue("tsSv", 1609491661001L);
+ row.putValue("bigDecimalSv", bigDecimal);
+ row.putValue("stringSv", "5");
+ row.putValue("jsonSv", "{\"a\":1}");
+ row.putValue("bytesSv", rawBytes);
+ row.putValue("uuidSv", uuidBytes);
+ row.putValue("intMv", new Object[]{7, 8});
+ row.putValue("longMv", new Object[]{9L, 10L});
+ row.putValue("floatMv", new Object[]{11.0f, 12.0f});
+ row.putValue("doubleMv", new Object[]{13.0, 14.0});
+ row.putValue("boolMv", new Object[]{0, 1});
+ row.putValue("tsMv", new Object[]{1609491661001L, 0L});
+ row.putValue("bigDecimalMv", new Object[]{bigDecimal, hugeBigDecimal});
+ row.putValue("stringMv", new Object[]{"15", "16"});
+ row.putValue("bytesMv", new Object[]{rawBytes, new byte[]{9}});
+ row.putValue("uuidMv", new Object[]{uuidBytes, otherUuidBytes});
+
+ File tmpDir = Files.createTempDirectory("allTypesRoundTrip").toFile();
+ try {
+ File avroFile = new File(tmpDir, "data.avro");
+ GenericData.Record record =
+ SegmentProcessorAvroUtils.convertGenericRowToAvroRecord(row, new
GenericData.Record(avroSchema));
+ try (DataFileWriter<GenericData.Record> writer =
+ new DataFileWriter<>(new GenericDatumWriter<>(avroSchema,
SegmentProcessorAvroUtils.getAvroDataModel()))) {
+ writer.create(avroSchema, avroFile);
+ writer.append(record);
+ }
+
+ GenericRow readRow;
+ try (AvroRecordReader recordReader = new AvroRecordReader()) {
+ recordReader.init(avroFile, pinotSchema.getColumnNames(), null);
+ assertTrue(recordReader.hasNext());
+ readRow = recordReader.next();
+ }
+ // Apply the same data-type transformation ingestion runs, so the
comparison is against Pinot's stored form.
+ for (FieldSpec fieldSpec : pinotSchema.getAllFieldSpecs()) {
+ String column = fieldSpec.getName();
+ readRow.putValue(column,
DataTypeTransformerUtils.transformValue(column, readRow.getValue(column),
+ PinotDataType.getPinotDataTypeForIngestion(fieldSpec)));
+ }
+
+ assertEquals(readRow.getValue("intSv"), 1);
+ assertEquals(readRow.getValue("longSv"), 2L);
+ assertEquals(readRow.getValue("floatSv"), 3.0f);
+ assertEquals(readRow.getValue("doubleSv"), 4.0);
+ assertEquals(readRow.getValue("boolSv"), 1);
+ assertEquals(readRow.getValue("tsSv"), 1609491661001L);
+ assertEquals(readRow.getValue("bigDecimalSv"), bigDecimal);
+ assertEquals(((BigDecimal) readRow.getValue("bigDecimalSv")).scale(),
bigDecimal.scale(),
+ "big-decimal must preserve the value's own scale");
+ assertEquals(readRow.getValue("stringSv"), "5");
+ assertEquals(readRow.getValue("jsonSv"), "{\"a\":1}");
+ assertEquals((byte[]) readRow.getValue("bytesSv"), rawBytes);
+ assertEquals((byte[]) readRow.getValue("uuidSv"), uuidBytes);
+ assertEquals((Object[]) readRow.getValue("intMv"), new Object[]{7, 8});
+ assertEquals((Object[]) readRow.getValue("longMv"), new Object[]{9L,
10L});
+ assertEquals((Object[]) readRow.getValue("floatMv"), new Object[]{11.0f,
12.0f});
+ assertEquals((Object[]) readRow.getValue("doubleMv"), new Object[]{13.0,
14.0});
+ assertEquals((Object[]) readRow.getValue("boolMv"), new Object[]{0, 1});
+ assertEquals((Object[]) readRow.getValue("tsMv"), new
Object[]{1609491661001L, 0L});
+ assertEquals((Object[]) readRow.getValue("bigDecimalMv"), new
Object[]{bigDecimal, hugeBigDecimal});
+ assertEquals((Object[]) readRow.getValue("stringMv"), new Object[]{"15",
"16"});
+ // byte[] elements need element-wise comparison — Object[] equality
would compare them by identity.
+ assertBytesArrayEquals(readRow.getValue("bytesMv"), rawBytes, new
byte[]{9});
+ assertBytesArrayEquals(readRow.getValue("uuidMv"), uuidBytes,
otherUuidBytes);
+ } finally {
+ FileUtils.deleteDirectory(tmpDir);
+ }
+ }
+
+ /// BOOLEAN is the one logical type with no Avro logical type to carry a
`Conversion`, so the stored int 0/1 has to
+ /// be coerced to Boolean when building the record — both for single values
and for multi-value elements.
+ @Test
+ public void testConvertGenericRowToAvroRecordCoercesStoredBooleans() {
+ org.apache.pinot.spi.data.Schema pinotSchema = new
org.apache.pinot.spi.data.Schema.SchemaBuilder()
+ .setSchemaName("boolSchema")
+ .addSingleValueDimension("boolSv", DataType.BOOLEAN)
+ .addMultiValueDimension("boolMv", DataType.BOOLEAN)
+ .build();
+ Schema avroSchema =
SegmentProcessorAvroUtils.convertPinotSchemaToAvroSchema(pinotSchema);
+
+ GenericRow row = new GenericRow();
+ row.putValue("boolSv", 0);
+ row.putValue("boolMv", new Object[]{1, 0});
+
+ GenericData.Record record =
+ SegmentProcessorAvroUtils.convertGenericRowToAvroRecord(row, new
GenericData.Record(avroSchema));
+
+ assertEquals(record.get("boolSv"), Boolean.FALSE);
+ assertEquals(record.get("boolMv"), List.of(Boolean.TRUE, Boolean.FALSE));
+ }
+
/// convertPinotSchemaToAvroSchema must emit SV UUID as
string{logicalType:uuid} and MV UUID as
/// array<string{logicalType:uuid}>, which is what the uuid Conversion above
pairs with.
@Test
@@ -144,4 +342,64 @@ public class SegmentProcessorAvroUtilsTest {
assertEquals(LogicalTypes.fromSchemaIgnoreInvalid(mvElementSchema),
LogicalTypes.uuid(),
"MV UUID elements must carry the uuid logical type");
}
+
+ /// One column per supported data type, single-value and (where Pinot allows
it) multi-value.
+ private static org.apache.pinot.spi.data.Schema allTypesPinotSchema() {
+ return new org.apache.pinot.spi.data.Schema.SchemaBuilder()
+ .setSchemaName("allTypes")
+ .addSingleValueDimension("intSv", DataType.INT)
+ .addSingleValueDimension("longSv", DataType.LONG)
+ .addSingleValueDimension("floatSv", DataType.FLOAT)
+ .addSingleValueDimension("doubleSv", DataType.DOUBLE)
+ .addSingleValueDimension("boolSv", DataType.BOOLEAN)
+ .addSingleValueDimension("tsSv", DataType.TIMESTAMP)
+ .addSingleValueDimension("bigDecimalSv", DataType.BIG_DECIMAL)
+ .addSingleValueDimension("stringSv", DataType.STRING)
+ .addSingleValueDimension("jsonSv", DataType.JSON)
+ .addSingleValueDimension("bytesSv", DataType.BYTES)
+ .addSingleValueDimension("uuidSv", DataType.UUID)
+ .addMultiValueDimension("intMv", DataType.INT)
+ .addMultiValueDimension("longMv", DataType.LONG)
+ .addMultiValueDimension("floatMv", DataType.FLOAT)
+ .addMultiValueDimension("doubleMv", DataType.DOUBLE)
+ .addMultiValueDimension("boolMv", DataType.BOOLEAN)
+ .addMultiValueDimension("tsMv", DataType.TIMESTAMP)
+ .addMultiValueDimension("bigDecimalMv", DataType.BIG_DECIMAL)
+ .addMultiValueDimension("stringMv", DataType.STRING)
+ .addMultiValueDimension("bytesMv", DataType.BYTES)
+ .addMultiValueDimension("uuidMv", DataType.UUID)
+ .build();
+ }
+
+ private static void assertBytesArrayEquals(Object actual, byte[]...
expected) {
+ byte[][] actualArray = (byte[][]) actual;
+ assertEquals(actualArray.length, expected.length);
+ for (int i = 0; i < expected.length; i++) {
+ assertEquals(actualArray[i], expected[i], "mismatch at index " + i);
+ }
+ }
+
+ private static void assertFieldType(Schema avroSchema, String field,
Schema.Type expectedType,
+ @Nullable String expectedLogicalType) {
+ assertAvroType(avroSchema.getField(field).schema(), field, expectedType,
expectedLogicalType);
+ }
+
+ private static void assertElementType(Schema avroSchema, String field,
Schema.Type expectedType,
+ @Nullable String expectedLogicalType) {
+ Schema fieldSchema = avroSchema.getField(field).schema();
+ assertEquals(fieldSchema.getType(), Schema.Type.ARRAY, field + " must be
an array");
+ assertAvroType(fieldSchema.getElementType(), field, expectedType,
expectedLogicalType);
+ }
+
+ private static void assertAvroType(Schema schema, String field, Schema.Type
expectedType,
+ @Nullable String expectedLogicalType) {
+ assertEquals(schema.getType(), expectedType, "unexpected Avro type for " +
field);
+ LogicalType logicalType = LogicalTypes.fromSchemaIgnoreInvalid(schema);
+ if (expectedLogicalType == null) {
+ assertNull(logicalType, field + " must carry no logical type");
+ } else {
+ assertNotNull(logicalType, field + " must carry the " +
expectedLogicalType + " logical type");
+ assertEquals(logicalType.getName(), expectedLogicalType, "unexpected
logical type for " + field);
+ }
+ }
}
diff --git
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
index 099ccae9170..11bb628a57f 100644
---
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
+++
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtil.java
@@ -20,6 +20,7 @@ package org.apache.pinot.plugin.inputformat.avro;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.IOException;
import org.apache.avro.LogicalType;
import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema;
@@ -38,6 +39,70 @@ public class AvroSchemaUtil {
// in AvroRecordExtractor; this class only deals with schema-shape mapping.
private static final String UUID = "uuid";
+ /// Returns the Avro schema for a single value of the given Pinot
[DataType]. This is the canonical Pinot-to-Avro
+ /// type mapping; it is driven by the **original (logical)** data type
rather than the stored type, so logical types
+ /// stay self-describing in the generated Avro schema instead of collapsing
to their physical storage type.
+ ///
+ /// | Pinot type | Avro type | Value handed to the Avro
writer |
+ ///
|---------------|--------------------------|----------------------------------------------------------|
+ /// | INT | `int` | `Integer`
|
+ /// | LONG | `long` | `Long`
|
+ /// | FLOAT | `float` | `Float`
|
+ /// | DOUBLE | `double` | `Double`
|
+ /// | BOOLEAN | `boolean` | `Boolean` (Pinot stores
`int` 0/1 — see note below) |
+ /// | TIMESTAMP | `long{timestamp-millis}` | `Long` millis since epoch
(the logical type's base type) |
+ /// | BIG_DECIMAL | `bytes{big-decimal}` | [java.math.BigDecimal] via
Avro's `BigDecimalConversion` |
+ /// | STRING / JSON | `string` | `String`
|
+ /// | BYTES | `bytes` | [java.nio.ByteBuffer]
|
+ /// | UUID | `string{uuid}` | 16-byte `byte[]` via a
`uuid` `Conversion` |
+ ///
+ /// `timestamp-millis` needs no write-side conversion because `Long` *is*
that logical type's base representation.
+ /// `big-decimal` is used for BIG_DECIMAL rather than `decimal(precision,
scale)` because Pinot does not pin a
+ /// per-column precision/scale, and `big-decimal` encodes the scale with
every value. BOOLEAN is the one type with
+ /// no Avro logical type to hang a `Conversion` on, so writers must coerce
Pinot's stored `int` 0/1 to `Boolean`
+ /// themselves (see
`SegmentProcessorAvroUtils#convertGenericRowToAvroRecord` and `AvroWriter`).
+ ///
+ /// This mapping is intentionally one-way: [#valueOf(Schema)] does **not**
map `timestamp-millis` back to TIMESTAMP
+ /// or `big-decimal` back to BIG_DECIMAL, so that Pinot schema inference
from existing Avro data keeps its
+ /// long-standing behavior.
+ ///
+ /// Throws [UnsupportedOperationException] for types with no Avro
representation (STRUCT / MAP / OPEN_STRUCT /
+ /// LIST / UNKNOWN).
+ public static Schema toAvroSchema(DataType dataType) {
+ switch (dataType) {
+ case INT:
+ return Schema.create(Schema.Type.INT);
+ case LONG:
+ return Schema.create(Schema.Type.LONG);
+ case FLOAT:
+ return Schema.create(Schema.Type.FLOAT);
+ case DOUBLE:
+ return Schema.create(Schema.Type.DOUBLE);
+ case BOOLEAN:
+ return Schema.create(Schema.Type.BOOLEAN);
+ case TIMESTAMP:
+ return
LogicalTypes.timestampMillis().addToSchema(Schema.create(Schema.Type.LONG));
+ case BIG_DECIMAL:
+ return
LogicalTypes.bigDecimal().addToSchema(Schema.create(Schema.Type.BYTES));
+ case STRING:
+ case JSON:
+ return Schema.create(Schema.Type.STRING);
+ case BYTES:
+ return Schema.create(Schema.Type.BYTES);
+ case UUID:
+ return
LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING));
+ default:
+ throw new UnsupportedOperationException("Unsupported data type: " +
dataType);
+ }
+ }
+
+ /// Returns the Avro schema for a whole Pinot column:
[#toAvroSchema(DataType)] for a single-value field, or an
+ /// array of it for a multi-value field.
+ public static Schema toAvroSchema(FieldSpec fieldSpec) {
+ Schema valueSchema = toAvroSchema(fieldSpec.getDataType());
+ return fieldSpec.isSingleValueField() ? valueSchema :
Schema.createArray(valueSchema);
+ }
+
/// Returns the Pinot data type for a bare Avro type. This does not honor
logical types (e.g. a `string` or `fixed`
/// carrying `logicalType:uuid` maps to STRING/BYTES, not UUID); prefer
[#valueOf(Schema)] when a full [Schema] is
/// available.
@@ -104,76 +169,26 @@ public class AvroSchemaUtil {
}
}
- /// Builds the Avro schema JSON for a single Pinot field. Used to generate
sample Avro data from a Pinot schema
- /// (see `AvroWriter`). Each field is emitted as a nullable union `["null",
<type>]`.
- ///
- /// The switch is driven by the original (logical) [DataType] rather than
the stored type, so logical types are
- /// represented faithfully instead of collapsing to their physical storage
type: BOOLEAN maps to Avro `boolean`,
- /// TIMESTAMP to a `timestamp-millis` long (not a plain `int`/`long`), and
UUID to a `uuid`-logical-type string
- /// (not raw `bytes`).
+ /// Builds the Avro schema JSON for a single Pinot field, as a nullable
union `["null", <type>]`. Used to generate
+ /// sample Avro data from a Pinot schema (see `AvroWriter`).
///
- /// This intentionally differs from the segment-processing converters
`AvroUtils.getAvroSchemaFromPinotSchema` and
- /// `SegmentProcessorAvroUtils.convertPinotSchemaToAvroSchema`, which switch
on the stored type because they
- /// serialize Pinot's physically-stored values (e.g. an int for BOOLEAN)
directly.
+ /// The `<type>` branch is [#toAvroSchema(DataType)] rendered as JSON, so
this shares the single logical-type
+ /// mapping documented there. The field is always emitted as the
single-value type even for a multi-value
+ /// [FieldSpec] — the data generator backing `AvroWriter` has never produced
Avro arrays.
public static ObjectNode toAvroSchemaJsonObject(FieldSpec fieldSpec) {
ObjectNode jsonSchema = JsonUtils.newObjectNode();
jsonSchema.put("name", fieldSpec.getName());
- DataType dataType = fieldSpec.getDataType();
- switch (dataType) {
- case INT:
- jsonSchema.set("type", convertStringsToJsonArray("null", "int"));
- return jsonSchema;
- case LONG:
- jsonSchema.set("type", convertStringsToJsonArray("null", "long"));
- return jsonSchema;
- case FLOAT:
- jsonSchema.set("type", convertStringsToJsonArray("null", "float"));
- return jsonSchema;
- case DOUBLE:
- jsonSchema.set("type", convertStringsToJsonArray("null", "double"));
- return jsonSchema;
- case BOOLEAN:
- jsonSchema.set("type", convertStringsToJsonArray("null", "boolean"));
- return jsonSchema;
- case TIMESTAMP:
- // TIMESTAMP is stored as LONG millis-since-epoch; annotate the long
branch with the timestamp-millis
- // logical type so the value stays a long but is self-describing as a
timestamp.
- ObjectNode timestampType = JsonUtils.newObjectNode();
- timestampType.put("type", "long");
- timestampType.put("logicalType", "timestamp-millis");
- ArrayNode timestampUnion = JsonUtils.newArrayNode();
- timestampUnion.add("null");
- timestampUnion.add(timestampType);
- jsonSchema.set("type", timestampUnion);
- return jsonSchema;
- case STRING:
- case JSON:
- jsonSchema.set("type", convertStringsToJsonArray("null", "string"));
- return jsonSchema;
- case UUID:
- // UUID is a logical type; represent it faithfully as an Avro string
annotated with logicalType "uuid" rather
- // than collapsing to raw bytes, so generated sample data round-trips
as canonical UUID strings.
- ObjectNode uuidType = JsonUtils.newObjectNode();
- uuidType.put("type", "string");
- uuidType.put("logicalType", "uuid");
- ArrayNode uuidUnion = JsonUtils.newArrayNode();
- uuidUnion.add("null");
- uuidUnion.add(uuidType);
- jsonSchema.set("type", uuidUnion);
- return jsonSchema;
- case BYTES:
- jsonSchema.set("type", convertStringsToJsonArray("null", "bytes"));
- return jsonSchema;
- default:
- throw new UnsupportedOperationException("Unsupported data type: " +
dataType);
- }
- }
-
- private static ArrayNode convertStringsToJsonArray(String... strings) {
- ArrayNode jsonArray = JsonUtils.newArrayNode();
- for (String string : strings) {
- jsonArray.add(string);
+ ArrayNode nullableUnion = JsonUtils.newArrayNode();
+ nullableUnion.add("null");
+ // Schema.toString() emits valid JSON: a bare name for primitives ("int"),
an object for logical types
+ // ({"type":"long","logicalType":"timestamp-millis"}).
+ try {
+
nullableUnion.add(JsonUtils.stringToJsonNode(toAvroSchema(fieldSpec.getDataType()).toString()));
+ } catch (IOException e) {
+ throw new IllegalStateException("Caught exception while parsing the Avro
schema generated for field: "
+ + fieldSpec.getName(), e);
}
- return jsonArray;
+ jsonSchema.set("type", nullableUnion);
+ return jsonSchema;
}
}
diff --git
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroUtils.java
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroUtils.java
index b01916df360..189657dbd5e 100644
---
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroUtils.java
+++
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/main/java/org/apache/pinot/plugin/inputformat/avro/AvroUtils.java
@@ -27,7 +27,6 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;
import javax.annotation.Nullable;
-import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema.Field;
import org.apache.avro.SchemaBuilder;
import org.apache.avro.file.DataFileStream;
@@ -151,73 +150,22 @@ public class AvroUtils {
}
}
- /**
- * Helper method to build Avro schema from Pinot schema.
- *
- * @param pinotSchema Pinot schema.
- * @return Avro schema.
- */
+ /// Builds an Avro schema from a Pinot schema, one non-nullable field per
[FieldSpec] in schema order.
+ ///
+ /// Field types come from [AvroSchemaUtil#toAvroSchema(FieldSpec)], which
maps the **original (logical)** Pinot data
+ /// type — so BOOLEAN becomes Avro `boolean`, TIMESTAMP a `timestamp-millis`
long, BIG_DECIMAL a `big-decimal`
+ /// bytes and UUID a `uuid` string, instead of all four collapsing to their
physical storage type. See that method
+ /// for the full mapping table and for the value representation each Avro
type expects.
+ ///
+ /// Rows are written into this schema by
`SegmentProcessorAvroUtils.convertGenericRowToAvroRecord` using the data
+ /// model returned by `SegmentProcessorAvroUtils.getAvroDataModel()`, which
registers the matching logical-type
+ /// conversions; the two must stay in sync.
public static org.apache.avro.Schema getAvroSchemaFromPinotSchema(Schema
pinotSchema) {
SchemaBuilder.FieldAssembler<org.apache.avro.Schema> fieldAssembler =
SchemaBuilder.record("record").fields();
-
for (FieldSpec fieldSpec : pinotSchema.getAllFieldSpecs()) {
- if (fieldSpec.getDataType() == DataType.UUID) {
- org.apache.avro.Schema uuidSchema =
LogicalTypes.uuid().addToSchema(org.apache.avro.Schema.create(
- org.apache.avro.Schema.Type.STRING));
- if (fieldSpec.isSingleValueField()) {
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type(uuidSchema).noDefault();
- } else {
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().array().items(uuidSchema).noDefault();
- }
- continue;
- }
- DataType storedType = fieldSpec.getDataType().getStoredType();
- if (fieldSpec.isSingleValueField()) {
- switch (storedType) {
- case INT:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().intType().noDefault();
- break;
- case LONG:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().longType().noDefault();
- break;
- case FLOAT:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().floatType().noDefault();
- break;
- case DOUBLE:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().doubleType().noDefault();
- break;
- case STRING:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().stringType().noDefault();
- break;
- case BYTES:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().bytesType().noDefault();
- break;
- default:
- throw new RuntimeException("Unsupported data type: " + storedType);
- }
- } else {
- switch (storedType) {
- case INT:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().array().items().intType().noDefault();
- break;
- case LONG:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().array().items().longType().noDefault();
- break;
- case FLOAT:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().array().items().floatType().noDefault();
- break;
- case DOUBLE:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().array().items().doubleType().noDefault();
- break;
- case STRING:
- fieldAssembler =
fieldAssembler.name(fieldSpec.getName()).type().array().items().stringType().noDefault();
- break;
- default:
- throw new RuntimeException("Unsupported data type: " + storedType);
- }
- }
+ fieldAssembler =
+
fieldAssembler.name(fieldSpec.getName()).type(AvroSchemaUtil.toAvroSchema(fieldSpec)).noDefault();
}
-
return fieldAssembler.endRecord();
}
diff --git
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
index 436e3d8c68f..a899d4efb69 100644
---
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
+++
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroSchemaUtilTest.java
@@ -31,7 +31,12 @@ import static org.testng.Assert.assertThrows;
public class AvroSchemaUtilTest {
- /// The switch must be driven by the original (logical) data type, not the
stored type. Otherwise BOOLEAN collapses
+ /// Every Pinot data type that has an Avro representation.
+ private static final DataType[] SUPPORTED_DATA_TYPES =
+ {DataType.INT, DataType.LONG, DataType.FLOAT, DataType.DOUBLE,
DataType.BOOLEAN, DataType.TIMESTAMP,
+ DataType.BIG_DECIMAL, DataType.STRING, DataType.JSON,
DataType.BYTES, DataType.UUID};
+
+ /// The mapping must be driven by the original (logical) data type, not the
stored type. Otherwise BOOLEAN collapses
/// to "int" and TIMESTAMP to a plain "long", misrepresenting the column in
the generated Avro schema.
@Test
public void testToAvroSchemaJsonObjectUsesOriginalType() {
@@ -44,28 +49,50 @@ public class AvroSchemaUtilTest {
assertPrimitiveType(DataType.BYTES, "bytes");
// Logical types must not collapse to their stored INT/LONG type.
assertPrimitiveType(DataType.BOOLEAN, "boolean");
+ assertLogicalType(DataType.TIMESTAMP, "long", "timestamp-millis");
+ assertLogicalType(DataType.BIG_DECIMAL, "bytes", "big-decimal");
+ }
- JsonNode type = typeOf(DataType.TIMESTAMP);
- assertEquals(type.get(0).asText(), "null");
- JsonNode timestampBranch = type.get(1);
- assertEquals(timestampBranch.get("type").asText(), "long");
- assertEquals(timestampBranch.get("logicalType").asText(),
"timestamp-millis");
+ /// UUID is a logical type; a single-value column maps to an Avro string
carrying the "uuid" logical type.
+ @Test
+ public void testToAvroSchemaJsonObjectForUuid() {
+ assertLogicalType(DataType.UUID, "string", "uuid");
}
- /// Types with no Avro mapping (e.g. BIG_DECIMAL) must be rejected rather
than silently mishandled.
+ /// The Avro-schema and JSON forms are two views of one mapping, so they
must agree for every supported type.
@Test
- public void testToAvroSchemaJsonObjectRejectsUnsupportedType() {
- assertThrows(UnsupportedOperationException.class,
- () -> AvroSchemaUtil.toAvroSchemaJsonObject(new
DimensionFieldSpec("col", DataType.BIG_DECIMAL, true)));
+ public void testToAvroSchemaJsonObjectMatchesToAvroSchema() {
+ for (DataType dataType : SUPPORTED_DATA_TYPES) {
+ JsonNode type = typeOf(dataType);
+ assertEquals(type.get(1).toString(),
AvroSchemaUtil.toAvroSchema(dataType).toString(),
+ "mismatch for " + dataType);
+ }
}
- /// UUID is a logical type; a single-value column maps to an Avro string
carrying the "uuid" logical type.
+ /// Single-value columns map to the bare value schema; multi-value columns
to an array of it.
@Test
- public void testToAvroSchemaJsonObjectForUuid() {
- JsonNode type = typeOf(DataType.UUID);
- assertEquals(type.get(0).asText(), "null");
- assertEquals(type.get(1).get("type").asText(), "string");
- assertEquals(type.get(1).get("logicalType").asText(), "uuid");
+ public void testToAvroSchemaHonorsMultiValue() {
+ for (DataType dataType : SUPPORTED_DATA_TYPES) {
+ Schema valueSchema = AvroSchemaUtil.toAvroSchema(dataType);
+ assertEquals(AvroSchemaUtil.toAvroSchema(new DimensionFieldSpec("col",
dataType, true)), valueSchema,
+ "SV mismatch for " + dataType);
+ if (dataType == DataType.JSON) {
+ // JSON has no multi-value form in Pinot.
+ continue;
+ }
+ Schema mvSchema = AvroSchemaUtil.toAvroSchema(new
DimensionFieldSpec("col", dataType, false));
+ assertEquals(mvSchema.getType(), Schema.Type.ARRAY, "MV mismatch for " +
dataType);
+ assertEquals(mvSchema.getElementType(), valueSchema, "MV element
mismatch for " + dataType);
+ }
+ }
+
+ /// Types with no Avro representation must be rejected rather than silently
mishandled.
+ @Test
+ public void testToAvroSchemaRejectsUnsupportedType() {
+ for (DataType dataType : new DataType[]{DataType.MAP, DataType.STRUCT,
DataType.OPEN_STRUCT, DataType.LIST,
+ DataType.UNKNOWN}) {
+ assertThrows(UnsupportedOperationException.class, () ->
AvroSchemaUtil.toAvroSchema(dataType));
+ }
}
@Test
@@ -109,6 +136,14 @@ public class AvroSchemaUtilTest {
assertEquals(type.get(1).asText(), expectedAvroType);
}
+ private static void assertLogicalType(DataType dataType, String
expectedAvroType, String expectedLogicalType) {
+ JsonNode type = typeOf(dataType);
+ assertEquals(type.get(0).asText(), "null");
+ JsonNode branch = type.get(1);
+ assertEquals(branch.get("type").asText(), expectedAvroType);
+ assertEquals(branch.get("logicalType").asText(), expectedLogicalType);
+ }
+
private static JsonNode typeOf(DataType dataType) {
ObjectNode jsonSchema = AvroSchemaUtil.toAvroSchemaJsonObject(new
DimensionFieldSpec("col", dataType, true));
assertEquals(jsonSchema.get("name").asText(), "col");
diff --git
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroUtilsTest.java
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroUtilsTest.java
index 704de6ca58a..1250cc48120 100644
---
a/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroUtilsTest.java
+++
b/pinot-plugins/pinot-input-format/pinot-avro-base/src/test/java/org/apache/pinot/plugin/inputformat/avro/AvroUtilsTest.java
@@ -22,6 +22,8 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import org.apache.avro.LogicalType;
import org.apache.avro.LogicalTypes;
import org.apache.pinot.spi.config.table.ingestion.ComplexTypeConfig;
import org.apache.pinot.spi.data.FieldSpec;
@@ -32,6 +34,8 @@ import org.testng.annotations.Test;
import org.testng.collections.Lists;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
public class AvroUtilsTest {
@@ -160,6 +164,86 @@ public class AvroUtilsTest {
assertEquals(fieldSchema.getLogicalType().getName(), "uuid");
}
+ /// The generated Avro schema must describe the *logical* Pinot type.
Switching on the stored type instead emitted
+ /// `int` for BOOLEAN, a bare `long` for TIMESTAMP, and rejected BIG_DECIMAL
outright.
+ @Test
+ public void testGetAvroSchemaFromPinotSchemaUsesLogicalTypes() {
+ Schema pinotSchema = new Schema.SchemaBuilder()
+ .addSingleValueDimension("intCol", DataType.INT)
+ .addSingleValueDimension("longCol", DataType.LONG)
+ .addSingleValueDimension("floatCol", DataType.FLOAT)
+ .addSingleValueDimension("doubleCol", DataType.DOUBLE)
+ .addSingleValueDimension("boolCol", DataType.BOOLEAN)
+ .addSingleValueDimension("tsCol", DataType.TIMESTAMP)
+ .addSingleValueDimension("bigDecimalCol", DataType.BIG_DECIMAL)
+ .addSingleValueDimension("stringCol", DataType.STRING)
+ .addSingleValueDimension("jsonCol", DataType.JSON)
+ .addSingleValueDimension("bytesCol", DataType.BYTES)
+ .addSingleValueDimension("uuidCol", DataType.UUID)
+ .build();
+
+ org.apache.avro.Schema avroSchema =
AvroUtils.getAvroSchemaFromPinotSchema(pinotSchema);
+
+ assertFieldType(avroSchema, "intCol", org.apache.avro.Schema.Type.INT,
null);
+ assertFieldType(avroSchema, "longCol", org.apache.avro.Schema.Type.LONG,
null);
+ assertFieldType(avroSchema, "floatCol", org.apache.avro.Schema.Type.FLOAT,
null);
+ assertFieldType(avroSchema, "doubleCol",
org.apache.avro.Schema.Type.DOUBLE, null);
+ assertFieldType(avroSchema, "boolCol",
org.apache.avro.Schema.Type.BOOLEAN, null);
+ assertFieldType(avroSchema, "tsCol", org.apache.avro.Schema.Type.LONG,
"timestamp-millis");
+ assertFieldType(avroSchema, "bigDecimalCol",
org.apache.avro.Schema.Type.BYTES, "big-decimal");
+ assertFieldType(avroSchema, "stringCol",
org.apache.avro.Schema.Type.STRING, null);
+ assertFieldType(avroSchema, "jsonCol", org.apache.avro.Schema.Type.STRING,
null);
+ assertFieldType(avroSchema, "bytesCol", org.apache.avro.Schema.Type.BYTES,
null);
+ assertFieldType(avroSchema, "uuidCol", org.apache.avro.Schema.Type.STRING,
"uuid");
+ }
+
+ /// Multi-value columns become arrays of the same value schema. Before the
fix the MV switch only covered
+ /// INT/LONG/FLOAT/DOUBLE/STRING, so MV BYTES, BIG_DECIMAL, BOOLEAN and
TIMESTAMP all threw or lost their type.
+ @Test
+ public void testGetAvroSchemaFromPinotSchemaForMultiValueColumns() {
+ Schema pinotSchema = new Schema.SchemaBuilder()
+ .addMultiValueDimension("intCol", DataType.INT)
+ .addMultiValueDimension("boolCol", DataType.BOOLEAN)
+ .addMultiValueDimension("tsCol", DataType.TIMESTAMP)
+ .addMultiValueDimension("bigDecimalCol", DataType.BIG_DECIMAL)
+ .addMultiValueDimension("bytesCol", DataType.BYTES)
+ .addMultiValueDimension("uuidCol", DataType.UUID)
+ .build();
+
+ org.apache.avro.Schema avroSchema =
AvroUtils.getAvroSchemaFromPinotSchema(pinotSchema);
+
+ assertElementType(avroSchema, "intCol", org.apache.avro.Schema.Type.INT,
null);
+ assertElementType(avroSchema, "boolCol",
org.apache.avro.Schema.Type.BOOLEAN, null);
+ assertElementType(avroSchema, "tsCol", org.apache.avro.Schema.Type.LONG,
"timestamp-millis");
+ assertElementType(avroSchema, "bigDecimalCol",
org.apache.avro.Schema.Type.BYTES, "big-decimal");
+ assertElementType(avroSchema, "bytesCol",
org.apache.avro.Schema.Type.BYTES, null);
+ assertElementType(avroSchema, "uuidCol",
org.apache.avro.Schema.Type.STRING, "uuid");
+ }
+
+ private static void assertFieldType(org.apache.avro.Schema avroSchema,
String field,
+ org.apache.avro.Schema.Type expectedType, @Nullable String
expectedLogicalType) {
+ assertAvroType(avroSchema.getField(field).schema(), field, expectedType,
expectedLogicalType);
+ }
+
+ private static void assertElementType(org.apache.avro.Schema avroSchema,
String field,
+ org.apache.avro.Schema.Type expectedType, @Nullable String
expectedLogicalType) {
+ org.apache.avro.Schema fieldSchema = avroSchema.getField(field).schema();
+ assertEquals(fieldSchema.getType(), org.apache.avro.Schema.Type.ARRAY,
field + " must be an array");
+ assertAvroType(fieldSchema.getElementType(), field, expectedType,
expectedLogicalType);
+ }
+
+ private static void assertAvroType(org.apache.avro.Schema schema, String
field,
+ org.apache.avro.Schema.Type expectedType, @Nullable String
expectedLogicalType) {
+ assertEquals(schema.getType(), expectedType, "unexpected Avro type for " +
field);
+ LogicalType logicalType = LogicalTypes.fromSchemaIgnoreInvalid(schema);
+ if (expectedLogicalType == null) {
+ assertNull(logicalType, field + " must carry no logical type");
+ } else {
+ assertNotNull(logicalType, field + " must carry the " +
expectedLogicalType + " logical type");
+ assertEquals(logicalType.getName(), expectedLogicalType, "unexpected
logical type for " + field);
+ }
+ }
+
@Test
public void testGetPinotSchemaFromAvroSchemaWithUuidArray() {
org.apache.avro.Schema uuidSchema =
diff --git
a/pinot-plugins/pinot-segment-writer/pinot-segment-writer-file-based/src/test/java/org/apache/pinot/plugin/segmentwriter/filebased/FileBasedSegmentWriterTest.java
b/pinot-plugins/pinot-segment-writer/pinot-segment-writer-file-based/src/test/java/org/apache/pinot/plugin/segmentwriter/filebased/FileBasedSegmentWriterTest.java
index 57d6f62feeb..3f249fdd679 100644
---
a/pinot-plugins/pinot-segment-writer/pinot-segment-writer-file-based/src/test/java/org/apache/pinot/plugin/segmentwriter/filebased/FileBasedSegmentWriterTest.java
+++
b/pinot-plugins/pinot-segment-writer/pinot-segment-writer-file-based/src/test/java/org/apache/pinot/plugin/segmentwriter/filebased/FileBasedSegmentWriterTest.java
@@ -21,7 +21,9 @@ package org.apache.pinot.plugin.segmentwriter.filebased;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import java.io.File;
+import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -29,6 +31,7 @@ import java.util.List;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.apache.pinot.common.utils.TarCompressionUtils;
+import org.apache.pinot.segment.local.segment.readers.PinotSegmentRecordReader;
import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
import org.apache.pinot.spi.config.table.TableConfig;
import org.apache.pinot.spi.config.table.TableType;
@@ -55,6 +58,10 @@ public class FileBasedSegmentWriterTest {
private static final String TABLE_NAME = "segmentWriter";
private static final String TIME_COLUMN_NAME = "aLong";
+ private static final long TIMESTAMP_VALUE = 1609491661001L;
+ // Beyond long precision and with a non-trivial scale, to prove the Avro
buffer's `big-decimal` logical type carries
+ // arbitrary precision and the value's own scale. Trailing zeros are avoided
because ingestion strips them.
+ private static final BigDecimal BIG_DECIMAL_VALUE = new
BigDecimal("-9999999999999999999999.12345");
private File _tmpDir;
private File _outputDir;
@@ -84,6 +91,8 @@ public class FileBasedSegmentWriterTest {
.addSingleValueDimension("anAdvancedMap_str",
FieldSpec.DataType.STRING)
.addSingleValueDimension("nullString", FieldSpec.DataType.STRING)
.addSingleValueDimension("aBoolean", FieldSpec.DataType.BOOLEAN)
+ .addSingleValueDimension("aTimestamp", FieldSpec.DataType.TIMESTAMP)
+ .addSingleValueDimension("aBigDecimal", FieldSpec.DataType.BIG_DECIMAL)
.addSingleValueDimension("aBytes", FieldSpec.DataType.BYTES)
.addMultiValueDimension("aStringList", FieldSpec.DataType.STRING)
.addMultiValueDimension("anIntList", FieldSpec.DataType.INT)
@@ -196,6 +205,41 @@ public class FileBasedSegmentWriterTest {
FileUtils.deleteQuietly(_outputDir);
}
+ /// The writer buffers rows in an Avro file and then builds the segment from
it, so every column has to survive a
+ /// full write/read round trip through the generated Avro schema and the
shared data model. This covers the logical
+ /// types that carry an Avro logical type (or, for BOOLEAN, a
stored-int-to-Boolean coercion) rather than being
+ /// written in their physical storage form.
+ @Test
+ public void testLogicalTypesRoundTripThroughAvroBuffer()
+ throws Exception {
+ FileUtils.deleteQuietly(_outputDir);
+ SegmentWriter segmentWriter = new FileBasedSegmentWriter();
+ segmentWriter.init(_tableConfig, _schema);
+ segmentWriter.collect(getGenericRow("record1", 1616238000000L));
+ segmentWriter.flush();
+ segmentWriter.close();
+
+ File segmentTar = new File(_outputDir,
"segmentWriter_1616238000000_1616238000000.tar.gz");
+ Assert.assertTrue(segmentTar.exists());
+ TarCompressionUtils.untar(segmentTar, _outputDir);
+ File segmentDir = new File(_outputDir,
"segmentWriter_1616238000000_1616238000000");
+
+ try (PinotSegmentRecordReader recordReader = new
PinotSegmentRecordReader(segmentDir)) {
+ Assert.assertTrue(recordReader.hasNext());
+ GenericRow row = recordReader.next();
+ // The segment stores Pinot's internal form: int 0/1 for BOOLEAN and
epoch millis for TIMESTAMP.
+ Assert.assertEquals(row.getValue("aBoolean"), 1);
+ Assert.assertEquals(row.getValue("aTimestamp"), TIMESTAMP_VALUE);
+ Assert.assertEquals(row.getValue("aBigDecimal"), BIG_DECIMAL_VALUE);
+ Assert.assertEquals(((BigDecimal) row.getValue("aBigDecimal")).scale(),
BIG_DECIMAL_VALUE.scale(),
+ "big-decimal must preserve the value's own scale");
+ Assert.assertEquals((byte[]) row.getValue("aBytes"),
"foo".getBytes(StandardCharsets.UTF_8));
+ Assert.assertEquals(row.getValue("aString"), "record1");
+ Assert.assertFalse(recordReader.hasNext());
+ }
+ FileUtils.deleteQuietly(_outputDir);
+ }
+
/**
* Tests flushing on empty collection
*/
@@ -381,6 +425,8 @@ public class FileBasedSegmentWriterTest {
row.putValue("aDouble", 10.5);
row.putValue("aFloat", 2.0);
row.putValue("aBoolean", true);
+ row.putValue("aTimestamp", new Timestamp(TIMESTAMP_VALUE));
+ row.putValue("aBigDecimal", BIG_DECIMAL_VALUE);
row.putValue("aBytes", "foo".getBytes(StandardCharsets.UTF_8));
List<String> stringList = new ArrayList<>();
stringList.add("a");
diff --git
a/pinot-tools/src/test/java/org/apache/pinot/tools/segment/converter/PinotSegmentConverterTest.java
b/pinot-tools/src/test/java/org/apache/pinot/tools/segment/converter/PinotSegmentConverterTest.java
index 666c06a4ba6..e6200c7e5b3 100644
---
a/pinot-tools/src/test/java/org/apache/pinot/tools/segment/converter/PinotSegmentConverterTest.java
+++
b/pinot-tools/src/test/java/org/apache/pinot/tools/segment/converter/PinotSegmentConverterTest.java
@@ -20,6 +20,8 @@ package org.apache.pinot.tools.segment.converter;
import java.io.File;
import java.io.IOException;
+import java.math.BigDecimal;
+import java.sql.Timestamp;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.pinot.plugin.inputformat.avro.AvroRecordReader;
@@ -264,6 +266,79 @@ public class PinotSegmentConverterTest {
}
}
+ private static final String BOOLEAN_SV_COLUMN = "boolSVColumn";
+ private static final String TIMESTAMP_SV_COLUMN = "tsSVColumn";
+ private static final String BIG_DECIMAL_SV_COLUMN = "bigDecimalSVColumn";
+ private static final String BOOLEAN_MV_COLUMN = "boolMVColumn";
+ private static final String TIMESTAMP_MV_COLUMN = "tsMVColumn";
+ private static final long TIMESTAMP_VALUE = 1609491661001L;
+ // Beyond long precision and with a non-trivial scale, to prove the exported
Avro `big-decimal` carries arbitrary
+ // precision and the value's own scale. Trailing zeros are deliberately
avoided: ingestion's SpecialValueTransformer
+ // strips them, so a value like "123.45000" would already be "123.45" by the
time it reaches the segment.
+ private static final BigDecimal BIG_DECIMAL_VALUE = new
BigDecimal("-9999999999999999999999.12345");
+
+ /// Builds a segment with BOOLEAN, TIMESTAMP and BIG_DECIMAL columns and
converts it through both the Avro and
+ /// Parquet converters. These are exported using their Avro logical types
(`boolean`, `long{timestamp-millis}` and
+ /// `bytes{big-decimal}`) rather than their stored types, so this covers the
stored-int-to-Boolean coercion and the
+ /// big-decimal Conversion on both write paths — including Parquet's
distinct AvroParquetWriter.withDataModel path.
+ /// Before the fix, BOOLEAN exported as a bare `int`, TIMESTAMP as a bare
`long`, and BIG_DECIMAL threw.
+ @Test
+ public void testLogicalTypeConverters()
+ throws Exception {
+ Schema logicalSchema = new Schema.SchemaBuilder()
+ .addSingleValueDimension(BOOLEAN_SV_COLUMN, DataType.BOOLEAN)
+ .addSingleValueDimension(TIMESTAMP_SV_COLUMN, DataType.TIMESTAMP)
+ .addSingleValueDimension(BIG_DECIMAL_SV_COLUMN, DataType.BIG_DECIMAL)
+ .addMultiValueDimension(BOOLEAN_MV_COLUMN, DataType.BOOLEAN)
+ .addMultiValueDimension(TIMESTAMP_MV_COLUMN, DataType.TIMESTAMP)
+ .build();
+ TableConfig logicalTableConfig = new
TableConfigBuilder(TableType.OFFLINE).setTableName("logicalTable").build();
+
+ GenericRow record = new GenericRow();
+ record.putValue(BOOLEAN_SV_COLUMN, true);
+ record.putValue(TIMESTAMP_SV_COLUMN, new Timestamp(TIMESTAMP_VALUE));
+ record.putValue(BIG_DECIMAL_SV_COLUMN, BIG_DECIMAL_VALUE);
+ record.putValue(BOOLEAN_MV_COLUMN, new Object[]{true, false});
+ record.putValue(TIMESTAMP_MV_COLUMN, new Object[]{new
Timestamp(TIMESTAMP_VALUE), new Timestamp(0L)});
+
+ SegmentGeneratorConfig config = new
SegmentGeneratorConfig(logicalTableConfig, logicalSchema);
+ config.setTableName("logicalTable");
+ config.setSegmentName("logicalSegment");
+ config.setOutDir(new File(TEMP_DIR, "logicalSegment").getPath());
+ SegmentIndexCreationDriverImpl driver = new
SegmentIndexCreationDriverImpl();
+ driver.init(config, new GenericRowRecordReader(List.of(record)));
+ driver.build();
+ String segmentDir = driver.getOutputDirectory().getPath();
+
+ File avroOut = new File(TEMP_DIR, "logicalSegment.avro");
+ new PinotSegmentToAvroConverter(segmentDir, avroOut.getPath()).convert();
+ try (AvroRecordReader reader = new AvroRecordReader()) {
+ reader.init(avroOut, logicalSchema.getFieldSpecMap().keySet(), null);
+ assertLogicalTypeRecord(reader.next());
+ assertFalse(reader.hasNext());
+ }
+
+ File parquetOut = new File(TEMP_DIR, "logicalSegment.parquet");
+ new PinotSegmentToParquetConverter(segmentDir,
parquetOut.getPath()).convert();
+ try (ParquetRecordReader reader = new ParquetRecordReader()) {
+ reader.init(parquetOut, logicalSchema.getFieldSpecMap().keySet(), null);
+ assertLogicalTypeRecord(reader.next());
+ assertFalse(reader.hasNext());
+ }
+ }
+
+ private static void assertLogicalTypeRecord(GenericRow record) {
+ // BOOLEAN reads back as a Boolean rather than the stored int, TIMESTAMP
as a Timestamp rather than a bare long.
+ assertEquals(record.getValue(BOOLEAN_SV_COLUMN), Boolean.TRUE);
+ assertEquals(record.getValue(TIMESTAMP_SV_COLUMN), new
Timestamp(TIMESTAMP_VALUE));
+ BigDecimal bigDecimal = (BigDecimal)
record.getValue(BIG_DECIMAL_SV_COLUMN);
+ assertEquals(bigDecimal, BIG_DECIMAL_VALUE);
+ assertEquals(bigDecimal.scale(), BIG_DECIMAL_VALUE.scale(), "big-decimal
must preserve the value's own scale");
+ assertEquals(record.getValue(BOOLEAN_MV_COLUMN), new
Object[]{Boolean.TRUE, Boolean.FALSE});
+ assertEquals(record.getValue(TIMESTAMP_MV_COLUMN),
+ new Object[]{new Timestamp(TIMESTAMP_VALUE), new Timestamp(0L)});
+ }
+
private static void assertUuidRecord(GenericRow record) {
// The reader may surface the uuid value as a String/UUID/byte[];
UuidUtils.toBytes normalizes all of them.
assertEquals(UuidUtils.toBytes(record.getValue(UUID_SV_COLUMN)),
UuidUtils.toBytes(UUID_SV_VALUE));
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]