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 5e58dd363aed feat(flink): support vector columns in Lance writer 
(#19831)
5e58dd363aed is described below

commit 5e58dd363aed21aa9c048b573023fff94bd798f6
Author: Shuo Cheng <[email protected]>
AuthorDate: Fri Sep 4 17:08:16 2026 +0800

    feat(flink): support vector columns in Lance writer (#19831)
---
 .../row/HoodieRowDataFileWriterFactory.java        |   5 +-
 .../io/storage/row/HoodieRowDataLanceWriter.java   |  22 +++--
 .../row/lance/HoodieFlinkLanceArrowUtils.java      |  53 +++++++++-
 .../io/storage/row/lance/LanceRowDataWriter.java   |  86 +++++++++++++++-
 .../row/TestHoodieFlinkLanceArrowUtils.java        |  66 ++++++++++++-
 .../storage/row/TestHoodieRowDataLanceWriter.java  | 103 ++++++++++++++++++++
 .../io/storage/row/TestLanceRowDataWriter.java     | 108 ++++++++++++++++++---
 7 files changed, 408 insertions(+), 35 deletions(-)

diff --git 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieRowDataFileWriterFactory.java
 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieRowDataFileWriterFactory.java
index 77054f98bf0f..7b15cfaca90d 100644
--- 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieRowDataFileWriterFactory.java
+++ 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/HoodieRowDataFileWriterFactory.java
@@ -36,9 +36,7 @@ import org.apache.hudi.storage.HoodieStorage;
 import org.apache.hudi.storage.StorageConfiguration;
 import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.storage.hadoop.HadoopStorageConfiguration;
-import org.apache.hudi.util.HoodieSchemaConverter;
 
-import org.apache.flink.table.types.logical.RowType;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FSDataOutputStream;
 
@@ -126,10 +124,9 @@ public class HoodieRowDataFileWriterFactory extends 
HoodieFileWriterFactory {
     boolean withOperation = 
config.getBooleanOrDefault(HoodieWriteConfig.ALLOW_OPERATION_METADATA_FIELD);
     Option<org.apache.hudi.common.bloom.BloomFilter> bloomFilter = 
enableBloomFilter(metaFieldsMode, config)
         ? Option.of(createBloomFilter(config)) : Option.empty();
-    RowType rowType = HoodieSchemaConverter.convertToRowType(schema);
     return new HoodieRowDataLanceWriter(
         path,
-        rowType,
+        schema,
         instantTime,
         taskContextSupplier,
         bloomFilter,
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 0990e3d6527a..238974cf3221 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
@@ -23,6 +23,7 @@ import org.apache.hudi.common.bloom.BloomFilter;
 import org.apache.hudi.common.engine.TaskContextSupplier;
 import org.apache.hudi.common.model.HoodieKey;
 import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
 import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.ValidationUtils;
 import org.apache.hudi.io.lance.HoodieBaseLanceWriter;
@@ -33,9 +34,10 @@ import org.apache.hudi.storage.StoragePath;
 import org.apache.arrow.vector.VectorSchemaRoot;
 import org.apache.arrow.vector.types.pojo.Schema;
 import org.apache.flink.table.data.RowData;
-import org.apache.flink.table.types.logical.RowType;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
 import java.util.function.Function;
 
 /**
@@ -47,7 +49,7 @@ public class HoodieRowDataLanceWriter extends 
HoodieBaseLanceWriter<RowData, Str
   private static final long MIN_RECORDS_FOR_SIZE_CHECK = 100L;
   private static final long MAX_RECORDS_FOR_SIZE_CHECK = 10000L;
 
-  private final RowType rowType;
+  private final HoodieSchema hoodieSchema;
   private final Schema arrowSchema;
   private final String fileName;
   private final String instantTime;
@@ -60,7 +62,7 @@ public class HoodieRowDataLanceWriter extends 
HoodieBaseLanceWriter<RowData, Str
 
   public HoodieRowDataLanceWriter(
       StoragePath file,
-      RowType rowType,
+      HoodieSchema hoodieSchema,
       String instantTime,
       TaskContextSupplier taskContextSupplier,
       Option<BloomFilter> bloomFilterOpt,
@@ -78,8 +80,8 @@ public class HoodieRowDataLanceWriter extends 
HoodieBaseLanceWriter<RowData, Str
     ValidationUtils.checkArgument(flushByteWatermark < allocatorSize,
         "flushByteWatermark (" + flushByteWatermark + ") must be less than 
allocatorSize ("
             + allocatorSize + ")");
-    this.rowType = rowType;
-    this.arrowSchema = HoodieFlinkLanceArrowUtils.toArrowSchema(rowType);
+    this.hoodieSchema = hoodieSchema.getNonNullType();
+    this.arrowSchema = 
HoodieFlinkLanceArrowUtils.toArrowSchema(this.hoodieSchema);
     this.fileName = file.getName();
     this.instantTime = instantTime;
     this.maxFileSize = maxFileSize;
@@ -134,6 +136,14 @@ public class HoodieRowDataLanceWriter extends 
HoodieBaseLanceWriter<RowData, Str
     return arrowSchema;
   }
 
+  @Override
+  protected Map<String, String> additionalSchemaMetadata() {
+    String value = HoodieSchema.buildVectorColumnsMetadataValue(hoodieSchema);
+    return value.isEmpty()
+        ? Collections.emptyMap()
+        : Collections.singletonMap(HoodieSchema.VECTOR_COLUMNS_METADATA_KEY, 
value);
+  }
+
   private class RowDataArrowWriter implements ArrowWriter<RowData> {
     private final VectorSchemaRoot root;
     private final LanceRowDataWriter writer;
@@ -141,7 +151,7 @@ public class HoodieRowDataLanceWriter extends 
HoodieBaseLanceWriter<RowData, Str
 
     private RowDataArrowWriter(VectorSchemaRoot root) {
       this.root = root;
-      this.writer = new LanceRowDataWriter(rowType, root.getFieldVectors(), 
utcTimestamp);
+      this.writer = new LanceRowDataWriter(hoodieSchema, 
root.getFieldVectors(), utcTimestamp);
     }
 
     @Override
diff --git 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/HoodieFlinkLanceArrowUtils.java
 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/HoodieFlinkLanceArrowUtils.java
index 6c4e4ac29cb0..9806f5e9ad1e 100644
--- 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/HoodieFlinkLanceArrowUtils.java
+++ 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/storage/row/lance/HoodieFlinkLanceArrowUtils.java
@@ -18,8 +18,12 @@
 
 package org.apache.hudi.io.storage.row.lance;
 
+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.ValidationUtils;
 import org.apache.hudi.exception.HoodieNotSupportedException;
+import org.apache.hudi.util.HoodieSchemaConverter;
 
 import org.apache.arrow.vector.BigIntVector;
 import org.apache.arrow.vector.BitVector;
@@ -72,6 +76,7 @@ import org.apache.flink.table.types.logical.VarCharType;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -88,10 +93,22 @@ public final class HoodieFlinkLanceArrowUtils {
   private HoodieFlinkLanceArrowUtils() {
   }
 
-  public static Schema toArrowSchema(RowType rowType) {
+  /**
+   * Converts a Hoodie write schema to the Arrow schema used by Lance.
+   *
+   * <p>This method distinguishes a Hoodie VECTOR from an ordinary Flink ARRAY 
and encodes
+   * top-level FLOAT/DOUBLE vectors as Arrow FixedSizeList fields.
+   */
+  public static Schema toArrowSchema(HoodieSchema hoodieSchema) {
+    HoodieSchema recordSchema = hoodieSchema.getNonNullType();
+    RowType rowType = HoodieSchemaConverter.convertToRowType(recordSchema);
+    List<HoodieSchemaField> hoodieFields = recordSchema.getFields();
+
     List<Field> fields = new ArrayList<>(rowType.getFieldCount());
-    for (RowType.RowField field : rowType.getFields()) {
-      fields.add(toArrowField(field.getName(), field.getType()));
+    for (int i = 0; i < rowType.getFieldCount(); i++) {
+      RowType.RowField rowField = rowType.getFields().get(i);
+      fields.add(toArrowField(
+          rowField.getName(), rowField.getType(), 
hoodieFields.get(i).schema().getNonNullType()));
     }
     return new Schema(fields);
   }
@@ -180,6 +197,36 @@ public final class HoodieFlinkLanceArrowUtils {
     return new Field(name, new FieldType(type.isNullable(), toArrowType(type), 
null), children);
   }
 
+  private static Field toArrowField(String name, LogicalType type, 
HoodieSchema hoodieSchema) {
+    if (hoodieSchema.getType() != HoodieSchemaType.VECTOR) {
+      return toArrowField(name, type);
+    }
+
+    HoodieSchema.Vector vectorSchema = (HoodieSchema.Vector) hoodieSchema;
+    validateLanceVector(name, vectorSchema);
+    ValidationUtils.checkArgument(type instanceof ArrayType,
+        "VECTOR column '" + name + "' must map to a Flink ARRAY type");
+    ArrayType arrayType = (ArrayType) type;
+    Field elementField = toArrowField("element", arrayType.getElementType());
+    return new Field(
+        name,
+        new FieldType(
+            type.isNullable(),
+            new ArrowType.FixedSizeList(vectorSchema.getDimension()),
+            null),
+        Collections.singletonList(elementField));
+  }
+
+  private static void validateLanceVector(String fieldName, 
HoodieSchema.Vector vectorSchema) {
+    HoodieSchema.Vector.VectorElementType elementType = 
vectorSchema.getVectorElementType();
+    if (elementType != HoodieSchema.Vector.VectorElementType.FLOAT
+        && elementType != HoodieSchema.Vector.VectorElementType.DOUBLE) {
+      throw new HoodieNotSupportedException(
+          "Lance base-file format currently supports FLOAT/DOUBLE VECTOR 
columns only; "
+              + "got element type " + elementType + " for field '" + fieldName 
+ "'");
+    }
+  }
+
   private static ArrowType toArrowType(LogicalType type) {
     switch (type.getTypeRoot()) {
       case BOOLEAN:
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
index 41c2481d5987..7d85b887fb05 100644
--- 
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
@@ -18,7 +18,12 @@
 
 package org.apache.hudi.io.storage.row.lance;
 
+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.schema.HoodieSchemaUtils;
 import org.apache.hudi.exception.HoodieNotSupportedException;
+import org.apache.hudi.util.HoodieSchemaConverter;
 
 import org.apache.arrow.vector.BigIntVector;
 import org.apache.arrow.vector.BitVector;
@@ -34,6 +39,7 @@ 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.FixedSizeListVector;
 import org.apache.arrow.vector.complex.ListVector;
 import org.apache.arrow.vector.complex.StructVector;
 import org.apache.flink.table.data.ArrayData;
@@ -64,10 +70,16 @@ public class LanceRowDataWriter {
 
   private final FieldWriter[] fieldWriters;
 
-  public LanceRowDataWriter(RowType rowType, List<FieldVector> vectors, 
boolean utcTimestamp) {
+  public LanceRowDataWriter(
+      HoodieSchema hoodieSchema, List<FieldVector> vectors, boolean 
utcTimestamp) {
+    HoodieSchema recordSchema = hoodieSchema.getNonNullType();
+    RowType rowType = HoodieSchemaConverter.convertToRowType(recordSchema);
+    List<HoodieSchemaField> hoodieFields = recordSchema.getFields();
     this.fieldWriters = new FieldWriter[rowType.getFieldCount()];
     for (int i = 0; i < fieldWriters.length; i++) {
-      fieldWriters[i] = createWriter(rowType.getTypeAt(i), vectors.get(i), 
utcTimestamp);
+      HoodieSchema fieldSchema = hoodieFields.get(i).schema().getNonNullType();
+      fieldWriters[i] = createWriter(
+          rowType.getTypeAt(i), fieldSchema, vectors.get(i), utcTimestamp, 
hoodieFields.get(i).name());
     }
   }
 
@@ -77,7 +89,12 @@ public class LanceRowDataWriter {
     }
   }
 
-  private static FieldWriter createWriter(LogicalType type, FieldVector 
vector, boolean utcTimestamp) {
+  private static FieldWriter createWriter(
+      LogicalType type,
+      HoodieSchema hoodieSchema,
+      FieldVector vector,
+      boolean utcTimestamp,
+      String fieldPath) {
     switch (type.getTypeRoot()) {
       case BOOLEAN:
         return new BooleanWriter((BitVector) vector);
@@ -113,15 +130,36 @@ public class LanceRowDataWriter {
         StructVector structVector = (StructVector) vector;
         FieldWriter[] fieldWriters = new FieldWriter[rowType.getFieldCount()];
         for (int i = 0; i < fieldWriters.length; i++) {
+          String childName = rowType.getFieldNames().get(i);
           fieldWriters[i] = createWriter(
-              rowType.getTypeAt(i), (FieldVector) 
structVector.getChildByOrdinal(i), utcTimestamp);
+              rowType.getTypeAt(i),
+              HoodieSchemaUtils.getFieldSchema(hoodieSchema, 
childName).getNonNullType(),
+              (FieldVector) structVector.getChildByOrdinal(i),
+              utcTimestamp,
+              fieldPath + "." + childName);
         }
         return new RowWriter(structVector, fieldWriters);
       case ARRAY:
         ArrayType arrayType = (ArrayType) type;
+        if (hoodieSchema != null && hoodieSchema.getType() == 
HoodieSchemaType.VECTOR) {
+          HoodieSchema.Vector vectorSchema = (HoodieSchema.Vector) 
hoodieSchema;
+          FixedSizeListVector fixedSizeListVector = (FixedSizeListVector) 
vector;
+          FieldWriter vectorElementWriter = createWriter(
+              arrayType.getElementType(),
+              null,
+              fixedSizeListVector.getDataVector(),
+              utcTimestamp,
+              fieldPath + "[]");
+          return new VectorWriter(
+              fixedSizeListVector, vectorElementWriter, 
vectorSchema.getDimension(), fieldPath);
+        }
         ListVector listVector = (ListVector) vector;
         FieldWriter elementWriter = createWriter(
-            arrayType.getElementType(), listVector.getDataVector(), 
utcTimestamp);
+            arrayType.getElementType(),
+            hoodieSchema.getElementType().getNonNullType(),
+            listVector.getDataVector(),
+            utcTimestamp,
+            fieldPath + "[]");
         return new ArrayWriter(listVector, elementWriter);
       default:
         throw unsupported(type);
@@ -468,6 +506,44 @@ public class LanceRowDataWriter {
     }
   }
 
+  private static class VectorWriter extends FieldWriter {
+    private final FixedSizeListVector vector;
+    private final FieldWriter elementWriter;
+    private final int dimension;
+    private final String fieldPath;
+
+    private VectorWriter(
+        FixedSizeListVector vector, FieldWriter elementWriter, int dimension, 
String fieldPath) {
+      super(vector);
+      this.vector = vector;
+      this.elementWriter = elementWriter;
+      this.dimension = dimension;
+      this.fieldPath = fieldPath;
+    }
+
+    @Override
+    void writeNonNull(RowData row, int ordinal, int rowId) {
+      writeVector(row.getArray(ordinal), rowId);
+    }
+
+    @Override
+    void writeNonNull(ArrayData array, int ordinal, int rowId) {
+      writeVector(array.getArray(ordinal), rowId);
+    }
+
+    private void writeVector(ArrayData array, int rowId) {
+      if (array.size() != dimension) {
+        throw new IllegalArgumentException(
+            "VECTOR column '" + fieldPath + "' requires dimension " + dimension
+                + " but row contains " + array.size() + " elements");
+      }
+      int startIndex = vector.startNewValue(rowId);
+      for (int i = 0; i < dimension; i++) {
+        elementWriter.write(array, i, startIndex + i);
+      }
+    }
+  }
+
   private static long timestampToMicros(TimestampData timestampData, int 
precision, boolean utcTimestamp) {
     long millis = utcTimestamp ? timestampData.getMillisecond() : 
timestampData.toTimestamp().getTime();
     return precision > 3 && utcTimestamp
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 007148a4df97..dc0b20839c58 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,30 +18,38 @@
 
 package org.apache.hudi.io.storage.row;
 
+import org.apache.hudi.common.schema.HoodieSchema;
 import org.apache.hudi.exception.HoodieNotSupportedException;
 import org.apache.hudi.io.storage.row.lance.HoodieFlinkLanceArrowUtils;
+import org.apache.hudi.util.HoodieSchemaConverter;
 
 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.Field;
 import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.types.pojo.Schema;
 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.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.TimestampType;
+import org.apache.flink.table.types.logical.TinyIntType;
 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.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -56,9 +64,10 @@ public class TestHoodieFlinkLanceArrowUtils {
     RowType rowType = RowType.of(
         new LogicalType[] {new TimestampType(6), new 
LocalZonedTimestampType(6)},
         new String[] {"timestamp", "local_timestamp"});
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(rowType, 
"record");
 
     RowType roundTripped = HoodieFlinkLanceArrowUtils.toRowType(
-        HoodieFlinkLanceArrowUtils.toArrowSchema(rowType));
+        HoodieFlinkLanceArrowUtils.toArrowSchema(hoodieSchema));
 
     assertInstanceOf(TimestampType.class, roundTripped.getTypeAt(0));
     assertInstanceOf(LocalZonedTimestampType.class, roundTripped.getTypeAt(1));
@@ -86,22 +95,69 @@ public class TestHoodieFlinkLanceArrowUtils {
   @Test
   public void testNestedSchemaRoundTrip() {
     RowType rowType = nestedRowType();
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(rowType, 
"record");
 
     RowType roundTripped = HoodieFlinkLanceArrowUtils.toRowType(
-        HoodieFlinkLanceArrowUtils.toArrowSchema(rowType));
+        HoodieFlinkLanceArrowUtils.toArrowSchema(hoodieSchema));
 
     assertEquals(rowType, roundTripped);
   }
 
   @Test
   public void testRejectsMapTypeWhenWritingSchema() {
-    HoodieNotSupportedException exception = 
assertThrows(HoodieNotSupportedException.class,
-        () -> HoodieFlinkLanceArrowUtils.toArrowSchema(RowType.of(
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(
+        RowType.of(
             new LogicalType[] {new MapType(new VarCharType(), new IntType())},
-            new String[] {"attributes"})));
+            new String[] {"attributes"}),
+        "record");
+    HoodieNotSupportedException exception = 
assertThrows(HoodieNotSupportedException.class,
+        () -> HoodieFlinkLanceArrowUtils.toArrowSchema(hoodieSchema));
     assertTrue(exception.getMessage().contains("Flink Lance base-file support 
currently supports primitive, ROW, and ARRAY columns;"));
   }
 
+  @Test
+  public void testVectorColumnsUseFixedSizeListAndOrdinaryArraysRemainLists() {
+    RowType rowType = RowType.of(
+        new LogicalType[] {
+            new ArrayType(new FloatType()),
+            new ArrayType(new FloatType(false)),
+            new ArrayType(new DoubleType(false))},
+        new String[] {"values", "embedding", "scores"});
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(
+        rowType, "vector_record", "embedding:2,scores:3");
+
+    Schema arrowSchema = 
HoodieFlinkLanceArrowUtils.toArrowSchema(hoodieSchema);
+
+    assertInstanceOf(ArrowType.List.class, 
arrowSchema.findField("values").getType());
+    assertFixedSizeList(arrowSchema.findField("embedding"), 2);
+    assertFixedSizeList(arrowSchema.findField("scores"), 3);
+  }
+
+  @Test
+  public void testRejectsInt8VectorForLance() {
+    RowType rowType = RowType.of(
+        new LogicalType[] {new ArrayType(new TinyIntType(false))},
+        new String[] {"embedding"});
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(
+        rowType, "vector_record", "embedding:2");
+
+    HoodieNotSupportedException exception = assertThrows(
+        HoodieNotSupportedException.class,
+        () -> HoodieFlinkLanceArrowUtils.toArrowSchema(hoodieSchema));
+
+    assertTrue(exception.getMessage().contains("FLOAT/DOUBLE VECTOR columns 
only"));
+    assertTrue(exception.getMessage().contains("embedding"));
+  }
+
+  private static void assertFixedSizeList(Field field, int dimension) {
+    ArrowType.FixedSizeList fixedSizeList =
+        assertInstanceOf(ArrowType.FixedSizeList.class, field.getType());
+    assertEquals(dimension, fixedSizeList.getListSize());
+    assertEquals("element", field.getChildren().get(0).getName());
+    assertFalse(field.getChildren().get(0).isNullable());
+    assertTrue(field.getMetadata().isEmpty());
+  }
+
   private static RowType nestedRowType() {
     RowType profileType = RowType.of(
         new LogicalType[] {new VarCharType(), new IntType()},
diff --git 
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestHoodieRowDataLanceWriter.java
 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestHoodieRowDataLanceWriter.java
new file mode 100644
index 000000000000..b7303a61e93f
--- /dev/null
+++ 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/io/storage/row/TestHoodieRowDataLanceWriter.java
@@ -0,0 +1,103 @@
+/*
+ * 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.common.engine.TaskContextSupplier;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.util.HoodieSchemaConverter;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.Float4Vector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.complex.FixedSizeListVector;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.flink.table.data.GenericArrayData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.types.logical.ArrayType;
+import org.apache.flink.table.types.logical.FloatType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.lance.file.LanceFileReader;
+
+import java.nio.file.Path;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+/** Tests for {@link HoodieRowDataLanceWriter}. */
+public class TestHoodieRowDataLanceWriter {
+
+  @TempDir
+  Path tempDir;
+
+  @Test
+  public void testWritesVectorDataAndFooterMetadata() throws Exception {
+    RowType rowType = RowType.of(
+        new LogicalType[] {new IntType(false), new ArrayType(new 
FloatType(false))},
+        new String[] {"id", "embedding"});
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(
+        rowType, "vector_record", "embedding:2");
+    StoragePath path = new 
StoragePath(tempDir.resolve("vector.lance").toUri());
+
+    try (HoodieRowDataLanceWriter writer = new HoodieRowDataLanceWriter(
+        path,
+        hoodieSchema,
+        "001",
+        mock(TaskContextSupplier.class),
+        Option.empty(),
+        128 * 1024 * 1024L,
+        64 * 1024 * 1024L,
+        16 * 1024 * 1024L,
+        true,
+        false,
+        false)) {
+      writer.writeRow("key1", GenericRowData.of(
+          1, new GenericArrayData(new Object[] {1.25F, 2.5F})));
+    }
+
+    try (BufferAllocator allocator = new RootAllocator();
+         LanceFileReader reader = LanceFileReader.open(path.toString(), 
allocator);
+         ArrowReader arrowReader = reader.readAll(null, null, 
Integer.MAX_VALUE)) {
+      assertEquals(1, reader.numRows());
+      Map<String, String> metadata = reader.schema().getCustomMetadata();
+      assertEquals("embedding:VECTOR(2)",
+          metadata.get(HoodieSchema.VECTOR_COLUMNS_METADATA_KEY));
+      ArrowType.FixedSizeList fixedSizeList = assertInstanceOf(
+          ArrowType.FixedSizeList.class, 
reader.schema().findField("embedding").getType());
+      assertEquals(2, fixedSizeList.getListSize());
+
+      assertTrue(arrowReader.loadNextBatch());
+      VectorSchemaRoot root = arrowReader.getVectorSchemaRoot();
+      FixedSizeListVector vector = (FixedSizeListVector) 
root.getVector("embedding");
+      Float4Vector elements = (Float4Vector) vector.getDataVector();
+      assertEquals(1.25F, elements.get(0));
+      assertEquals(2.5F, elements.get(1));
+    }
+  }
+}
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
index d43c2ad6d9b1..19e28f345224 100644
--- 
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
@@ -18,15 +18,20 @@
 
 package org.apache.hudi.io.storage.row;
 
+import org.apache.hudi.common.schema.HoodieSchema;
 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.hudi.util.HoodieSchemaConverter;
 
 import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.Float4Vector;
+import org.apache.arrow.vector.Float8Vector;
 import org.apache.arrow.vector.IntVector;
 import org.apache.arrow.vector.TimeStampMicroVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.complex.FixedSizeListVector;
 import org.apache.arrow.vector.complex.ListVector;
 import org.apache.arrow.vector.complex.StructVector;
 import org.apache.arrow.vector.types.TimeUnit;
@@ -65,6 +70,8 @@ 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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -75,14 +82,15 @@ public class TestLanceRowDataWriter {
 
   @Test
   public void testPrimitiveValuesAndNulls() {
-    RowType rowType = primitiveRowType();
+    HoodieSchema hoodieSchema = 
HoodieSchemaConverter.convertToSchema(primitiveRowType(), "record");
+    RowType rowType = 
HoodieSchemaConverter.convertToRowType(hoodieSchema.getNonNullType());
     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,
+        12,
+        1234,
         123456,
         20000,
         12345678,
@@ -100,9 +108,9 @@ public class TestLanceRowDataWriter {
 
     try (BufferAllocator allocator = new RootAllocator();
          VectorSchemaRoot root = VectorSchemaRoot.create(
-             HoodieFlinkLanceArrowUtils.toArrowSchema(rowType), allocator)) {
+             HoodieFlinkLanceArrowUtils.toArrowSchema(hoodieSchema), 
allocator)) {
       root.allocateNew();
-      LanceRowDataWriter writer = new LanceRowDataWriter(rowType, 
root.getFieldVectors(), true);
+      LanceRowDataWriter writer = new LanceRowDataWriter(hoodieSchema, 
root.getFieldVectors(), true);
       writer.write(values, 0);
       writer.write(nulls, 1);
       root.getFieldVectors().forEach(vector -> vector.setValueCount(2));
@@ -110,8 +118,8 @@ public class TestLanceRowDataWriter {
 
       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(12, actual.getInt(1));
+      assertEquals(1234, actual.getInt(2));
       assertEquals(123456, actual.getInt(3));
       assertEquals(20000, actual.getInt(4));
       assertEquals(12345678, actual.getInt(5));
@@ -144,10 +152,11 @@ public class TestLanceRowDataWriter {
              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);
+      HoodieSchema hoodieSchema = 
HoodieSchemaConverter.convertToSchema(rowType, "record");
+      new LanceRowDataWriter(hoodieSchema, Collections.singletonList(vector), 
true).write(rowData, 0);
       assertEquals(1234567L, vector.get(0));
 
-      new LanceRowDataWriter(rowType, Collections.singletonList(vector), 
false).write(rowData, 1);
+      new LanceRowDataWriter(hoodieSchema, Collections.singletonList(vector), 
false).write(rowData, 1);
       assertEquals(timestampData.toTimestamp().getTime() * 1000L, 
vector.get(1));
     }
   }
@@ -155,6 +164,7 @@ public class TestLanceRowDataWriter {
   @Test
   public void testNestedValueRoundTripAndListValueCount() {
     RowType rowType = nestedRowType();
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(rowType, 
"record");
     GenericRowData first = GenericRowData.of(
         GenericRowData.of(
             StringData.fromString("alice"),
@@ -180,9 +190,9 @@ public class TestLanceRowDataWriter {
 
     try (BufferAllocator allocator = new RootAllocator();
          VectorSchemaRoot root = VectorSchemaRoot.create(
-             HoodieFlinkLanceArrowUtils.toArrowSchema(rowType), allocator)) {
+             HoodieFlinkLanceArrowUtils.toArrowSchema(hoodieSchema), 
allocator)) {
       root.allocateNew();
-      LanceRowDataWriter writer = new LanceRowDataWriter(rowType, 
root.getFieldVectors(), true);
+      LanceRowDataWriter writer = new LanceRowDataWriter(hoodieSchema, 
root.getFieldVectors(), true);
       writer.write(first, 0);
       writer.write(second, 1);
       writer.write(third, 2);
@@ -205,18 +215,92 @@ public class TestLanceRowDataWriter {
   @Test
   public void testRejectsMapType() {
     MapType mapType = new MapType(new VarCharType(), new IntType());
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(
+        RowType.of(new LogicalType[] {mapType}, new String[] {"attributes"}), 
"record");
 
     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"}),
+              hoodieSchema,
               Collections.singletonList(vector),
               true));
       assertTrue(exception.getMessage().contains(mapUnsupportedMessage()));
     }
   }
 
+  @Test
+  public void testWritesFloatAndDoubleVectors() {
+    RowType rowType = RowType.of(
+        new LogicalType[] {
+            new ArrayType(new FloatType(false)),
+            new ArrayType(new DoubleType(false)),
+            new ArrayType(new IntType())},
+        new String[] {"embedding", "scores", "values"});
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(
+        rowType, "vector_record", "embedding:2,scores:3");
+    GenericRowData first = GenericRowData.of(
+        new GenericArrayData(new Object[] {1.25F, 2.5F}),
+        new GenericArrayData(new Object[] {3.0D, 4.0D, 5.0D}),
+        new GenericArrayData(new Object[] {6, null}));
+    GenericRowData second = GenericRowData.of(
+        null,
+        new GenericArrayData(new Object[] {7.0D, 8.0D, 9.0D}),
+        new GenericArrayData(new Object[0]));
+
+    try (BufferAllocator allocator = new RootAllocator();
+         VectorSchemaRoot root = VectorSchemaRoot.create(
+             HoodieFlinkLanceArrowUtils.toArrowSchema(hoodieSchema), 
allocator)) {
+      root.allocateNew();
+      LanceRowDataWriter writer = new LanceRowDataWriter(
+          hoodieSchema, root.getFieldVectors(), true);
+      writer.write(first, 0);
+      writer.write(second, 1);
+      root.getFieldVectors().forEach(vector -> vector.setValueCount(2));
+      root.setRowCount(2);
+
+      FixedSizeListVector floatVector = (FixedSizeListVector) 
root.getVector("embedding");
+      Float4Vector floatElements = (Float4Vector) floatVector.getDataVector();
+      assertFalse(floatVector.isNull(0));
+      assertTrue(floatVector.isNull(1));
+      assertEquals(1.25F, floatElements.get(0));
+      assertEquals(2.5F, floatElements.get(1));
+
+      FixedSizeListVector doubleVector = (FixedSizeListVector) 
root.getVector("scores");
+      Float8Vector doubleElements = (Float8Vector) 
doubleVector.getDataVector();
+      assertEquals(3, doubleVector.getListSize());
+      assertEquals(3.0D, doubleElements.get(0));
+      assertEquals(5.0D, doubleElements.get(2));
+      assertEquals(7.0D, doubleElements.get(3));
+      assertEquals(9.0D, doubleElements.get(5));
+
+      assertInstanceOf(ListVector.class, root.getVector("values"));
+    }
+  }
+
+  @Test
+  public void testRejectsInvalidVectorDimension() {
+    RowType rowType = RowType.of(
+        new LogicalType[] {new ArrayType(new FloatType(false))},
+        new String[] {"embedding"});
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(
+        rowType, "vector_record", "embedding:2");
+
+    try (BufferAllocator allocator = new RootAllocator();
+         VectorSchemaRoot root = VectorSchemaRoot.create(
+             HoodieFlinkLanceArrowUtils.toArrowSchema(hoodieSchema), 
allocator)) {
+      root.allocateNew();
+      LanceRowDataWriter writer = new LanceRowDataWriter(
+          hoodieSchema, root.getFieldVectors(), true);
+
+      IllegalArgumentException dimensionError = assertThrows(
+          IllegalArgumentException.class,
+          () -> writer.write(GenericRowData.of(
+              new GenericArrayData(new Object[] {1.0F})), 0));
+      assertTrue(dimensionError.getMessage().contains("requires dimension 2"));
+    }
+  }
+
   private static String mapUnsupportedMessage() {
     return "Flink Lance base-file support currently supports primitive, ROW, 
and ARRAY columns;";
   }

Reply via email to