This is an automated email from the ASF dual-hosted git repository.
danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new 427efb265429 fix(flink): normalize row logical conversions and improve
coverage (#19351)
427efb265429 is described below
commit 427efb2654296cd78c93e2602ca06f6f82bf0733
Author: Danny Chan <[email protected]>
AuthorDate: Thu Jul 23 17:42:02 2026 +0800
fix(flink): normalize row logical conversions and improve coverage (#19351)
* test(flink): improve row conversion coverage
---
.../hudi/client/model/HoodieFlinkRecord.java | 6 +-
.../apache/hudi/util/AvroToRowDataConverters.java | 3 +-
.../TestHoodieFlinkInternalRowSerializer.java | 125 ++++++++++++
.../hudi/client/model/TestHoodieFlinkRecord.java | 116 +++++++++++
.../row/parquet/TestParquetRowDataWriter.java | 176 +++++++++++++++++
.../row/parquet/TestParquetSchemaConverter.java | 63 ++++++
.../apache/hudi/util/TestRowDataAvroRoundTrip.java | 212 +++++++++++++++++++++
.../org/apache/hudi/util/TestRowDataUtils.java | 127 ++++++++++++
.../hudi/util/TestVectorConversionUtils.java | 156 +++++++++++++++
9 files changed, 979 insertions(+), 5 deletions(-)
diff --git
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/client/model/HoodieFlinkRecord.java
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/client/model/HoodieFlinkRecord.java
index 5bf98cfdd2ce..913cd5e68350 100644
---
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/client/model/HoodieFlinkRecord.java
+++
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/client/model/HoodieFlinkRecord.java
@@ -46,7 +46,6 @@ import org.apache.flink.table.data.DecimalData;
import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.data.StringData;
-import org.apache.flink.table.data.TimestampData;
import org.apache.flink.table.data.utils.JoinedRowData;
import java.io.ByteArrayOutputStream;
@@ -170,11 +169,10 @@ public class HoodieFlinkRecord extends
HoodieRecord<RowData> {
return LocalDate.ofEpochDay(((Integer) fieldValue).longValue());
} else if (schemaType == HoodieSchemaType.TIMESTAMP &&
keepConsistentLogicalTimestamp) {
HoodieSchema.Timestamp timestampSchema = (HoodieSchema.Timestamp)
fieldSchema;
- TimestampData ts = (TimestampData) fieldValue;
if (timestampSchema.getPrecision() == HoodieSchema.TimePrecision.MILLIS)
{
- return ts.getMillisecond();
+ return fieldValue;
} else if (timestampSchema.getPrecision() ==
HoodieSchema.TimePrecision.MICROS) {
- return ts.getMillisecond() / 1000;
+ return ((Long) fieldValue) / 1000;
}
} else if (schemaType == HoodieSchemaType.DECIMAL) {
return ((DecimalData) fieldValue).toBigDecimal();
diff --git
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/AvroToRowDataConverters.java
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/AvroToRowDataConverters.java
index 0a7d421c2b83..cc6c90c341f8 100644
---
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/AvroToRowDataConverters.java
+++
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/AvroToRowDataConverters.java
@@ -412,7 +412,8 @@ public class AvroToRowDataConverters {
public long convertDate(Object object) {
final org.joda.time.LocalDate value = (org.joda.time.LocalDate) object;
- return value.toDate().getTime();
+ return LocalDate.of(
+ value.getYear(), value.getMonthOfYear(),
value.getDayOfMonth()).toEpochDay();
}
public int convertTime(Object object) {
diff --git
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/client/model/TestHoodieFlinkInternalRowSerializer.java
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/client/model/TestHoodieFlinkInternalRowSerializer.java
new file mode 100644
index 000000000000..60227372c7f1
--- /dev/null
+++
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/client/model/TestHoodieFlinkInternalRowSerializer.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.client.model;
+
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputSerializer;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.GenericArrayData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.TimestampData;
+import org.apache.flink.table.types.logical.RowType;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestHoodieFlinkInternalRowSerializer {
+
+ private static final RowType ROW_TYPE = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("name", DataTypes.STRING()),
+ DataTypes.FIELD("amount", DataTypes.DECIMAL(10, 2)),
+ DataTypes.FIELD("created", DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(6)),
+ DataTypes.FIELD("tags", DataTypes.ARRAY(DataTypes.STRING())))
+ .getLogicalType();
+
+ @Test
+ void testRoundTripCopyAndStreamCopy() throws Exception {
+ HoodieFlinkInternalRowSerializer serializer = new
HoodieFlinkInternalRowSerializer(ROW_TYPE);
+ GenericRowData payload = GenericRowData.of(
+ StringData.fromString("alice"),
+ DecimalData.fromBigDecimal(new BigDecimal("12.34"), 10, 2),
+
TimestampData.fromInstant(Instant.parse("2025-02-03T04:05:06.123456Z")),
+ new GenericArrayData(new Object[] {StringData.fromString("x"), null}));
+ HoodieFlinkInternalRow original = new HoodieFlinkInternalRow(
+ "key", "partition", "file", "instant", "I", false, payload);
+
+ DataOutputSerializer output = new DataOutputSerializer(128);
+ serializer.serialize(original, output);
+ HoodieFlinkInternalRow restored = serializer.deserialize(
+ new DataInputDeserializer(output.getCopyOfBuffer()));
+ assertDataRecord(restored);
+
+ HoodieFlinkInternalRow copied = serializer.copy(original);
+ assertNotSame(original, copied);
+ assertNotSame(original.getRowData(), copied.getRowData());
+ assertDataRecord(copied);
+
+ DataOutputSerializer copiedOutput = new DataOutputSerializer(128);
+ serializer.copy(new DataInputDeserializer(output.getCopyOfBuffer()),
copiedOutput);
+ assertDataRecord(serializer.deserialize(new
DataInputDeserializer(copiedOutput.getCopyOfBuffer())));
+ }
+
+ @Test
+ void testIndexRecordRoundTripAndSerializerContract() throws Exception {
+ HoodieFlinkInternalRowSerializer serializer = new
HoodieFlinkInternalRowSerializer(ROW_TYPE);
+ HoodieFlinkInternalRow indexRecord =
+ new HoodieFlinkInternalRow("key", "partition", "file", "instant");
+ DataOutputSerializer output = new DataOutputSerializer(64);
+ serializer.serialize(indexRecord, output);
+
+ HoodieFlinkInternalRow restored = serializer.deserialize(
+ indexRecord, new DataInputDeserializer(output.getCopyOfBuffer()));
+ assertTrue(restored.isIndexRecord());
+ assertEquals("key", restored.getRecordKey());
+ assertEquals("partition", restored.getPartitionPath());
+ assertEquals("file", restored.getFileId());
+ assertEquals("instant", restored.getInstantTime());
+ assertEquals("", restored.getOperationType());
+
+ DataOutputSerializer copiedOutput = new DataOutputSerializer(64);
+ serializer.copy(new DataInputDeserializer(output.getCopyOfBuffer()),
copiedOutput);
+ assertTrue(serializer.deserialize(
+ new
DataInputDeserializer(copiedOutput.getCopyOfBuffer())).isIndexRecord());
+
+ assertFalse(serializer.isImmutableType());
+ assertEquals(-1, serializer.getLength());
+ assertEquals(serializer, serializer.duplicate());
+ assertEquals(serializer.hashCode(), serializer.duplicate().hashCode());
+ assertFalse(serializer.equals(null));
+ assertFalse(serializer.equals("serializer"));
+ assertThrows(UnsupportedOperationException.class,
serializer::createInstance);
+ assertThrows(UnsupportedOperationException.class,
+ () -> serializer.copy(indexRecord, indexRecord));
+ assertThrows(UnsupportedOperationException.class,
serializer::snapshotConfiguration);
+ }
+
+ private static void assertDataRecord(HoodieFlinkInternalRow record) {
+ assertFalse(record.isIndexRecord());
+ assertEquals("key", record.getRecordKey());
+ assertEquals("partition", record.getPartitionPath());
+ assertEquals("file", record.getFileId());
+ assertEquals("instant", record.getInstantTime());
+ assertEquals("I", record.getOperationType());
+ assertEquals("alice", record.getRowData().getString(0).toString());
+ assertEquals(new BigDecimal("12.34"), record.getRowData().getDecimal(1,
10, 2).toBigDecimal());
+ assertEquals(Instant.parse("2025-02-03T04:05:06.123456Z"),
+ record.getRowData().getTimestamp(2, 6).toInstant());
+ assertEquals("x", record.getRowData().getArray(3).getString(0).toString());
+ assertTrue(record.getRowData().getArray(3).isNullAt(1));
+ }
+}
diff --git
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/client/model/TestHoodieFlinkRecord.java
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/client/model/TestHoodieFlinkRecord.java
index 6744e2d14830..9753628da410 100644
---
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/client/model/TestHoodieFlinkRecord.java
+++
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/client/model/TestHoodieFlinkRecord.java
@@ -20,20 +20,33 @@ package org.apache.hudi.client.model;
import org.apache.hudi.common.model.HoodieKey;
import org.apache.hudi.common.model.HoodieOperation;
+import org.apache.hudi.common.model.HoodieRecord;
import org.apache.hudi.common.schema.HoodieSchema;
import org.apache.hudi.common.schema.HoodieSchemaField;
import org.apache.hudi.common.schema.HoodieSchemaType;
+import org.apache.hudi.common.util.Option;
import org.apache.hudi.common.util.OrderingValues;
+import org.apache.flink.table.data.DecimalData;
import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.TimestampData;
import org.junit.jupiter.api.Test;
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
import java.util.Arrays;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Unit tests for {@link HoodieFlinkRecord}.
@@ -266,4 +279,107 @@ public class TestHoodieFlinkRecord {
HoodieFlinkRecord updatedRecord = (HoodieFlinkRecord)
record.updateMetaField(schema, 0, "20240101000001");
assertEquals(HoodieOperation.INSERT, updatedRecord.getOperation());
}
+
+ @Test
+ public void testRecordContract() {
+ HoodieKey key = new HoodieKey("id-001", "partition-1");
+ GenericRowData row = GenericRowData.of(StringData.fromString("id-001"));
+ HoodieFlinkRecord record = new HoodieFlinkRecord(
+ key, HoodieOperation.INSERT, 100L, row, true);
+
+ assertEquals(HoodieRecord.HoodieRecordType.FLINK, record.getRecordType());
+ assertEquals(key, record.newInstance().getKey());
+ assertEquals("new-key", record.newInstance(
+ new HoodieKey("new-key", "p"),
HoodieOperation.UPDATE_AFTER).getRecordKey());
+ assertEquals("another-key", record.newInstance(
+ new HoodieKey("another-key", "p")).getRecordKey());
+ assertFalse(record.shouldIgnore(null, new Properties()));
+ assertSame(record, record.copy());
+ assertTrue(record.getMetadata().isEmpty());
+
+ HoodieFlinkRecord empty = new HoodieFlinkRecord(
+ key, HoodieOperation.INSERT, (RowData) null);
+ assertTrue(empty.checkIsDelete(null, new Properties()));
+ }
+
+ @Test
+ public void testConvertColumnValueForLogicalType() {
+ TimestampData timestamp = TimestampData.fromInstant(
+ Instant.parse("2025-02-03T04:05:06.123456Z"));
+ HoodieFlinkRecord record = new
HoodieFlinkRecord(GenericRowData.of(timestamp));
+
+ assertNull(record.convertColumnValueForLogicalType(
+ HoodieSchema.create(HoodieSchemaType.STRING), null, true));
+ assertEquals(LocalDate.ofEpochDay(2),
record.convertColumnValueForLogicalType(
+ HoodieSchema.createDate(), 2, true));
+
+ HoodieSchema millisSchema = HoodieSchema.createTimestampMillis();
+ HoodieSchema millisRecordSchema = HoodieSchema.createRecord(
+ "millis_record", null, null,
+ Arrays.asList(HoodieSchemaField.of("event_time", millisSchema)));
+ Object millisValue = record.getColumnValueAsJava(
+ millisRecordSchema, "event_time", new Properties());
+ assertEquals(timestamp.getMillisecond(), millisValue);
+ assertEquals(timestamp.getMillisecond(),
record.convertColumnValueForLogicalType(
+ millisSchema, millisValue, true));
+
+ HoodieSchema microsSchema = HoodieSchema.createTimestampMicros();
+ HoodieSchema microsRecordSchema = HoodieSchema.createRecord(
+ "micros_record", null, null,
+ Arrays.asList(HoodieSchemaField.of("event_time", microsSchema)));
+ Object microsValue = record.getColumnValueAsJava(
+ microsRecordSchema, "event_time", new Properties());
+ long expectedMicros = timestamp.toInstant().getEpochSecond() * 1_000_000
+ + timestamp.toInstant().getNano() / 1_000;
+ assertEquals(expectedMicros, microsValue);
+ assertEquals(timestamp.getMillisecond(),
record.convertColumnValueForLogicalType(
+ microsSchema, microsValue, true));
+
+ assertEquals(new BigDecimal("12.34"),
record.convertColumnValueForLogicalType(
+ HoodieSchema.createDecimal("decimal", null, null, 10, 2, 5),
+ DecimalData.fromBigDecimal(new BigDecimal("12.34"), 10, 2), true));
+ assertSame(millisValue, record.convertColumnValueForLogicalType(
+ millisSchema, millisValue, false));
+ }
+
+ @Test
+ public void testUnsupportedOperations() {
+ HoodieKey key = new HoodieKey("id-001", "partition-1");
+ GenericRowData row = GenericRowData.of(StringData.fromString("id-001"));
+ HoodieFlinkRecord record = new HoodieFlinkRecord(
+ key, HoodieOperation.INSERT, 100L, row, true);
+ assertThrows(UnsupportedOperationException.class,
+ () -> record.writeRecordPayload(row, null, null));
+ assertThrows(UnsupportedOperationException.class,
+ () -> record.readRecordPayload(null, null));
+ assertThrows(UnsupportedOperationException.class,
+ () -> record.getColumnValues(null, null, false));
+ assertThrows(UnsupportedOperationException.class,
+ () -> record.joinWith(record, null));
+ assertThrows(UnsupportedOperationException.class,
+ () -> record.wrapIntoHoodieRecordPayloadWithParams(
+ null, null, Option.empty(), false, Option.empty(), false,
Option.empty()));
+ assertThrows(UnsupportedOperationException.class,
+ () -> record.wrapIntoHoodieRecordPayloadWithKeyGen(null, null,
Option.empty()));
+ assertThrows(UnsupportedOperationException.class,
+ () -> record.truncateRecordKey(null, null, null));
+ }
+
+ @Test
+ public void testKeyLookupAndAvroMaterialization() {
+ HoodieSchema schema = HoodieSchema.createRecord("test", null, null,
Arrays.asList(
+ HoodieSchemaField.of("id",
HoodieSchema.create(HoodieSchemaType.STRING)),
+ HoodieSchemaField.of("value",
HoodieSchema.create(HoodieSchemaType.INT))));
+ HoodieFlinkRecord record = new HoodieFlinkRecord(GenericRowData.of(
+ StringData.fromString("id-001"), 42));
+
+ assertEquals("id-001", record.getRecordKey(schema, "id"));
+ // The second lookup exercises the cached record-key path.
+ assertEquals("id-001", record.getRecordKey(schema, "id"));
+ assertEquals("id-001", record.toIndexedRecord(schema, new Properties())
+ .get().getData().get(0).toString());
+ assertTrue(record.getAvroBytes(schema, new Properties()).size() > 0);
+ assertEquals(OrderingValues.getDefault(),
+ record.getOrderingValueAsJava(schema, new Properties(), null));
+ }
}
diff --git
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/parquet/TestParquetRowDataWriter.java
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/parquet/TestParquetRowDataWriter.java
new file mode 100644
index 000000000000..7d4c685c7a5f
--- /dev/null
+++
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/parquet/TestParquetRowDataWriter.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.io.storage.row.parquet;
+
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.util.HoodieSchemaConverter;
+
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.GenericArrayData;
+import org.apache.flink.table.data.GenericMapData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.TimestampData;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.parquet.io.api.RecordConsumer;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+class TestParquetRowDataWriter {
+
+ @Test
+ void testWritePrimitiveNestedArrayMapDecimalAndTimestampValues() {
+ RowType rowType = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("text", DataTypes.STRING()),
+ DataTypes.FIELD("flag", DataTypes.BOOLEAN()),
+ DataTypes.FIELD("bytes", DataTypes.BYTES()),
+ DataTypes.FIELD("tiny", DataTypes.TINYINT()),
+ DataTypes.FIELD("small", DataTypes.SMALLINT()),
+ DataTypes.FIELD("number", DataTypes.INT()),
+ DataTypes.FIELD("big", DataTypes.BIGINT()),
+ DataTypes.FIELD("ratio", DataTypes.FLOAT()),
+ DataTypes.FIELD("score", DataTypes.DOUBLE()),
+ DataTypes.FIELD("day", DataTypes.DATE()),
+ DataTypes.FIELD("time", DataTypes.TIME(3)),
+ DataTypes.FIELD("small_decimal", DataTypes.DECIMAL(10, 2)),
+ DataTypes.FIELD("large_decimal", DataTypes.DECIMAL(30, 4)),
+ DataTypes.FIELD("timestamp3", DataTypes.TIMESTAMP(3)),
+ DataTypes.FIELD("timestamp6", DataTypes.TIMESTAMP_LTZ(6)),
+ DataTypes.FIELD("items", DataTypes.ARRAY(DataTypes.STRING())),
+ DataTypes.FIELD("empty_items", DataTypes.ARRAY(DataTypes.INT())),
+ DataTypes.FIELD("attributes", DataTypes.MAP(DataTypes.STRING(),
DataTypes.INT())),
+ DataTypes.FIELD("nested", DataTypes.ROW(
+ DataTypes.FIELD("id", DataTypes.BIGINT()),
+ DataTypes.FIELD("label", DataTypes.STRING()),
+ DataTypes.FIELD("optional", DataTypes.INT()))),
+ DataTypes.FIELD("null_field", DataTypes.STRING()))
+ .notNull().getLogicalType();
+ HoodieSchema schema = HoodieSchemaConverter.convertToSchema(rowType,
"writer_record");
+
+ Map<Object, Object> attributes = new LinkedHashMap<>();
+ attributes.put(StringData.fromString("present"), 1);
+ attributes.put(StringData.fromString("missing"), null);
+ GenericRowData row = GenericRowData.of(
+ StringData.fromString("hello"), true, new byte[] {1, 2},
+ 3, 4, 5, 6L, 7.5f, 8.25d, 9, 10,
+ DecimalData.fromBigDecimal(new BigDecimal("12.34"), 10, 2),
+ DecimalData.fromBigDecimal(new
BigDecimal("12345678901234567890.1234"), 30, 4),
+ TimestampData.fromInstant(Instant.parse("2025-02-03T04:05:06.123Z")),
+
TimestampData.fromInstant(Instant.parse("2025-02-03T04:05:06.123456Z")),
+ new GenericArrayData(new Object[] {StringData.fromString("a"), null,
StringData.fromString("c")}),
+ new GenericArrayData(new int[0]),
+ new GenericMapData(attributes),
+ GenericRowData.of(99L, StringData.fromString("nested"), null),
+ null);
+
+ RecordConsumer consumer = mock(RecordConsumer.class);
+ new ParquetRowDataWriter(consumer, true, schema).write(row);
+
+ verify(consumer).startMessage();
+ verify(consumer).endMessage();
+ verify(consumer, atLeastOnce()).startField(any(String.class), anyInt());
+ verify(consumer, atLeastOnce()).addBoolean(true);
+ verify(consumer, atLeastOnce()).addInteger(5);
+ verify(consumer, atLeastOnce()).addLong(6L);
+ verify(consumer, atLeastOnce()).addFloat(7.5f);
+ verify(consumer, atLeastOnce()).addDouble(8.25d);
+ verify(consumer, atLeastOnce()).addBinary(any());
+ verify(consumer, atLeastOnce()).startGroup();
+ verify(consumer, atLeastOnce()).endGroup();
+ verify(consumer, never()).startField(eq("null_field"), anyInt());
+ }
+
+ @Test
+ void testNonUtcTimestampWriter() {
+ RowType rowType = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("timestamp3", DataTypes.TIMESTAMP(3)),
+ DataTypes.FIELD("timestamp6",
DataTypes.TIMESTAMP(6))).notNull().getLogicalType();
+ HoodieSchema schema = HoodieSchemaConverter.convertToSchema(rowType,
"timestamps");
+ GenericRowData row = GenericRowData.of(
+ TimestampData.fromInstant(Instant.parse("2025-02-03T04:05:06.123Z")),
+
TimestampData.fromInstant(Instant.parse("2025-02-03T04:05:06.123456Z")));
+ RecordConsumer consumer = mock(RecordConsumer.class);
+
+ new ParquetRowDataWriter(consumer, false, schema).write(row);
+
+ verify(consumer, times(2)).addLong(anyLong());
+ }
+
+ @Test
+ void testArrayElementWritersForPrimitiveAndComplexTypes() {
+ RowType rowType = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("booleans", DataTypes.ARRAY(DataTypes.BOOLEAN())),
+ DataTypes.FIELD("longs", DataTypes.ARRAY(DataTypes.BIGINT())),
+ DataTypes.FIELD("floats", DataTypes.ARRAY(DataTypes.FLOAT())),
+ DataTypes.FIELD("doubles", DataTypes.ARRAY(DataTypes.DOUBLE())),
+ DataTypes.FIELD("binaries", DataTypes.ARRAY(DataTypes.BYTES())),
+ DataTypes.FIELD("small_decimals",
DataTypes.ARRAY(DataTypes.DECIMAL(10, 2))),
+ DataTypes.FIELD("large_decimals",
DataTypes.ARRAY(DataTypes.DECIMAL(30, 4))),
+ DataTypes.FIELD("timestamps", DataTypes.ARRAY(DataTypes.TIMESTAMP(6))),
+ DataTypes.FIELD("nested_arrays",
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT()))),
+ DataTypes.FIELD("maps", DataTypes.ARRAY(
+ DataTypes.MAP(DataTypes.STRING(), DataTypes.INT()))),
+ DataTypes.FIELD("rows", DataTypes.ARRAY(DataTypes.ROW(
+ DataTypes.FIELD("id", DataTypes.BIGINT()),
+ DataTypes.FIELD("name",
DataTypes.STRING()))))).notNull().getLogicalType();
+ HoodieSchema schema = HoodieSchemaConverter.convertToSchema(rowType,
"array_elements");
+ Map<Object, Object> map = new LinkedHashMap<>();
+ map.put(StringData.fromString("key"), 1);
+ GenericRowData row = GenericRowData.of(
+ new GenericArrayData(new boolean[] {true, false}),
+ new GenericArrayData(new long[] {1L, 2L}),
+ new GenericArrayData(new float[] {1.5f, 2.5f}),
+ new GenericArrayData(new double[] {3.5d, 4.5d}),
+ new GenericArrayData(new Object[] {new byte[] {1}, new byte[] {2}}),
+ new GenericArrayData(new Object[] {
+ DecimalData.fromBigDecimal(new BigDecimal("12.34"), 10, 2)}),
+ new GenericArrayData(new Object[] {
+ DecimalData.fromBigDecimal(new
BigDecimal("12345678901234567890.1234"), 30, 4)}),
+ new GenericArrayData(new Object[] {
+
TimestampData.fromInstant(Instant.parse("2025-02-03T04:05:06.123456Z"))}),
+ new GenericArrayData(new Object[] {new GenericArrayData(new int[] {1,
2})}),
+ new GenericArrayData(new Object[] {new GenericMapData(map)}),
+ new GenericArrayData(new Object[] {
+ GenericRowData.of(1L, StringData.fromString("nested"))}));
+ RecordConsumer consumer = mock(RecordConsumer.class);
+
+ new ParquetRowDataWriter(consumer, true, schema).write(row);
+
+ verify(consumer, atLeastOnce()).addBoolean(true);
+ verify(consumer, atLeastOnce()).addLong(1L);
+ verify(consumer, atLeastOnce()).addFloat(1.5f);
+ verify(consumer, atLeastOnce()).addDouble(3.5d);
+ verify(consumer, atLeastOnce()).addBinary(any());
+ }
+}
diff --git
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/parquet/TestParquetSchemaConverter.java
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/parquet/TestParquetSchemaConverter.java
index 23c2d6dac56e..962af8f6e04f 100644
---
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/parquet/TestParquetSchemaConverter.java
+++
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/parquet/TestParquetSchemaConverter.java
@@ -287,6 +287,69 @@ public class TestParquetSchemaConverter {
assertThat(messageType.toString(), is(expected));
}
+ @Test
+ void testConvertAnnotatedPrimitiveParquetTypes() {
+ MessageType parquet = new MessageType("annotated",
+ Types.primitive(PrimitiveType.PrimitiveTypeName.INT32,
Type.Repetition.REQUIRED)
+ .as(LogicalTypeAnnotation.decimalType(2, 8)).named("decimal32"),
+ Types.primitive(PrimitiveType.PrimitiveTypeName.INT32,
Type.Repetition.OPTIONAL)
+ .as(LogicalTypeAnnotation.intType(8, true)).named("tiny"),
+ Types.primitive(PrimitiveType.PrimitiveTypeName.INT32,
Type.Repetition.OPTIONAL)
+ .as(LogicalTypeAnnotation.intType(16, true)).named("small"),
+ Types.primitive(PrimitiveType.PrimitiveTypeName.INT32,
Type.Repetition.OPTIONAL)
+ .as(LogicalTypeAnnotation.intType(32, true)).named("number"),
+ Types.primitive(PrimitiveType.PrimitiveTypeName.INT32,
Type.Repetition.OPTIONAL)
+ .as(LogicalTypeAnnotation.dateType()).named("day"),
+ Types.primitive(PrimitiveType.PrimitiveTypeName.INT32,
Type.Repetition.OPTIONAL)
+ .as(LogicalTypeAnnotation.timeType(true,
LogicalTypeAnnotation.TimeUnit.MILLIS)).named("time"),
+ Types.primitive(PrimitiveType.PrimitiveTypeName.INT64,
Type.Repetition.OPTIONAL)
+ .as(LogicalTypeAnnotation.decimalType(3, 12)).named("decimal64"),
+ Types.primitive(PrimitiveType.PrimitiveTypeName.INT96,
Type.Repetition.OPTIONAL)
+ .named("timestamp96"),
+ Types.primitive(PrimitiveType.PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY,
Type.Repetition.OPTIONAL)
+ .length(8).as(LogicalTypeAnnotation.decimalType(4,
16)).named("decimal_fixed"));
+
+ RowType converted = ParquetSchemaConverter.convertToRowType(parquet);
+ assertEquals("DECIMAL(8, 2) NOT NULL",
converted.getTypeAt(0).asSummaryString());
+ assertEquals("TINYINT", converted.getTypeAt(1).asSummaryString());
+ assertEquals("SMALLINT", converted.getTypeAt(2).asSummaryString());
+ assertEquals("INT", converted.getTypeAt(3).asSummaryString());
+ assertEquals("DATE", converted.getTypeAt(4).asSummaryString());
+ assertEquals("TIME(0)", converted.getTypeAt(5).asSummaryString());
+ assertEquals("DECIMAL(12, 3)", converted.getTypeAt(6).asSummaryString());
+ assertEquals("TIMESTAMP(9)", converted.getTypeAt(7).asSummaryString());
+ assertEquals("DECIMAL(16, 4)", converted.getTypeAt(8).asSummaryString());
+ }
+
+ @Test
+ void testConvertLocalZonedTimestampToParquet() {
+ RowType rowType = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("local_millis", DataTypes.TIMESTAMP_LTZ(3)),
+ DataTypes.FIELD("local_micros",
DataTypes.TIMESTAMP_LTZ(6))).notNull().getLogicalType();
+ MessageType parquet =
ParquetSchemaConverter.convertToParquetMessageType("local", rowType);
+ assertEquals(LogicalTypeAnnotation.timestampType(
+ false, LogicalTypeAnnotation.TimeUnit.MILLIS),
+ parquet.getType("local_millis").getLogicalTypeAnnotation());
+ assertEquals(LogicalTypeAnnotation.timestampType(
+ false, LogicalTypeAnnotation.TimeUnit.MICROS),
+ parquet.getType("local_micros").getLogicalTypeAnnotation());
+ }
+
+ @Test
+ void testConvertBinaryDateAndTimeToParquet() {
+ RowType rowType = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("payload", DataTypes.BYTES()),
+ DataTypes.FIELD("day", DataTypes.DATE()),
+ DataTypes.FIELD("time", DataTypes.TIME(3))).notNull().getLogicalType();
+ MessageType parquet =
ParquetSchemaConverter.convertToParquetMessageType("primitives", rowType);
+ assertEquals(PrimitiveType.PrimitiveTypeName.BINARY,
+ parquet.getType("payload").asPrimitiveType().getPrimitiveTypeName());
+ assertEquals(LogicalTypeAnnotation.dateType(),
+ parquet.getType("day").getLogicalTypeAnnotation());
+ assertEquals(LogicalTypeAnnotation.timeType(true,
LogicalTypeAnnotation.TimeUnit.MILLIS),
+ parquet.getType("time").getLogicalTypeAnnotation());
+ }
+
/**
* A Parquet group with metadata + value binary fields but NO VARIANT
annotation must be
* treated as a plain ROW. Only the Parquet {@code VARIANT} annotation
triggers variant
diff --git
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestRowDataAvroRoundTrip.java
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestRowDataAvroRoundTrip.java
new file mode 100644
index 000000000000..b9e53c985151
--- /dev/null
+++
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestRowDataAvroRoundTrip.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.util;
+
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaType;
+
+import org.apache.avro.generic.GenericRecord;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.ArrayData;
+import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.GenericArrayData;
+import org.apache.flink.table.data.GenericMapData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.MapData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.TimestampData;
+import org.apache.flink.table.types.logical.RowType;
+import org.joda.time.DateTime;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestRowDataAvroRoundTrip {
+
+ private static final RowType ROW_TYPE = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("tiny", DataTypes.TINYINT().notNull()),
+ DataTypes.FIELD("small", DataTypes.SMALLINT().notNull()),
+ DataTypes.FIELD("flag", DataTypes.BOOLEAN().notNull()),
+ DataTypes.FIELD("number", DataTypes.INT().notNull()),
+ DataTypes.FIELD("big", DataTypes.BIGINT().notNull()),
+ DataTypes.FIELD("ratio", DataTypes.FLOAT().notNull()),
+ DataTypes.FIELD("score", DataTypes.DOUBLE().notNull()),
+ DataTypes.FIELD("day", DataTypes.DATE().notNull()),
+ DataTypes.FIELD("time", DataTypes.TIME(3).notNull()),
+ DataTypes.FIELD("name", DataTypes.STRING()),
+ DataTypes.FIELD("payload", DataTypes.BYTES()),
+ DataTypes.FIELD("amount", DataTypes.DECIMAL(20, 4)),
+ DataTypes.FIELD("timestamp3", DataTypes.TIMESTAMP(3)),
+ DataTypes.FIELD("timestamp6", DataTypes.TIMESTAMP(6)),
+ DataTypes.FIELD("local_timestamp3", DataTypes.TIMESTAMP_LTZ(3)),
+ DataTypes.FIELD("local_timestamp6", DataTypes.TIMESTAMP_LTZ(6)),
+ DataTypes.FIELD("items", DataTypes.ARRAY(DataTypes.STRING())),
+ DataTypes.FIELD("attributes", DataTypes.MAP(DataTypes.STRING(),
DataTypes.INT())),
+ DataTypes.FIELD("nested", DataTypes.ROW(
+ DataTypes.FIELD("id", DataTypes.BIGINT()),
+ DataTypes.FIELD("label", DataTypes.STRING()))),
+ DataTypes.FIELD("nullable", DataTypes.STRING()))
+ .notNull().getLogicalType();
+
+ @Test
+ public void testAllSupportedTypesRoundTripValueByValue() {
+ Instant instant = Instant.parse("2025-02-03T04:05:06.123456Z");
+ Map<Object, Object> attributes = new LinkedHashMap<>();
+ attributes.put(StringData.fromString("one"), 1);
+ attributes.put(StringData.fromString("missing"), null);
+ GenericRowData input = GenericRowData.of(
+ (byte) 1, (short) 2, true, 3, 4L, 5.5f, 6.25d, 20, 1234,
+ StringData.fromString("alice"), new byte[] {7, 8},
+ DecimalData.fromBigDecimal(new BigDecimal("123456789012.3400"), 20, 4),
+ TimestampData.fromInstant(instant), TimestampData.fromInstant(instant),
+ TimestampData.fromInstant(instant), TimestampData.fromInstant(instant),
+ new GenericArrayData(new Object[] {StringData.fromString("a"), null,
StringData.fromString("c")}),
+ new GenericMapData(attributes),
+ GenericRowData.of(99L, StringData.fromString("nested")),
+ null);
+
+ HoodieSchema schema = HoodieSchemaConverter.convertToSchema(ROW_TYPE,
"AllTypes");
+ GenericRecord avro = (GenericRecord)
RowDataToAvroConverters.createConverter(ROW_TYPE)
+ .convert(schema, input);
+ RowData output = (RowData) AvroToRowDataConverters
+ .createRowConverter(schema, ROW_TYPE, true).convert(avro);
+ RowData defaultOutput = (RowData) AvroToRowDataConverters
+ .createRowConverter(ROW_TYPE).convert(avro);
+ RowData nonUtcOutput = (RowData) AvroToRowDataConverters
+ .createRowConverter(ROW_TYPE, false).convert(avro);
+ RowData schemaOutput = (RowData) AvroToRowDataConverters
+ .createRowConverter(schema).convert(avro);
+
+ assertEquals((byte) 1, output.getByte(0));
+ assertEquals((short) 2, output.getShort(1));
+ assertTrue(output.getBoolean(2));
+ assertEquals(3, output.getInt(3));
+ assertEquals(4L, output.getLong(4));
+ assertEquals(5.5f, output.getFloat(5));
+ assertEquals(6.25d, output.getDouble(6));
+ assertEquals(20, output.getInt(7));
+ assertEquals(1234, output.getInt(8));
+ assertEquals("alice", output.getString(9).toString());
+ assertArrayEquals(new byte[] {7, 8}, output.getBinary(10));
+ assertEquals(new BigDecimal("123456789012.3400"), output.getDecimal(11,
20, 4).toBigDecimal());
+ assertEquals(instant.toEpochMilli(), output.getTimestamp(12,
3).getMillisecond());
+ assertEquals(instant, output.getTimestamp(13, 6).toInstant());
+ assertEquals(instant.toEpochMilli(), output.getTimestamp(14,
3).getMillisecond());
+ assertEquals(instant, output.getTimestamp(15, 6).toInstant());
+ assertEquals("a", output.getArray(16).getString(0).toString());
+ assertTrue(output.getArray(16).isNullAt(1));
+ assertEquals("c", output.getArray(16).getString(2).toString());
+ assertMap(output.getMap(17));
+ assertEquals(99L, output.getRow(18, 2).getLong(0));
+ assertEquals("nested", output.getRow(18, 2).getString(1).toString());
+ assertTrue(output.isNullAt(19));
+ assertEquals("alice", defaultOutput.getString(9).toString());
+ assertEquals("alice", nonUtcOutput.getString(9).toString());
+ assertEquals("alice", schemaOutput.getString(9).toString());
+ }
+
+ @Test
+ public void testPrimitiveRuntimeRepresentations() {
+ HoodieSchema intSchema =
HoodieSchemaConverter.convertToSchema(DataTypes.INT().getLogicalType());
+
assertNull(AvroToRowDataConverters.createConverter(DataTypes.NULL().getLogicalType(),
true)
+ .convert("ignored"));
+ assertEquals((byte) 3, AvroToRowDataConverters.createConverter(
+ DataTypes.TINYINT().getLogicalType(), true).convert(3));
+ assertEquals((short) 4, AvroToRowDataConverters.createConverter(
+ DataTypes.SMALLINT().getLogicalType(), true).convert(4));
+ assertEquals(5, AvroToRowDataConverters.createConverter(
+ DataTypes.INT().getLogicalType(), true).convert(5));
+ assertEquals(2, AvroToRowDataConverters.createConverter(
+ DataTypes.DATE().getLogicalType(),
true).convert(LocalDate.ofEpochDay(2)));
+ assertEquals(1234, AvroToRowDataConverters.createConverter(
+ DataTypes.TIME(3).getLogicalType(),
true).convert(LocalTime.ofNanoOfDay(1_234_000_000L)));
+ assertArrayEquals(new byte[] {1, 2}, (byte[])
AvroToRowDataConverters.createConverter(
+ DataTypes.BYTES().getLogicalType(), true).convert(new byte[] {1, 2}));
+ byte[] unscaled = new BigInteger("1234").toByteArray();
+ assertEquals(new BigDecimal("12.34"), ((DecimalData)
AvroToRowDataConverters.createConverter(
+ DataTypes.DECIMAL(8, 2).getLogicalType(),
true).convert(ByteBuffer.wrap(unscaled))).toBigDecimal());
+ assertEquals(new BigDecimal("12.34"), ((DecimalData)
AvroToRowDataConverters.createConverter(
+ DataTypes.DECIMAL(8, 2).getLogicalType(),
true).convert(unscaled)).toBigDecimal());
+ assertEquals(7,
RowDataToAvroConverters.createConverter(DataTypes.TINYINT().getLogicalType())
+ .convert(intSchema, (byte) 7));
+ assertEquals(8,
RowDataToAvroConverters.createConverter(DataTypes.SMALLINT().getLogicalType())
+ .convert(intSchema, (short) 8));
+
assertNull(RowDataToAvroConverters.createConverter(DataTypes.NULL().getLogicalType())
+ .convert(HoodieSchema.create(HoodieSchemaType.NULL), "ignored"));
+ assertEquals(ByteBuffer.wrap(new byte[] {4, 5}),
RowDataToAvroConverters.createConverter(
+ DataTypes.BYTES().getLogicalType()).convert(
+
HoodieSchemaConverter.convertToSchema(DataTypes.BYTES().getLogicalType()), new
byte[] {4, 5}));
+ HoodieSchema.Vector vectorSchema = HoodieSchema.createVector(2);
+ Object vector = RowDataToAvroConverters.createConverter(
+ DataTypes.ARRAY(DataTypes.FLOAT()).getLogicalType()).convert(
+ vectorSchema, new GenericArrayData(new float[] {1.0f, 2.0f}));
+ assertTrue(vector instanceof org.apache.avro.generic.GenericData.Fixed);
+ }
+
+ @Test
+ public void testTimestampAndJodaRuntimeRepresentations() {
+ Instant instant = Instant.parse("2025-02-03T04:05:06.123Z");
+ assertEquals(instant, ((TimestampData)
AvroToRowDataConverters.createConverter(
+ DataTypes.TIMESTAMP(3).getLogicalType(),
true).convert(instant)).toInstant());
+ assertEquals(instant.toEpochMilli(), ((TimestampData)
AvroToRowDataConverters.createConverter(
+ DataTypes.TIMESTAMP(3).getLogicalType(), true).convert(new
DateTime(instant.toEpochMilli())))
+ .getMillisecond());
+ TimestampData nonUtcTimestamp = (TimestampData)
AvroToRowDataConverters.createConverter(
+ DataTypes.TIMESTAMP(3).getLogicalType(),
false).convert(instant.toEpochMilli());
+ assertEquals(instant.toEpochMilli(),
nonUtcTimestamp.toTimestamp().getTime());
+ assertEquals((int) LocalDate.of(2025, 2, 3).toEpochDay(),
+
AvroToRowDataConverters.createConverter(DataTypes.DATE().getLogicalType(), true)
+ .convert(new org.joda.time.LocalDate(2025, 2, 3)));
+ assertEquals(14_706_123,
+
AvroToRowDataConverters.createConverter(DataTypes.TIME(3).getLogicalType(),
true)
+ .convert(new org.joda.time.LocalTime(4, 5, 6, 123)));
+ assertSame(AvroToRowDataConverters.JodaConverter.getConverter(),
+ AvroToRowDataConverters.JodaConverter.getConverter());
+ assertThrows(IllegalArgumentException.class,
+ () ->
AvroToRowDataConverters.createConverter(DataTypes.TIMESTAMP(9).getLogicalType(),
true));
+ }
+
+ private static void assertMap(MapData map) {
+ ArrayData keys = map.keyArray();
+ ArrayData values = map.valueArray();
+ Map<String, Integer> actual = new LinkedHashMap<>();
+ for (int i = 0; i < map.size(); i++) {
+ actual.put(keys.getString(i).toString(), values.isNullAt(i) ? null :
values.getInt(i));
+ }
+ Map<String, Integer> expected = new LinkedHashMap<>();
+ expected.put("one", 1);
+ expected.put("missing", null);
+ assertEquals(expected, actual);
+ }
+}
diff --git
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestRowDataUtils.java
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestRowDataUtils.java
new file mode 100644
index 000000000000..0249e74028cf
--- /dev/null
+++
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestRowDataUtils.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.util;
+
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.TimestampData;
+import org.apache.flink.table.types.logical.LocalZonedTimestampType;
+import org.apache.flink.table.types.logical.TimestampType;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TestRowDataUtils {
+
+ @Test
+ void testJavaAndFlinkValueConverters() {
+ assertNull(RowDataUtils.NULL_GETTER.getFieldOrNull(GenericRowData.of(1)));
+ assertNull(RowDataUtils.javaValFunc(DataTypes.NULL().getLogicalType(),
true).apply("ignored"));
+ assertEquals(7,
RowDataUtils.javaValFunc(DataTypes.TINYINT().getLogicalType(),
true).apply((byte) 7));
+ assertEquals(8,
RowDataUtils.javaValFunc(DataTypes.SMALLINT().getLogicalType(),
true).apply((short) 8));
+ assertEquals(2,
RowDataUtils.javaValFunc(DataTypes.DATE().getLogicalType(), true).apply(2));
+ assertEquals("text",
RowDataUtils.javaValFunc(DataTypes.STRING().getLogicalType(), true)
+ .apply(StringData.fromString("text")));
+ assertArrayEquals(new byte[] {1, 2}, ((ByteBuffer)
RowDataUtils.javaValFunc(
+ DataTypes.BYTES().getLogicalType(), true).apply(new byte[] {1,
2})).array());
+ assertEquals(new BigDecimal("12.30"), RowDataUtils.javaValFunc(
+ DataTypes.DECIMAL(8, 2).getLogicalType(), true)
+ .apply(DecimalData.fromBigDecimal(new BigDecimal("12.30"), 8, 2)));
+
+ assertEquals((byte) 7,
RowDataUtils.flinkValFunc(DataTypes.TINYINT().getLogicalType(),
true).apply((byte) 7));
+ assertEquals((short) 8,
RowDataUtils.flinkValFunc(DataTypes.SMALLINT().getLogicalType(),
true).apply((short) 8));
+ assertEquals(3,
RowDataUtils.flinkValFunc(DataTypes.DATE().getLogicalType(), true)
+ .apply(LocalDate.ofEpochDay(3)));
+ assertEquals("text",
RowDataUtils.flinkValFunc(DataTypes.STRING().getLogicalType(), true)
+ .apply("text").toString());
+ ByteBuffer buffer = ByteBuffer.wrap(new byte[] {3, 4});
+ assertSame(buffer,
RowDataUtils.flinkValFunc(DataTypes.BYTES().getLogicalType(),
true).apply(buffer));
+ assertEquals(new BigDecimal("45.60"), ((DecimalData)
RowDataUtils.flinkValFunc(
+ DataTypes.DECIMAL(8, 2).getLogicalType(), true).apply(new
BigDecimal("45.60"))).toBigDecimal());
+ }
+
+ @Test
+ void testTimestampConvertersAtMillisAndMicros() {
+ TimestampData timestamp =
TimestampData.fromInstant(Instant.parse("2025-02-03T04:05:06.123456Z"));
+ long millis = timestamp.toInstant().toEpochMilli();
+ long micros = timestamp.toInstant().getEpochSecond() * 1_000_000 + 123456;
+
+ assertEquals(millis,
RowDataUtils.javaValFunc(DataTypes.TIMESTAMP_LTZ(3).getLogicalType(), true)
+ .apply(timestamp));
+ assertEquals(micros,
RowDataUtils.javaValFunc(DataTypes.TIMESTAMP_LTZ(6).getLogicalType(), true)
+ .apply(timestamp));
+ assertEquals(millis,
RowDataUtils.javaValFunc(DataTypes.TIMESTAMP(3).getLogicalType(), true)
+ .apply(timestamp));
+ assertEquals(micros,
RowDataUtils.javaValFunc(DataTypes.TIMESTAMP(6).getLogicalType(), true)
+ .apply(timestamp));
+ long localMillis = (long) RowDataUtils.javaValFunc(
+ DataTypes.TIMESTAMP(3).getLogicalType(), false).apply(timestamp);
+ long localMicros = (long) RowDataUtils.javaValFunc(
+ DataTypes.TIMESTAMP(6).getLogicalType(), false).apply(timestamp);
+
+ assertEquals(Instant.ofEpochMilli(millis), ((TimestampData)
RowDataUtils.flinkValFunc(
+ DataTypes.TIMESTAMP_LTZ(3).getLogicalType(),
true).apply(millis)).toInstant());
+ assertEquals(timestamp.toInstant(), ((TimestampData)
RowDataUtils.flinkValFunc(
+ DataTypes.TIMESTAMP_LTZ(6).getLogicalType(),
true).apply(micros)).toInstant());
+ assertEquals(millis, ((TimestampData) RowDataUtils.flinkValFunc(
+ DataTypes.TIMESTAMP(3).getLogicalType(),
false).apply(millis)).toTimestamp().getTime());
+ assertEquals(timestamp.toInstant(), ((TimestampData)
RowDataUtils.flinkValFunc(
+ DataTypes.TIMESTAMP(6).getLogicalType(),
true).apply(micros)).toInstant());
+ assertEquals(localMillis,
RowDataUtils.javaValFunc(DataTypes.TIMESTAMP(3).getLogicalType(), false)
+
.apply(RowDataUtils.flinkValFunc(DataTypes.TIMESTAMP(3).getLogicalType(),
false).apply(localMillis)));
+ assertEquals(localMicros,
RowDataUtils.javaValFunc(DataTypes.TIMESTAMP(6).getLogicalType(), false)
+
.apply(RowDataUtils.flinkValFunc(DataTypes.TIMESTAMP(6).getLogicalType(),
false).apply(localMicros)));
+
+ assertThrows(UnsupportedOperationException.class,
+ () ->
RowDataUtils.javaValFunc(DataTypes.TIMESTAMP(9).getLogicalType(), true));
+ assertThrows(UnsupportedOperationException.class,
+ () ->
RowDataUtils.flinkValFunc(DataTypes.TIMESTAMP_LTZ(9).getLogicalType(), true));
+ }
+
+ @Test
+ void testGenericConversionAndPrecision() {
+ assertNull(RowDataUtils.convertValueToFlinkType(null));
+ assertEquals("value",
RowDataUtils.convertValueToFlinkType("value").toString());
+ assertEquals(new BigDecimal("1.20"), ((DecimalData)
RowDataUtils.convertValueToFlinkType(
+ new BigDecimal("1.20"))).toBigDecimal());
+ Timestamp timestamp = Timestamp.valueOf("2025-02-03 04:05:06.123456");
+ assertEquals(timestamp, ((TimestampData)
RowDataUtils.convertValueToFlinkType(timestamp)).toTimestamp());
+ assertEquals(2,
RowDataUtils.convertValueToFlinkType(LocalDate.ofEpochDay(2)));
+ assertArrayEquals(new byte[] {9, 8},
+ (byte[]) RowDataUtils.convertValueToFlinkType(ByteBuffer.wrap(new
byte[] {9, 8})));
+ Object marker = new Object();
+ assertSame(marker, RowDataUtils.convertValueToFlinkType(marker));
+ assertEquals(3, RowDataUtils.precision(new TimestampType(3)));
+ assertEquals(6, RowDataUtils.precision(new LocalZonedTimestampType(6)));
+ assertThrows(AssertionError.class,
+ () -> RowDataUtils.precision(DataTypes.INT().getLogicalType()));
+ }
+}
diff --git
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestVectorConversionUtils.java
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestVectorConversionUtils.java
new file mode 100644
index 000000000000..e5a2e8478fd2
--- /dev/null
+++
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestVectorConversionUtils.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.util;
+
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaField;
+import org.apache.hudi.common.schema.HoodieSchemaType;
+import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.exception.SchemaCompatibilityException;
+
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.ArrayData;
+import org.apache.flink.table.data.GenericArrayData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.types.RowKind;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TestVectorConversionUtils {
+
+ private static final HoodieSchema.Vector FLOAT_VECTOR =
HoodieSchema.createVector(2);
+ private static final HoodieSchema.Vector DOUBLE_VECTOR =
HoodieSchema.createVector(
+ 2, HoodieSchema.Vector.VectorElementType.DOUBLE);
+ private static final HoodieSchema.Vector INT8_VECTOR =
HoodieSchema.createVector(
+ 3, HoodieSchema.Vector.VectorElementType.INT8);
+
+ @Test
+ void testDetectAndRewriteVectorColumns() {
+ HoodieSchema schema = vectorRecordSchema();
+ String[] names = {"id", "Float_Vec", "double_vec", "codes"};
+ int[] selected = {3, 0, 1};
+ Map<Integer, HoodieSchema.Vector> detected =
+ VectorConversionUtils.detectVectorColumns(names, selected, schema);
+ assertEquals(INT8_VECTOR, detected.get(0));
+ assertEquals(FLOAT_VECTOR, detected.get(2));
+ assertFalse(detected.containsKey(1));
+
+ DataType requested = DataTypes.ROW(
+ DataTypes.FIELD("codes",
DataTypes.ARRAY(DataTypes.TINYINT()).notNull()),
+ DataTypes.FIELD("id", DataTypes.INT().notNull()),
+ DataTypes.FIELD("float_vec",
DataTypes.ARRAY(DataTypes.FLOAT()))).notNull();
+ HoodieSchema projectedSchema = HoodieSchema.createRecord("projected",
null, null, Arrays.asList(
+ HoodieSchemaField.of("codes", INT8_VECTOR),
+ HoodieSchemaField.of("id", HoodieSchema.create(HoodieSchemaType.INT)),
+ HoodieSchemaField.of("float_vec", FLOAT_VECTOR)));
+ DataType physical = VectorConversionUtils.getParquetReadDataType(
+ requested, projectedSchema, detected);
+ RowType physicalRow = (RowType) physical.getLogicalType();
+ assertEquals(LogicalTypeRoot.VARBINARY,
physicalRow.getTypeAt(0).getTypeRoot());
+ assertEquals(LogicalTypeRoot.INTEGER,
physicalRow.getTypeAt(1).getTypeRoot());
+ assertEquals(LogicalTypeRoot.VARBINARY,
physicalRow.getTypeAt(2).getTypeRoot());
+ assertFalse(physicalRow.getTypeAt(0).isNullable());
+ assertSame(requested, VectorConversionUtils.getParquetReadDataType(
+ requested, projectedSchema, Collections.emptyMap()));
+
+ DataType[] fieldTypes = {
+ DataTypes.INT(), DataTypes.ARRAY(DataTypes.FLOAT()),
+ DataTypes.ARRAY(DataTypes.DOUBLE()),
DataTypes.ARRAY(DataTypes.TINYINT())};
+ DataType[] physicalTypes =
VectorConversionUtils.getParquetReadFieldTypes(names, fieldTypes, schema);
+ assertEquals(LogicalTypeRoot.INTEGER,
physicalTypes[0].getLogicalType().getTypeRoot());
+ assertEquals(LogicalTypeRoot.VARBINARY,
physicalTypes[1].getLogicalType().getTypeRoot());
+ assertEquals(LogicalTypeRoot.VARBINARY,
physicalTypes[2].getLogicalType().getTypeRoot());
+ assertEquals(LogicalTypeRoot.VARBINARY,
physicalTypes[3].getLogicalType().getTypeRoot());
+ }
+
+ @Test
+ void testEncodeDecodeAndIteratorConversion() throws Exception {
+ byte[] floatBytes = VectorConversionUtils.encodeVectorArrayData(
+ new GenericArrayData(new float[] {1.25f, -2.5f}), FLOAT_VECTOR);
+ byte[] doubleBytes = VectorConversionUtils.encodeVectorArrayData(
+ new GenericArrayData(new double[] {3.5d, -4.75d}), DOUBLE_VECTOR);
+ byte[] int8Bytes = VectorConversionUtils.encodeVectorArrayData(
+ new GenericArrayData(new byte[] {1, -2, 3}), INT8_VECTOR);
+
+ ArrayData floats = VectorConversionUtils.createVectorArrayData(floatBytes,
FLOAT_VECTOR);
+ assertEquals(1.25f, floats.getFloat(0));
+ assertEquals(-2.5f, floats.getFloat(1));
+ ArrayData doubles =
VectorConversionUtils.createVectorArrayData(doubleBytes, DOUBLE_VECTOR);
+ assertEquals(3.5d, doubles.getDouble(0));
+ assertEquals(-4.75d, doubles.getDouble(1));
+ ArrayData codes = VectorConversionUtils.createVectorArrayData(int8Bytes,
INT8_VECTOR);
+ assertEquals((byte) -2, codes.getByte(1));
+ assertArrayEquals(int8Bytes,
VectorConversionUtils.encodeVectorArrayData(codes, INT8_VECTOR));
+ assertThrows(IllegalArgumentException.class, () ->
VectorConversionUtils.encodeVectorArrayData(
+ new GenericArrayData(new float[] {1.0f}), FLOAT_VECTOR));
+
+ GenericRowData physical = GenericRowData.of(
+ StringData.fromString("key"), floatBytes, null);
+ physical.setRowKind(RowKind.UPDATE_AFTER);
+ RowType rowType = (RowType) DataTypes.ROW(
+ DataTypes.FIELD("id", DataTypes.STRING()),
+ DataTypes.FIELD("vector", DataTypes.BYTES()),
+ DataTypes.FIELD("optional", DataTypes.INT())).getLogicalType();
+ ClosableIterator<RowData> wrapped =
VectorConversionUtils.wrapVectorColumnIterator(
+
ClosableIterator.wrap(Collections.<RowData>singletonList(physical).iterator()),
+ rowType, Collections.singletonMap(1, FLOAT_VECTOR));
+ RowData converted = wrapped.next();
+ assertEquals(RowKind.UPDATE_AFTER, converted.getRowKind());
+ assertEquals("key", converted.getString(0).toString());
+ assertEquals(1.25f, converted.getArray(1).getFloat(0));
+ assertTrue(converted.isNullAt(2));
+ wrapped.close();
+ }
+
+ @Test
+ void testVectorLogicalTypeValidation() {
+ VectorConversionUtils.validateVectorLogicalType(
+ FLOAT_VECTOR, DataTypes.ARRAY(DataTypes.FLOAT()).getLogicalType());
+ VectorConversionUtils.validateVectorLogicalType(
+ DOUBLE_VECTOR, DataTypes.ARRAY(DataTypes.DOUBLE()).getLogicalType());
+ VectorConversionUtils.validateVectorLogicalType(
+ INT8_VECTOR, DataTypes.ARRAY(DataTypes.TINYINT()).getLogicalType());
+ assertThrows(SchemaCompatibilityException.class,
+ () -> VectorConversionUtils.validateVectorLogicalType(
+ FLOAT_VECTOR,
DataTypes.ARRAY(DataTypes.DOUBLE()).getLogicalType()));
+ }
+
+ private static HoodieSchema vectorRecordSchema() {
+ return HoodieSchema.createRecord("vectors", null, null, Arrays.asList(
+ HoodieSchemaField.of("id", HoodieSchema.create(HoodieSchemaType.INT)),
+ HoodieSchemaField.of("float_vec", FLOAT_VECTOR),
+ HoodieSchemaField.of("double_vec", DOUBLE_VECTOR),
+ HoodieSchemaField.of("codes", INT8_VECTOR)));
+ }
+}