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 29a4925a0f01 feat(flink): support nested data type for flink lance 
reader/writer (#19242)
29a4925a0f01 is described below

commit 29a4925a0f01f121f17623e19a9afa562bcef567
Author: Shuo Cheng <[email protected]>
AuthorDate: Tue Jul 14 12:44:17 2026 +0800

    feat(flink): support nested data type for flink lance reader/writer (#19242)
    
    * feat(flink): support nested data type for flink lance reader/writer
---
 .../io/storage/row/HoodieRowDataLanceWriter.java   |   9 +-
 .../{ => lance}/HoodieFlinkLanceArrowUtils.java    | 183 ++++----
 .../io/storage/row/lance/LanceRowDataWriter.java   | 482 +++++++++++++++++++++
 .../row/TestHoodieFlinkLanceArrowUtils.java        |  58 ++-
 .../io/storage/row/TestLanceRowDataWriter.java     | 274 ++++++++++++
 .../table/format/HoodieRowDataLanceReader.java     |   2 +-
 .../apache/hudi/table/ITTestHoodieDataSource.java  |  43 ++
 7 files changed, 937 insertions(+), 114 deletions(-)

diff --git 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieRowDataLanceWriter.java
 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieRowDataLanceWriter.java
index 6381dba3005f..0990e3d6527a 100644
--- 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieRowDataLanceWriter.java
+++ 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieRowDataLanceWriter.java
@@ -26,6 +26,8 @@ import org.apache.hudi.common.model.HoodieRecord;
 import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.ValidationUtils;
 import org.apache.hudi.io.lance.HoodieBaseLanceWriter;
+import org.apache.hudi.io.storage.row.lance.HoodieFlinkLanceArrowUtils;
+import org.apache.hudi.io.storage.row.lance.LanceRowDataWriter;
 import org.apache.hudi.storage.StoragePath;
 
 import org.apache.arrow.vector.VectorSchemaRoot;
@@ -134,18 +136,17 @@ public class HoodieRowDataLanceWriter extends 
HoodieBaseLanceWriter<RowData, Str
 
   private class RowDataArrowWriter implements ArrowWriter<RowData> {
     private final VectorSchemaRoot root;
+    private final LanceRowDataWriter writer;
     private int rowId;
 
     private RowDataArrowWriter(VectorSchemaRoot root) {
       this.root = root;
+      this.writer = new LanceRowDataWriter(rowType, root.getFieldVectors(), 
utcTimestamp);
     }
 
     @Override
     public void write(RowData row) {
-      for (int i = 0; i < rowType.getFieldCount(); i++) {
-        HoodieFlinkLanceArrowUtils.writeValue(rowType.getTypeAt(i), 
root.getVector(i), rowId, row, i, utcTimestamp);
-      }
-      rowId++;
+      writer.write(row, rowId++);
     }
 
     @Override
diff --git 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieFlinkLanceArrowUtils.java
 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/HoodieFlinkLanceArrowUtils.java
similarity index 66%
rename from 
hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieFlinkLanceArrowUtils.java
rename to 
hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/HoodieFlinkLanceArrowUtils.java
index 07171f615b46..6c4e4ac29cb0 100644
--- 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieFlinkLanceArrowUtils.java
+++ 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/HoodieFlinkLanceArrowUtils.java
@@ -16,8 +16,9 @@
  * limitations under the License.
  */
 
-package org.apache.hudi.io.storage.row;
+package org.apache.hudi.io.storage.row.lance;
 
+import org.apache.hudi.common.util.ValidationUtils;
 import org.apache.hudi.exception.HoodieNotSupportedException;
 
 import org.apache.arrow.vector.BigIntVector;
@@ -30,11 +31,13 @@ import org.apache.arrow.vector.Float8Vector;
 import org.apache.arrow.vector.IntVector;
 import org.apache.arrow.vector.SmallIntVector;
 import org.apache.arrow.vector.TimeMilliVector;
-import org.apache.arrow.vector.TimeStampMicroVector;
+import org.apache.arrow.vector.TimeStampVector;
 import org.apache.arrow.vector.TinyIntVector;
 import org.apache.arrow.vector.ValueVector;
 import org.apache.arrow.vector.VarBinaryVector;
 import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.complex.ListVector;
+import org.apache.arrow.vector.complex.StructVector;
 import org.apache.arrow.vector.types.DateUnit;
 import org.apache.arrow.vector.types.FloatingPointPrecision;
 import org.apache.arrow.vector.types.TimeUnit;
@@ -42,11 +45,14 @@ import org.apache.arrow.vector.types.pojo.ArrowType;
 import org.apache.arrow.vector.types.pojo.Field;
 import org.apache.arrow.vector.types.pojo.FieldType;
 import org.apache.arrow.vector.types.pojo.Schema;
+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.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.types.logical.ArrayType;
 import org.apache.flink.table.types.logical.BigIntType;
 import org.apache.flink.table.types.logical.BooleanType;
 import org.apache.flink.table.types.logical.DateType;
@@ -66,13 +72,16 @@ import org.apache.flink.table.types.logical.VarCharType;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
-import static 
org.apache.flink.table.types.logical.utils.LogicalTypeChecks.getPrecision;
-
 /**
- * Primitive RowData/Arrow conversion helpers for Flink Lance base files.
+ * RowData/Arrow conversion helpers for Flink Lance base files.
+ *
+ * <p>Supports primitive types, {@code ROW}, and {@code ARRAY}. Currently, 
{@code MAP} is not
+ * supported in the current Lance version. Enable it once the lance version is 
upgraded to support
+ * {@code MAP}.
+ *
+ * @see <a href="https://github.com/lance-format/lance/issues/3620";>Lance 
issue #3620</a>
  */
 public final class HoodieFlinkLanceArrowUtils {
 
@@ -90,7 +99,7 @@ public final class HoodieFlinkLanceArrowUtils {
   public static RowType toRowType(Schema schema) {
     List<RowType.RowField> fields = new ArrayList<>(schema.getFields().size());
     for (Field field : schema.getFields()) {
-      fields.add(new RowType.RowField(field.getName(), 
toLogicalType(field.getType())));
+      fields.add(new RowType.RowField(field.getName(), toLogicalType(field, 
field.getName())));
     }
     return new RowType(fields);
   }
@@ -108,68 +117,10 @@ public final class HoodieFlinkLanceArrowUtils {
     return rowData;
   }
 
-  public static void writeValue(LogicalType type, FieldVector vector, int 
rowId, RowData rowData, int ordinal) {
-    writeValue(type, vector, rowId, rowData, ordinal, true);
-  }
-
-  public static void writeValue(LogicalType type, FieldVector vector, int 
rowId, RowData rowData, int ordinal, boolean utcTimestamp) {
-    if (rowData.isNullAt(ordinal)) {
-      vector.setNull(rowId);
-      return;
-    }
-    switch (type.getTypeRoot()) {
-      case BOOLEAN:
-        ((BitVector) vector).setSafe(rowId, rowData.getBoolean(ordinal) ? 1 : 
0);
-        return;
-      case TINYINT:
-        ((TinyIntVector) vector).setSafe(rowId, rowData.getByte(ordinal));
-        return;
-      case SMALLINT:
-        ((SmallIntVector) vector).setSafe(rowId, rowData.getShort(ordinal));
-        return;
-      case INTEGER:
-        ((IntVector) vector).setSafe(rowId, rowData.getInt(ordinal));
-        return;
-      case DATE:
-        ((DateDayVector) vector).setSafe(rowId, rowData.getInt(ordinal));
-        return;
-      case TIME_WITHOUT_TIME_ZONE:
-        ((TimeMilliVector) vector).setSafe(rowId, rowData.getInt(ordinal));
-        return;
-      case BIGINT:
-        ((BigIntVector) vector).setSafe(rowId, rowData.getLong(ordinal));
-        return;
-      case FLOAT:
-        ((Float4Vector) vector).setSafe(rowId, rowData.getFloat(ordinal));
-        return;
-      case DOUBLE:
-        ((Float8Vector) vector).setSafe(rowId, rowData.getDouble(ordinal));
-        return;
-      case CHAR:
-      case VARCHAR:
-        ((VarCharVector) vector).setSafe(rowId, 
rowData.getString(ordinal).toBytes());
-        return;
-      case BINARY:
-      case VARBINARY:
-        ((VarBinaryVector) vector).setSafe(rowId, rowData.getBinary(ordinal));
-        return;
-      case DECIMAL:
-        DecimalType decimalType = (DecimalType) type;
-        DecimalData decimal = rowData.getDecimal(ordinal, 
decimalType.getPrecision(), decimalType.getScale());
-        ((DecimalVector) vector).setSafe(rowId, decimal.toBigDecimal());
-        return;
-      case TIMESTAMP_WITHOUT_TIME_ZONE:
-      case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
-        TimestampData timestamp = rowData.getTimestamp(ordinal, 
getPrecision(type));
-        long micros = timestampToMicros(timestamp, getPrecision(type), 
utcTimestamp);
-        ((TimeStampMicroVector) vector).setSafe(rowId, micros);
-        return;
-      default:
-        throw unsupported(type);
-    }
-  }
-
   private static Object readValue(LogicalType type, ValueVector vector, int 
rowId) {
+    if (vector.isNull(rowId)) {
+      return null;
+    }
     switch (type.getTypeRoot()) {
       case BOOLEAN:
         return ((BitVector) vector).get(rowId) == 1;
@@ -201,15 +152,32 @@ public final class HoodieFlinkLanceArrowUtils {
         return DecimalData.fromBigDecimal(decimal, decimalType.getPrecision(), 
decimalType.getScale());
       case TIMESTAMP_WITHOUT_TIME_ZONE:
       case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
-        long micros = ((TimeStampMicroVector) vector).get(rowId);
+        long micros = ((TimeStampVector) vector).get(rowId);
         return TimestampData.fromEpochMillis(Math.floorDiv(micros, 1000L), 
(int) Math.floorMod(micros, 1000L) * 1000);
+      case ROW:
+        return readRow((RowType) type, (StructVector) vector, rowId);
+      case ARRAY:
+        return readArray((ArrayType) type, (ListVector) vector, rowId);
       default:
         throw unsupported(type);
     }
   }
 
   private static Field toArrowField(String name, LogicalType type) {
-    return new Field(name, FieldType.nullable(toArrowType(type)), 
Collections.emptyList());
+    List<Field> children = new ArrayList<>();
+    switch (type.getTypeRoot()) {
+      case ROW:
+        for (RowType.RowField field : ((RowType) type).getFields()) {
+          children.add(toArrowField(field.getName(), field.getType()));
+        }
+        break;
+      case ARRAY:
+        children.add(toArrowField("element", ((ArrayType) 
type).getElementType()));
+        break;
+      default:
+        break;
+    }
+    return new Field(name, new FieldType(type.isNullable(), toArrowType(type), 
null), children);
   }
 
   private static ArrowType toArrowType(LogicalType type) {
@@ -245,61 +213,96 @@ public final class HoodieFlinkLanceArrowUtils {
         return new ArrowType.Timestamp(TimeUnit.MICROSECOND, null);
       case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
         return new ArrowType.Timestamp(TimeUnit.MICROSECOND, "UTC");
+      case ROW:
+        return ArrowType.Struct.INSTANCE;
+      case ARRAY:
+        return ArrowType.List.INSTANCE;
       default:
         throw unsupported(type);
     }
   }
 
-  private static LogicalType toLogicalType(ArrowType arrowType) {
+  private static LogicalType toLogicalType(Field field, String path) {
+    ArrowType arrowType = field.getType();
+    LogicalType logicalType;
     if (arrowType instanceof ArrowType.Bool) {
-      return new BooleanType();
+      logicalType = new BooleanType();
     } else if (arrowType instanceof ArrowType.Int) {
       ArrowType.Int intType = (ArrowType.Int) arrowType;
       switch (intType.getBitWidth()) {
         case 8:
-          return new TinyIntType();
+          logicalType = new TinyIntType();
+          break;
         case 16:
-          return new SmallIntType();
+          logicalType = new SmallIntType();
+          break;
         case 32:
-          return new IntType();
+          logicalType = new IntType();
+          break;
         case 64:
-          return new BigIntType();
+          logicalType = new BigIntType();
+          break;
         default:
           throw new HoodieNotSupportedException("Unsupported Arrow int width 
for Lance Flink reader: " + intType.getBitWidth());
       }
     } else if (arrowType instanceof ArrowType.FloatingPoint) {
       ArrowType.FloatingPoint fp = (ArrowType.FloatingPoint) arrowType;
-      return fp.getPrecision() == FloatingPointPrecision.SINGLE
+      logicalType = fp.getPrecision() == FloatingPointPrecision.SINGLE
           ? new FloatType()
           : new DoubleType();
     } else if (arrowType instanceof ArrowType.Utf8) {
-      return new VarCharType();
+      logicalType = new VarCharType();
     } else if (arrowType instanceof ArrowType.Binary) {
-      return new VarBinaryType();
+      logicalType = new VarBinaryType();
     } else if (arrowType instanceof ArrowType.Date) {
-      return new DateType();
+      logicalType = new DateType();
     } else if (arrowType instanceof ArrowType.Time) {
-      return new TimeType();
+      logicalType = new TimeType();
     } else if (arrowType instanceof ArrowType.Decimal) {
       ArrowType.Decimal decimal = (ArrowType.Decimal) arrowType;
-      return new DecimalType(decimal.getPrecision(), decimal.getScale());
+      logicalType = new DecimalType(decimal.getPrecision(), 
decimal.getScale());
     } else if (arrowType instanceof ArrowType.Timestamp) {
       ArrowType.Timestamp timestamp = (ArrowType.Timestamp) arrowType;
-      return timestamp.getTimezone() == null
+      logicalType = timestamp.getTimezone() == null
           ? new TimestampType(6)
           : new LocalZonedTimestampType(6);
+    } else if (arrowType instanceof ArrowType.Struct) {
+      List<RowType.RowField> fields = new 
ArrayList<>(field.getChildren().size());
+      for (Field child : field.getChildren()) {
+        fields.add(new RowType.RowField(child.getName(), toLogicalType(child, 
path + "." + child.getName())));
+      }
+      logicalType = new RowType(field.isNullable(), fields);
+    } else if (arrowType instanceof ArrowType.List) {
+      ValidationUtils.checkArgument(field.getChildren().size() == 1,
+          String.format("Unsupported Arrow schema at '%s': LIST must contain 
exactly one child field", path));
+      Field element = field.getChildren().get(0);
+      logicalType = new ArrayType(field.isNullable(), toLogicalType(element, 
path + "[]"));
+    } else {
+      throw new HoodieNotSupportedException("Unsupported Arrow type for Lance 
Flink reader at '" + path + "': " + arrowType);
     }
-    throw new HoodieNotSupportedException("Unsupported Arrow type for Lance 
Flink reader: " + arrowType);
+    return logicalType.copy(field.isNullable());
   }
 
-  private static long timestampToMicros(TimestampData timestampData, int 
precision, boolean utcTimestamp) {
-    long millis = utcTimestamp ? timestampData.getMillisecond() : 
timestampData.toTimestamp().getTime();
-    return precision > 3 && utcTimestamp
-        ? millis * 1000L + timestampData.getNanoOfMillisecond() / 1000L
-        : millis * 1000L;
+  private static RowData readRow(RowType rowType, StructVector vector, int 
rowId) {
+    GenericRowData row = new GenericRowData(rowType.getFieldCount());
+    for (int i = 0; i < rowType.getFieldCount(); i++) {
+      row.setField(i, readValue(rowType.getTypeAt(i), 
vector.getChildByOrdinal(i), rowId));
+    }
+    return row;
+  }
+
+  private static ArrayData readArray(ArrayType arrayType, ListVector vector, 
int rowId) {
+    int startIndex = vector.getElementStartIndex(rowId);
+    int endIndex = vector.getElementEndIndex(rowId);
+    Object[] values = new Object[endIndex - startIndex];
+    FieldVector dataVector = vector.getDataVector();
+    for (int i = 0; i < values.length; i++) {
+      values[i] = readValue(arrayType.getElementType(), dataVector, startIndex 
+ i);
+    }
+    return new GenericArrayData(values);
   }
 
   private static HoodieNotSupportedException unsupported(LogicalType type) {
-    return new HoodieNotSupportedException("Flink Lance base-file support 
currently supports primitive append-only columns; unsupported type: " + type);
+    return new HoodieNotSupportedException("Flink Lance base-file support 
currently supports primitive, ROW, and ARRAY columns; unsupported type: " + 
type);
   }
 }
diff --git 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/LanceRowDataWriter.java
 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/LanceRowDataWriter.java
new file mode 100644
index 000000000000..41c2481d5987
--- /dev/null
+++ 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/LanceRowDataWriter.java
@@ -0,0 +1,482 @@
+/*
+ * 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.lance;
+
+import org.apache.hudi.exception.HoodieNotSupportedException;
+
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.BitVector;
+import org.apache.arrow.vector.DateDayVector;
+import org.apache.arrow.vector.DecimalVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.Float4Vector;
+import org.apache.arrow.vector.Float8Vector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.SmallIntVector;
+import org.apache.arrow.vector.TimeMilliVector;
+import org.apache.arrow.vector.TimeStampVector;
+import org.apache.arrow.vector.TinyIntVector;
+import org.apache.arrow.vector.VarBinaryVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.complex.ListVector;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.flink.table.data.ArrayData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.TimestampData;
+import org.apache.flink.table.types.logical.ArrayType;
+import org.apache.flink.table.types.logical.DecimalType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+
+import java.util.List;
+
+import static 
org.apache.flink.table.types.logical.utils.LogicalTypeChecks.getPrecision;
+
+/**
+ * Writes Flink {@link RowData} values into Arrow vectors used by Lance base 
files.
+ *
+ * <p>The type-specific writer tree is created once for the supplied Arrow 
vectors. Record writes
+ * then use direct {@link RowData} and {@link ArrayData} accessors without 
creating field or element
+ * getters on the record-level path.
+ *
+ * <p>Supports primitive types, {@code ROW}, and {@code ARRAY}. {@code MAP} is 
not supported by the
+ * current Lance Java writer. Enable it once Lance adds Java writer support 
for {@code MAP}.
+ *
+ * @see <a href="https://github.com/lance-format/lance/issues/3620";>Lance 
issue #3620</a>
+ */
+public class LanceRowDataWriter {
+
+  private final FieldWriter[] fieldWriters;
+
+  public LanceRowDataWriter(RowType rowType, List<FieldVector> vectors, 
boolean utcTimestamp) {
+    this.fieldWriters = new FieldWriter[rowType.getFieldCount()];
+    for (int i = 0; i < fieldWriters.length; i++) {
+      fieldWriters[i] = createWriter(rowType.getTypeAt(i), vectors.get(i), 
utcTimestamp);
+    }
+  }
+
+  public void write(RowData row, int rowId) {
+    for (int i = 0; i < fieldWriters.length; i++) {
+      fieldWriters[i].write(row, i, rowId);
+    }
+  }
+
+  private static FieldWriter createWriter(LogicalType type, FieldVector 
vector, boolean utcTimestamp) {
+    switch (type.getTypeRoot()) {
+      case BOOLEAN:
+        return new BooleanWriter((BitVector) vector);
+      case TINYINT:
+        return new TinyIntWriter((TinyIntVector) vector);
+      case SMALLINT:
+        return new SmallIntWriter((SmallIntVector) vector);
+      case INTEGER:
+        return new IntWriter((IntVector) vector);
+      case DATE:
+        return new DateWriter((DateDayVector) vector);
+      case TIME_WITHOUT_TIME_ZONE:
+        return new TimeWriter((TimeMilliVector) vector);
+      case BIGINT:
+        return new BigIntWriter((BigIntVector) vector);
+      case FLOAT:
+        return new FloatWriter((Float4Vector) vector);
+      case DOUBLE:
+        return new DoubleWriter((Float8Vector) vector);
+      case CHAR:
+      case VARCHAR:
+        return new StringWriter((VarCharVector) vector);
+      case BINARY:
+      case VARBINARY:
+        return new BinaryWriter((VarBinaryVector) vector);
+      case DECIMAL:
+        return new DecimalWriter((DecimalVector) vector, (DecimalType) type);
+      case TIMESTAMP_WITHOUT_TIME_ZONE:
+      case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+        return new TimestampWriter((TimeStampVector) vector, 
getPrecision(type), utcTimestamp);
+      case ROW:
+        RowType rowType = (RowType) type;
+        StructVector structVector = (StructVector) vector;
+        FieldWriter[] fieldWriters = new FieldWriter[rowType.getFieldCount()];
+        for (int i = 0; i < fieldWriters.length; i++) {
+          fieldWriters[i] = createWriter(
+              rowType.getTypeAt(i), (FieldVector) 
structVector.getChildByOrdinal(i), utcTimestamp);
+        }
+        return new RowWriter(structVector, fieldWriters);
+      case ARRAY:
+        ArrayType arrayType = (ArrayType) type;
+        ListVector listVector = (ListVector) vector;
+        FieldWriter elementWriter = createWriter(
+            arrayType.getElementType(), listVector.getDataVector(), 
utcTimestamp);
+        return new ArrayWriter(listVector, elementWriter);
+      default:
+        throw unsupported(type);
+    }
+  }
+
+  private abstract static class FieldWriter {
+    protected final FieldVector fieldVector;
+
+    private FieldWriter(FieldVector vector) {
+      this.fieldVector = vector;
+    }
+
+    final void write(RowData row, int ordinal, int rowId) {
+      if (row.isNullAt(ordinal)) {
+        fieldVector.setNull(rowId);
+      } else {
+        writeNonNull(row, ordinal, rowId);
+      }
+    }
+
+    final void write(ArrayData array, int ordinal, int rowId) {
+      if (array.isNullAt(ordinal)) {
+        fieldVector.setNull(rowId);
+      } else {
+        writeNonNull(array, ordinal, rowId);
+      }
+    }
+
+    abstract void writeNonNull(RowData row, int ordinal, int rowId);
+
+    abstract void writeNonNull(ArrayData array, int ordinal, int rowId);
+  }
+
+  private static class BooleanWriter extends FieldWriter {
+    private final BitVector vector;
+
+    private BooleanWriter(BitVector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getBoolean(ordinal) ? 1 : 0);
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getBoolean(ordinal) ? 1 : 0);
+    }
+  }
+
+  private static class TinyIntWriter extends FieldWriter {
+    private final TinyIntVector vector;
+
+    private TinyIntWriter(TinyIntVector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getByte(ordinal));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getByte(ordinal));
+    }
+  }
+
+  private static class SmallIntWriter extends FieldWriter {
+    private final SmallIntVector vector;
+
+    private SmallIntWriter(SmallIntVector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getShort(ordinal));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getShort(ordinal));
+    }
+  }
+
+  private static class IntWriter extends FieldWriter {
+    private final IntVector vector;
+
+    private IntWriter(IntVector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getInt(ordinal));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getInt(ordinal));
+    }
+  }
+
+  private static class DateWriter extends FieldWriter {
+    private final DateDayVector vector;
+
+    private DateWriter(DateDayVector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getInt(ordinal));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getInt(ordinal));
+    }
+  }
+
+  private static class TimeWriter extends FieldWriter {
+    private final TimeMilliVector vector;
+
+    private TimeWriter(TimeMilliVector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getInt(ordinal));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getInt(ordinal));
+    }
+  }
+
+  private static class BigIntWriter extends FieldWriter {
+    private final BigIntVector vector;
+
+    private BigIntWriter(BigIntVector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getLong(ordinal));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getLong(ordinal));
+    }
+  }
+
+  private static class FloatWriter extends FieldWriter {
+    private final Float4Vector vector;
+
+    private FloatWriter(Float4Vector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getFloat(ordinal));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getFloat(ordinal));
+    }
+  }
+
+  private static class DoubleWriter extends FieldWriter {
+    private final Float8Vector vector;
+
+    private DoubleWriter(Float8Vector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getDouble(ordinal));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getDouble(ordinal));
+    }
+  }
+
+  private static class StringWriter extends FieldWriter {
+    private final VarCharVector vector;
+
+    private StringWriter(VarCharVector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getString(ordinal).toBytes());
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getString(ordinal).toBytes());
+    }
+  }
+
+  private static class BinaryWriter extends FieldWriter {
+    private final VarBinaryVector vector;
+
+    private BinaryWriter(VarBinaryVector vector) {
+      super(vector);
+      this.vector = vector;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getBinary(ordinal));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getBinary(ordinal));
+    }
+  }
+
+  private static class DecimalWriter extends FieldWriter {
+    private final DecimalVector vector;
+    private final int precision;
+    private final int scale;
+
+    private DecimalWriter(DecimalVector vector, DecimalType decimalType) {
+      super(vector);
+      this.vector = vector;
+      this.precision = decimalType.getPrecision();
+      this.scale = decimalType.getScale();
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, row.getDecimal(ordinal, precision, 
scale).toBigDecimal());
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, array.getDecimal(ordinal, precision, 
scale).toBigDecimal());
+    }
+  }
+
+  private static class TimestampWriter extends FieldWriter {
+    private final TimeStampVector vector;
+    private final int precision;
+    private final boolean utcTimestamp;
+
+    private TimestampWriter(TimeStampVector vector, int precision, boolean 
utcTimestamp) {
+      super(vector);
+      this.vector = vector;
+      this.precision = precision;
+      this.utcTimestamp = utcTimestamp;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      vector.setSafe(rowId, timestampToMicros(row.getTimestamp(ordinal, 
precision), precision, utcTimestamp));
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      vector.setSafe(rowId, timestampToMicros(array.getTimestamp(ordinal, 
precision), precision, utcTimestamp));
+    }
+  }
+
+  private static class RowWriter extends FieldWriter {
+    private final StructVector vector;
+    private final FieldWriter[] fieldWriters;
+
+    private RowWriter(StructVector vector, FieldWriter[] fieldWriters) {
+      super(vector);
+      this.vector = vector;
+      this.fieldWriters = fieldWriters;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      writeRow(row.getRow(ordinal, fieldWriters.length), rowId);
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      writeRow(array.getRow(ordinal, fieldWriters.length), rowId);
+    }
+
+    private void writeRow(RowData row, int rowId) {
+      vector.setIndexDefined(rowId);
+      for (int i = 0; i < fieldWriters.length; i++) {
+        fieldWriters[i].write(row, i, rowId);
+      }
+    }
+  }
+
+  private static class ArrayWriter extends FieldWriter {
+    private final ListVector vector;
+    private final FieldWriter elementWriter;
+
+    private ArrayWriter(ListVector vector, FieldWriter elementWriter) {
+      super(vector);
+      this.vector = vector;
+      this.elementWriter = elementWriter;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      writeArray(row.getArray(ordinal), rowId);
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      writeArray(array.getArray(ordinal), rowId);
+    }
+
+    private void writeArray(ArrayData array, int rowId) {
+      int startIndex = vector.startNewValue(rowId);
+      for (int i = 0; i < array.size(); i++) {
+        elementWriter.write(array, i, startIndex + i);
+      }
+      vector.endValue(rowId, array.size());
+    }
+  }
+
+  private static long timestampToMicros(TimestampData timestampData, int 
precision, boolean utcTimestamp) {
+    long millis = utcTimestamp ? timestampData.getMillisecond() : 
timestampData.toTimestamp().getTime();
+    return precision > 3 && utcTimestamp
+        ? millis * 1000L + timestampData.getNanoOfMillisecond() / 1000L
+        : millis * 1000L;
+  }
+
+  private static HoodieNotSupportedException unsupported(LogicalType type) {
+    return new HoodieNotSupportedException(
+        "Flink Lance base-file support currently supports primitive, ROW, and 
ARRAY columns; unsupported type: " + type);
+  }
+}
diff --git 
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestHoodieFlinkLanceArrowUtils.java
 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestHoodieFlinkLanceArrowUtils.java
index 2a9ecc5b0145..007148a4df97 100644
--- 
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestHoodieFlinkLanceArrowUtils.java
+++ 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestHoodieFlinkLanceArrowUtils.java
@@ -18,25 +18,33 @@
 
 package org.apache.hudi.io.storage.row;
 
+import org.apache.hudi.exception.HoodieNotSupportedException;
+import org.apache.hudi.io.storage.row.lance.HoodieFlinkLanceArrowUtils;
+
 import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.memory.RootAllocator;
 import org.apache.arrow.vector.TimeStampMicroVector;
 import org.apache.arrow.vector.types.TimeUnit;
 import org.apache.arrow.vector.types.pojo.ArrowType;
 import org.apache.arrow.vector.types.pojo.FieldType;
-import org.apache.flink.table.data.GenericRowData;
 import org.apache.flink.table.data.RowData;
 import org.apache.flink.table.data.TimestampData;
+import org.apache.flink.table.types.logical.ArrayType;
+import org.apache.flink.table.types.logical.IntType;
 import org.apache.flink.table.types.logical.LocalZonedTimestampType;
 import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.MapType;
 import org.apache.flink.table.types.logical.RowType;
 import org.apache.flink.table.types.logical.TimestampType;
+import org.apache.flink.table.types.logical.VarCharType;
 import org.junit.jupiter.api.Test;
 
 import java.util.Collections;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Tests for {@link HoodieFlinkLanceArrowUtils}.
@@ -56,24 +64,6 @@ public class TestHoodieFlinkLanceArrowUtils {
     assertInstanceOf(LocalZonedTimestampType.class, roundTripped.getTypeAt(1));
   }
 
-  @Test
-  public void testTimestampWriteHonorsUtcTimestampFlag() {
-    TimestampData timestampData = TimestampData.fromEpochMillis(1234L, 567000);
-    GenericRowData rowData = GenericRowData.of(timestampData);
-
-    try (BufferAllocator allocator = new RootAllocator();
-         TimeStampMicroVector vector = new TimeStampMicroVector(
-             "ts",
-             FieldType.nullable(new ArrowType.Timestamp(TimeUnit.MICROSECOND, 
null)),
-             allocator)) {
-      HoodieFlinkLanceArrowUtils.writeValue(new TimestampType(6), vector, 0, 
rowData, 0, true);
-      assertEquals(1234567L, vector.get(0));
-
-      HoodieFlinkLanceArrowUtils.writeValue(new TimestampType(6), vector, 1, 
rowData, 0, false);
-      assertEquals(timestampData.toTimestamp().getTime() * 1000L, 
vector.get(1));
-    }
-  }
-
   @Test
   public void testTimestampReadNormalizesPreEpochMicros() {
     try (BufferAllocator allocator = new RootAllocator();
@@ -92,4 +82,34 @@ public class TestHoodieFlinkLanceArrowUtils {
       assertEquals(TimestampData.fromEpochMillis(-1235L, 433000), 
rowData.getTimestamp(0, 6));
     }
   }
+
+  @Test
+  public void testNestedSchemaRoundTrip() {
+    RowType rowType = nestedRowType();
+
+    RowType roundTripped = HoodieFlinkLanceArrowUtils.toRowType(
+        HoodieFlinkLanceArrowUtils.toArrowSchema(rowType));
+
+    assertEquals(rowType, roundTripped);
+  }
+
+  @Test
+  public void testRejectsMapTypeWhenWritingSchema() {
+    HoodieNotSupportedException exception = 
assertThrows(HoodieNotSupportedException.class,
+        () -> HoodieFlinkLanceArrowUtils.toArrowSchema(RowType.of(
+            new LogicalType[] {new MapType(new VarCharType(), new IntType())},
+            new String[] {"attributes"})));
+    assertTrue(exception.getMessage().contains("Flink Lance base-file support 
currently supports primitive, ROW, and ARRAY columns;"));
+  }
+
+  private static RowType nestedRowType() {
+    RowType profileType = RowType.of(
+        new LogicalType[] {new VarCharType(), new IntType()},
+        new String[] {"name", "age"});
+    ArrayType numbersType = new ArrayType(new IntType());
+    ArrayType profilesType = new ArrayType(profileType);
+    return RowType.of(
+        new LogicalType[] {profileType, numbersType, profilesType},
+        new String[] {"profile", "numbers", "profiles"});
+  }
 }
diff --git 
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestLanceRowDataWriter.java
 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestLanceRowDataWriter.java
new file mode 100644
index 000000000000..d43c2ad6d9b1
--- /dev/null
+++ 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestLanceRowDataWriter.java
@@ -0,0 +1,274 @@
+/*
+ * 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;
+
+import org.apache.hudi.exception.HoodieNotSupportedException;
+import org.apache.hudi.io.storage.row.lance.HoodieFlinkLanceArrowUtils;
+import org.apache.hudi.io.storage.row.lance.LanceRowDataWriter;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.TimeStampMicroVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.complex.ListVector;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.arrow.vector.types.TimeUnit;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.FieldType;
+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.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.TimestampData;
+import org.apache.flink.table.types.logical.ArrayType;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.BinaryType;
+import org.apache.flink.table.types.logical.BooleanType;
+import org.apache.flink.table.types.logical.CharType;
+import org.apache.flink.table.types.logical.DateType;
+import org.apache.flink.table.types.logical.DecimalType;
+import org.apache.flink.table.types.logical.DoubleType;
+import org.apache.flink.table.types.logical.FloatType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LocalZonedTimestampType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.MapType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.table.types.logical.SmallIntType;
+import org.apache.flink.table.types.logical.TimeType;
+import org.apache.flink.table.types.logical.TimestampType;
+import org.apache.flink.table.types.logical.TinyIntType;
+import org.apache.flink.table.types.logical.VarBinaryType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link LanceRowDataWriter}.
+ */
+public class TestLanceRowDataWriter {
+
+  @Test
+  public void testPrimitiveValuesAndNulls() {
+    RowType rowType = primitiveRowType();
+    TimestampData timestamp = TimestampData.fromEpochMillis(1234L, 567000);
+    TimestampData localTimestamp = TimestampData.fromEpochMillis(5678L, 
123000);
+    DecimalData decimal = DecimalData.fromBigDecimal(new 
BigDecimal("12345.67"), 10, 2);
+    GenericRowData values = GenericRowData.of(
+        true,
+        (byte) 12,
+        (short) 1234,
+        123456,
+        20000,
+        12345678,
+        1234567890123L,
+        1.25F,
+        2.5D,
+        StringData.fromString("char"),
+        StringData.fromString("varchar"),
+        new byte[] {1, 2, 3},
+        new byte[] {4, 5, 6, 7},
+        decimal,
+        timestamp,
+        localTimestamp);
+    GenericRowData nulls = new GenericRowData(rowType.getFieldCount());
+
+    try (BufferAllocator allocator = new RootAllocator();
+         VectorSchemaRoot root = VectorSchemaRoot.create(
+             HoodieFlinkLanceArrowUtils.toArrowSchema(rowType), allocator)) {
+      root.allocateNew();
+      LanceRowDataWriter writer = new LanceRowDataWriter(rowType, 
root.getFieldVectors(), true);
+      writer.write(values, 0);
+      writer.write(nulls, 1);
+      root.getFieldVectors().forEach(vector -> vector.setValueCount(2));
+      root.setRowCount(2);
+
+      RowData actual = HoodieFlinkLanceArrowUtils.toRowData(rowType, 
root.getFieldVectors(), 0);
+      assertEquals(true, actual.getBoolean(0));
+      assertEquals((byte) 12, actual.getByte(1));
+      assertEquals((short) 1234, actual.getShort(2));
+      assertEquals(123456, actual.getInt(3));
+      assertEquals(20000, actual.getInt(4));
+      assertEquals(12345678, actual.getInt(5));
+      assertEquals(1234567890123L, actual.getLong(6));
+      assertEquals(1.25F, actual.getFloat(7));
+      assertEquals(2.5D, actual.getDouble(8));
+      assertEquals(StringData.fromString("char"), actual.getString(9));
+      assertEquals(StringData.fromString("varchar"), actual.getString(10));
+      assertArrayEquals(new byte[] {1, 2, 3}, actual.getBinary(11));
+      assertArrayEquals(new byte[] {4, 5, 6, 7}, actual.getBinary(12));
+      assertEquals(decimal, actual.getDecimal(13, 10, 2));
+      assertEquals(timestamp, actual.getTimestamp(14, 6));
+      assertEquals(localTimestamp, actual.getTimestamp(15, 6));
+
+      RowData actualNulls = HoodieFlinkLanceArrowUtils.toRowData(rowType, 
root.getFieldVectors(), 1);
+      for (int i = 0; i < rowType.getFieldCount(); i++) {
+        assertTrue(actualNulls.isNullAt(i));
+      }
+    }
+  }
+
+  @Test
+  public void testTimestampWriteHonorsUtcTimestampFlag() {
+    TimestampData timestampData = TimestampData.fromEpochMillis(1234L, 567000);
+    GenericRowData rowData = GenericRowData.of(timestampData);
+
+    try (BufferAllocator allocator = new RootAllocator();
+         TimeStampMicroVector vector = new TimeStampMicroVector(
+             "ts",
+             FieldType.nullable(new ArrowType.Timestamp(TimeUnit.MICROSECOND, 
null)),
+             allocator)) {
+      RowType rowType = RowType.of(new LogicalType[] {new TimestampType(6)}, 
new String[] {"ts"});
+      new LanceRowDataWriter(rowType, Collections.singletonList(vector), 
true).write(rowData, 0);
+      assertEquals(1234567L, vector.get(0));
+
+      new LanceRowDataWriter(rowType, Collections.singletonList(vector), 
false).write(rowData, 1);
+      assertEquals(timestampData.toTimestamp().getTime() * 1000L, 
vector.get(1));
+    }
+  }
+
+  @Test
+  public void testNestedValueRoundTripAndListValueCount() {
+    RowType rowType = nestedRowType();
+    GenericRowData first = GenericRowData.of(
+        GenericRowData.of(
+            StringData.fromString("alice"),
+            null,
+            new GenericArrayData(new Object[] 
{StringData.fromString("primary"), null})),
+        new GenericArrayData(new Object[] {1, null}),
+        new GenericArrayData(new Object[] {
+            GenericRowData.of(
+                StringData.fromString("child"),
+                3,
+                new GenericArrayData(new Object[0])),
+            null}),
+        new GenericArrayData(new Object[] {
+            new GenericArrayData(new Object[] {1, null}),
+            null,
+            new GenericArrayData(new Object[0])}));
+    GenericRowData second = GenericRowData.of(
+        null,
+        new GenericArrayData(new Object[0]),
+        new GenericArrayData(new Object[0]),
+        new GenericArrayData(new Object[0]));
+    GenericRowData third = new GenericRowData(rowType.getFieldCount());
+
+    try (BufferAllocator allocator = new RootAllocator();
+         VectorSchemaRoot root = VectorSchemaRoot.create(
+             HoodieFlinkLanceArrowUtils.toArrowSchema(rowType), allocator)) {
+      root.allocateNew();
+      LanceRowDataWriter writer = new LanceRowDataWriter(rowType, 
root.getFieldVectors(), true);
+      writer.write(first, 0);
+      writer.write(second, 1);
+      writer.write(third, 2);
+      root.getFieldVectors().forEach(vector -> vector.setValueCount(3));
+      root.setRowCount(3);
+
+      assertEquals(first, HoodieFlinkLanceArrowUtils.toRowData(rowType, 
root.getFieldVectors(), 0));
+      assertEquals(second, HoodieFlinkLanceArrowUtils.toRowData(rowType, 
root.getFieldVectors(), 1));
+      assertEquals(third, HoodieFlinkLanceArrowUtils.toRowData(rowType, 
root.getFieldVectors(), 2));
+      assertEquals(2, ((ListVector) 
root.getVector(1)).getDataVector().getValueCount());
+      assertEquals(2, ((ListVector) ((StructVector) 
root.getVector(0)).getChild("tags"))
+          .getDataVector().getValueCount());
+
+      ListVector matrixVector = (ListVector) root.getVector(3);
+      assertEquals(3, matrixVector.getDataVector().getValueCount());
+      assertEquals(2, ((ListVector) 
matrixVector.getDataVector()).getDataVector().getValueCount());
+    }
+  }
+
+  @Test
+  public void testRejectsMapType() {
+    MapType mapType = new MapType(new VarCharType(), new IntType());
+
+    try (BufferAllocator allocator = new RootAllocator();
+         IntVector vector = new IntVector("attributes", allocator)) {
+      HoodieNotSupportedException exception = 
assertThrows(HoodieNotSupportedException.class,
+          () -> new LanceRowDataWriter(
+              RowType.of(new LogicalType[] {mapType}, new String[] 
{"attributes"}),
+              Collections.singletonList(vector),
+              true));
+      assertTrue(exception.getMessage().contains(mapUnsupportedMessage()));
+    }
+  }
+
+  private static String mapUnsupportedMessage() {
+    return "Flink Lance base-file support currently supports primitive, ROW, 
and ARRAY columns;";
+  }
+
+  private static RowType nestedRowType() {
+    ArrayType tagsType = new ArrayType(new VarCharType());
+    RowType profileType = RowType.of(
+        new LogicalType[] {new VarCharType(), new IntType(), tagsType},
+        new String[] {"name", "age", "tags"});
+    ArrayType numbersType = new ArrayType(new IntType());
+    ArrayType profilesType = new ArrayType(profileType);
+    ArrayType matrixType = new ArrayType(numbersType);
+    return RowType.of(
+        new LogicalType[] {profileType, numbersType, profilesType, matrixType},
+        new String[] {"profile", "numbers", "profiles", "matrix"});
+  }
+
+  private static RowType primitiveRowType() {
+    return RowType.of(
+        new LogicalType[] {
+            new BooleanType(),
+            new TinyIntType(),
+            new SmallIntType(),
+            new IntType(),
+            new DateType(),
+            new TimeType(),
+            new BigIntType(),
+            new FloatType(),
+            new DoubleType(),
+            new CharType(4),
+            new VarCharType(),
+            new BinaryType(3),
+            new VarBinaryType(),
+            new DecimalType(10, 2),
+            new TimestampType(6),
+            new LocalZonedTimestampType(6)},
+        new String[] {
+            "boolean_value",
+            "tinyint_value",
+            "smallint_value",
+            "int_value",
+            "date_value",
+            "time_value",
+            "bigint_value",
+            "float_value",
+            "double_value",
+            "char_value",
+            "varchar_value",
+            "binary_value",
+            "varbinary_value",
+            "decimal_value",
+            "timestamp_value",
+            "local_timestamp_value"});
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/HoodieRowDataLanceReader.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/HoodieRowDataLanceReader.java
index 19a96d7c8f36..02d1f3c073f3 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/HoodieRowDataLanceReader.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/HoodieRowDataLanceReader.java
@@ -34,7 +34,7 @@ import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieIOException;
 import org.apache.hudi.exception.HoodieValidationException;
 import org.apache.hudi.io.memory.HoodieArrowAllocator;
-import org.apache.hudi.io.storage.row.HoodieFlinkLanceArrowUtils;
+import org.apache.hudi.io.storage.row.lance.HoodieFlinkLanceArrowUtils;
 import org.apache.hudi.source.ExpressionPredicates;
 import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.util.HoodieSchemaConverter;
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestHoodieDataSource.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestHoodieDataSource.java
index 4d03c3d9cc82..07c3f0c05ecd 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestHoodieDataSource.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestHoodieDataSource.java
@@ -1734,6 +1734,49 @@ public class ITTestHoodieDataSource {
     assertRowsEquals(projectedRows, "[+I[Alice, id1], +I[Bob, id2]]");
   }
 
+  @ParameterizedTest
+  @EnumSource(value = HoodieTableType.class)
+  void testLanceFormatNestedTypesUpsertWriteAndRead(HoodieTableType tableType) 
{
+    String createHoodieTable = sql("lance_nested")
+        .field("id int not null")
+        .field("ts bigint")
+        .field("f_row row(f_name varchar(10), f_age int)")
+        .field("f_array array<row(f_name varchar(10), f_age int)>")
+        .field("f_nested_array array<row(f_scores array<int>)>")
+        .pkField("id")
+        .noPartition()
+        .option(FlinkOptions.PATH, tempFile.getAbsolutePath())
+        .option(FlinkOptions.OPERATION, "upsert")
+        .option(FlinkOptions.ORDERING_FIELDS, "ts")
+        .option(FlinkOptions.TABLE_TYPE, tableType)
+        .option("hoodie.table.base.file.format", "LANCE")
+        .end();
+    batchTableEnv.executeSql(createHoodieTable);
+
+    execInsertSql(batchTableEnv, "insert into lance_nested values "
+        + "(1, 1, ROW('alice', 30), ARRAY[ROW('child1', 1), ROW('child2', 2)], 
"
+        + "ARRAY[ROW(ARRAY[1, 2]), ROW(ARRAY[3])]),"
+        + "(2, 2, ROW('bob', 31), ARRAY[ROW('child3', 3)], 
ARRAY[ROW(ARRAY[4])])");
+
+    execInsertSql(batchTableEnv, "insert into lance_nested values "
+        + "(1, 3, ROW('alice_v2', 32), ARRAY[ROW('child4', 4)], 
ARRAY[ROW(ARRAY[5, 6])]),"
+        + "(3, 4, ROW('charlie', 33), ARRAY[ROW('child5', 5)], 
ARRAY[ROW(ARRAY[7])])");
+
+    List<Row> rows = CollectionUtil.iterableToList(
+        () -> batchTableEnv.sqlQuery("select * from 
lance_nested").execute().collect());
+    assertRowsEqualsUnordered(Arrays.asList(
+        row(1, 3L, row("alice_v2", 32), array(row("child4", 4)), 
array(row((Object) array(5, 6)))),
+        row(2, 2L, row("bob", 31), array(row("child3", 3)), array(row((Object) 
array(4)))),
+        row(3, 4L, row("charlie", 33), array(row("child5", 5)), 
array(row((Object) array(7))))), rows);
+
+    List<Row> projectedRows = CollectionUtil.iterableToList(
+        () -> batchTableEnv.sqlQuery("select f_nested_array, id from 
lance_nested").execute().collect());
+    assertRowsEqualsUnordered(Arrays.asList(
+        row(array(row((Object) array(5, 6))), 1),
+        row(array(row((Object) array(4))), 2),
+        row(array(row((Object) array(7))), 3)), projectedRows);
+  }
+
   @Test
   void testLanceFormatCopyOnWriteUpsertWriteAndRead() {
     String createHoodieTable = sql("t1")

Reply via email to