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 d9e6f42045cd feat(flink): support vector columns in Lance reader 
(#19842)
d9e6f42045cd is described below

commit d9e6f42045cdf69f0d3f6f9627125338cbfeee39
Author: Shuo Cheng <[email protected]>
AuthorDate: Tue Sep 8 12:18:47 2026 +0800

    feat(flink): support vector columns in Lance reader (#19842)
    
    * feat(flink): support vector columns in Lance reader
---
 .../row/lance/HoodieFlinkLanceArrowUtils.java      |  22 +-
 .../apache/hudi/util/HoodieSchemaConverter.java    |  18 +-
 .../org/apache/hudi/util/VectorColumnParser.java   |  14 +-
 .../apache/hudi/util/VectorConversionUtils.java    |   9 +-
 .../hudi/util/TestHoodieSchemaConverter.java       |  16 ++
 .../hudi/util/TestVectorConversionUtils.java       |  14 +-
 .../org/apache/hudi/table/format/FormatUtils.java  |  15 +-
 .../table/format/HoodieRowDataLanceReader.java     |  78 ++++++-
 .../table/format/cow/CopyOnWriteInputFormat.java   |  20 +-
 .../apache/hudi/table/ITTestVectorDataSource.java  |  60 ++++++
 .../table/format/TestHoodieRowDataLanceReader.java | 229 ++++++++++++++++++++-
 .../format/cow/TestCopyOnWriteInputFormat.java     |  63 ++++++
 12 files changed, 515 insertions(+), 43 deletions(-)

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 9806f5e9ad1e..896ce10af1bb 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
@@ -40,6 +40,8 @@ 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.BaseListVector;
+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.DateUnit;
@@ -174,7 +176,7 @@ public final class HoodieFlinkLanceArrowUtils {
       case ROW:
         return readRow((RowType) type, (StructVector) vector, rowId);
       case ARRAY:
-        return readArray((ArrayType) type, (ListVector) vector, rowId);
+        return readArray((ArrayType) type, vector, rowId);
       default:
         throw unsupported(type);
     }
@@ -219,6 +221,9 @@ public final class HoodieFlinkLanceArrowUtils {
 
   private static void validateLanceVector(String fieldName, 
HoodieSchema.Vector vectorSchema) {
     HoodieSchema.Vector.VectorElementType elementType = 
vectorSchema.getVectorElementType();
+    // Keep the on-disk encoding aligned with Spark Lance writes. The current 
lance-spark
+    // VectorUtils.shouldBeFixedSizeList recognizes only Array<Float> and 
Array<Double>;
+    // an INT8 vector would otherwise be emitted as a variable-size List 
instead of FixedSizeList.
     if (elementType != HoodieSchema.Vector.VectorElementType.FLOAT
         && elementType != HoodieSchema.Vector.VectorElementType.DOUBLE) {
       throw new HoodieNotSupportedException(
@@ -319,9 +324,9 @@ public final class HoodieFlinkLanceArrowUtils {
         fields.add(new RowType.RowField(child.getName(), toLogicalType(child, 
path + "." + child.getName())));
       }
       logicalType = new RowType(field.isNullable(), fields);
-    } else if (arrowType instanceof ArrowType.List) {
+    } else if (arrowType instanceof ArrowType.List || arrowType instanceof 
ArrowType.FixedSizeList) {
       ValidationUtils.checkArgument(field.getChildren().size() == 1,
-          String.format("Unsupported Arrow schema at '%s': LIST must contain 
exactly one child field", path));
+          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 {
@@ -338,11 +343,14 @@ public final class HoodieFlinkLanceArrowUtils {
     return row;
   }
 
-  private static ArrayData readArray(ArrayType arrayType, ListVector vector, 
int rowId) {
-    int startIndex = vector.getElementStartIndex(rowId);
-    int endIndex = vector.getElementEndIndex(rowId);
+  private static ArrayData readArray(ArrayType arrayType, ValueVector vector, 
int rowId) {
+    BaseListVector listVector = (BaseListVector) vector;
+    FieldVector dataVector = vector instanceof FixedSizeListVector
+        ? ((FixedSizeListVector) vector).getDataVector()
+        : ((ListVector) vector).getDataVector();
+    int startIndex = listVector.getElementStartIndex(rowId);
+    int endIndex = listVector.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);
     }
diff --git 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/HoodieSchemaConverter.java
 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/HoodieSchemaConverter.java
index af992d1a18f4..c0a267e32bba 100644
--- 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/HoodieSchemaConverter.java
+++ 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/HoodieSchemaConverter.java
@@ -103,16 +103,24 @@ public class HoodieSchemaConverter {
       String vectorColumns) {
     Map<String, Integer> vectorColumnMap = vectorColumns == null || 
vectorColumns.trim().isEmpty()
         ? Collections.emptyMap() : VectorColumnParser.parse(vectorColumns);
-    validateVectorColumns(logicalType, vectorColumnMap);
     return convertToSchema(logicalType, rowName, vectorColumnMap);
   }
 
-  private static HoodieSchema convertToSchema(
+  /**
+   * Converts a Flink LogicalType into a HoodieSchema with the specified 
top-level VECTOR columns.
+   *
+   * @param logicalType   Flink logical type
+   * @param rowName       the record name
+   * @param vectorColumns vector column names and dimensions
+   * @return HoodieSchema matching this logical type
+   */
+  public static HoodieSchema convertToSchema(
       LogicalType logicalType,
       String rowName,
       Map<String, Integer> vectorColumns) {
     ValidationUtils.checkArgument(vectorColumns.isEmpty() || logicalType 
instanceof RowType,
         "VECTOR columns can only be configured for top-level ROW schemas.");
+    validateVectorColumns(logicalType, vectorColumns);
 
     int precision;
     boolean nullable = logicalType.isNullable();
@@ -285,15 +293,15 @@ public class HoodieSchemaConverter {
    * an unknown column is rejected instead of being silently ignored during 
conversion.
    *
    * @param logicalType   Flink logical type
-   * @param vectorColumns parsed vector columns (normalized column name to 
dimension), may be empty
+   * @param vectorColumns parsed vector columns (column name to dimension), 
may be empty
    */
   private static void validateVectorColumns(LogicalType logicalType, 
Map<String, Integer> vectorColumns) {
     if (vectorColumns.isEmpty()) {
       return;
     }
-    List<String> normalizedFieldNames = ((RowType) 
logicalType).getFieldNames();
+    List<String> fieldNames = ((RowType) logicalType).getFieldNames();
     vectorColumns.keySet().stream()
-        .filter(vectorColumn -> !normalizedFieldNames.contains(vectorColumn))
+        .filter(vectorColumn -> !fieldNames.contains(vectorColumn))
         .findFirst()
         .ifPresent(vectorColumn -> {
           throw new IllegalArgumentException("VECTOR column '" + vectorColumn 
+ "' does not exist in the table schema.");
diff --git 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/VectorColumnParser.java
 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/VectorColumnParser.java
index 68724be54cb6..4af139b2a05f 100644
--- 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/VectorColumnParser.java
+++ 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/VectorColumnParser.java
@@ -24,7 +24,6 @@ import org.apache.flink.table.types.logical.ArrayType;
 import org.apache.flink.table.types.logical.LogicalType;
 
 import java.util.LinkedHashMap;
-import java.util.Locale;
 import java.util.Map;
 
 /**
@@ -33,8 +32,8 @@ import java.util.Map;
  *
  * <p>The option uses {@code colName[:dimension]} entries separated by commas, 
for example
  * {@code embedding:4,features:3,codes:4}. The dimension defaults to {@value 
#DEFAULT_VECTOR_DIMENSION}
- * when omitted. Column names are matched case-insensitively. The vector 
element type is inferred
- * from the Flink array element type: {@code ARRAY<FLOAT>}, {@code 
ARRAY<DOUBLE>}, or
+ * when omitted. The vector element type is inferred from the Flink array 
element type:
+ * {@code ARRAY<FLOAT>}, {@code ARRAY<DOUBLE>}, or
  * {@code ARRAY<TINYINT>} map to FLOAT, DOUBLE, and INT8 respectively.
  *
  * <p>The parser validates the descriptor syntax (well-formed entries, no 
duplicate columns,
@@ -50,10 +49,10 @@ public class VectorColumnParser {
 
   /**
    * Parses the {@code hoodie.vector.columns} descriptor string into a map 
from the
-   * (lower-cased) column name to its vector dimension, preserving declaration 
order.
+   * column name to its vector dimension, preserving declaration order.
    *
    * @param vectorColumns comma-separated {@code colName[:dimension]} 
descriptors
-   * @return map from normalized column name to dimension
+   * @return map from column name to dimension
    * @throws IllegalArgumentException if a descriptor is malformed, 
duplicated, or has a non-positive dimension
    */
   public static Map<String, Integer> parse(String vectorColumns) {
@@ -69,8 +68,7 @@ public class VectorColumnParser {
             "Invalid VECTOR column descriptor '" + entry + "'. Expected 
format: columnName[:dimension].");
       }
       String columnName = parts[0].trim();
-      String normalizedColumnName = columnName.toLowerCase(Locale.ROOT);
-      if (parsed.containsKey(normalizedColumnName)) {
+      if (parsed.containsKey(columnName)) {
         throw new IllegalArgumentException("Duplicate VECTOR column descriptor 
for column: " + columnName);
       }
       int dimension = DEFAULT_VECTOR_DIMENSION;
@@ -89,7 +87,7 @@ public class VectorColumnParser {
       if (dimension <= 0) {
         throw new IllegalArgumentException("VECTOR dimension must be positive 
for column '" + columnName + "': " + dimension);
       }
-      parsed.put(normalizedColumnName, dimension);
+      parsed.put(columnName, dimension);
     }
     return parsed;
   }
diff --git 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/VectorConversionUtils.java
 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/VectorConversionUtils.java
index c3721bae71ea..85488d2cd241 100644
--- 
a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/VectorConversionUtils.java
+++ 
b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/util/VectorConversionUtils.java
@@ -42,7 +42,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.stream.Collectors;
 
@@ -76,7 +75,7 @@ public final class VectorConversionUtils {
     Map<String, HoodieSchema.Vector> vectorFields = 
getVectorFields(tableSchema);
     Map<Integer, HoodieSchema.Vector> vectorColumnInfo = new LinkedHashMap<>();
     for (int i = 0; i < selectedFields.length; i++) {
-      HoodieSchema.Vector vector = 
vectorFields.get(fullFieldNames[selectedFields[i]].toLowerCase(Locale.ROOT));
+      HoodieSchema.Vector vector = 
vectorFields.get(fullFieldNames[selectedFields[i]]);
       if (vector != null) {
         vectorColumnInfo.put(i, vector);
       }
@@ -143,7 +142,7 @@ public final class VectorConversionUtils {
     }
     DataType[] readFieldTypes = Arrays.copyOf(fullFieldTypes, 
fullFieldTypes.length);
     for (int i = 0; i < fullFieldNames.length; i++) {
-      if 
(vectorFields.containsKey(fullFieldNames[i].toLowerCase(Locale.ROOT))) {
+      if (vectorFields.containsKey(fullFieldNames[i])) {
         readFieldTypes[i] = 
DataTypes.of(bytesType(fullFieldTypes[i].getLogicalType()));
       }
     }
@@ -192,13 +191,13 @@ public final class VectorConversionUtils {
   }
 
   /**
-   * Returns VECTOR fields keyed by lower-cased field name.
+   * Returns VECTOR fields keyed by field name.
    */
   private static Map<String, HoodieSchema.Vector> getVectorFields(HoodieSchema 
tableSchema) {
     return tableSchema.getFields().stream()
         .filter(field -> field.schema().getNonNullType().getType() == 
HoodieSchemaType.VECTOR)
         .collect(Collectors.toMap(
-            field -> field.name().toLowerCase(Locale.ROOT),
+            field -> field.name(),
             field -> (HoodieSchema.Vector) field.schema().getNonNullType()));
   }
 
diff --git 
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestHoodieSchemaConverter.java
 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestHoodieSchemaConverter.java
index 22a7c711ea4e..0acfee129694 100644
--- 
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestHoodieSchemaConverter.java
+++ 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestHoodieSchemaConverter.java
@@ -560,6 +560,22 @@ public class TestHoodieSchemaConverter {
     assertEquals(2, ((HoodieSchema.Vector) defaultEmbedding).getDimension());
   }
 
+  @Test
+  public void testVectorColumnNamesAreCaseSensitive() {
+    RowType rowType = (RowType) DataTypes.ROW(
+        DataTypes.FIELD("Embedding", 
DataTypes.ARRAY(DataTypes.FLOAT().notNull()).notNull()))
+        .notNull()
+        .getLogicalType();
+
+    HoodieSchema schema = HoodieSchemaConverter.convertToSchema(rowType, 
"test_record", "Embedding:4");
+    HoodieSchema vector = 
schema.getField("Embedding").get().schema().getNonNullType();
+    assertEquals(HoodieSchemaType.VECTOR, vector.getType());
+    assertEquals(4, ((HoodieSchema.Vector) vector).getDimension());
+
+    assertThrows(IllegalArgumentException.class,
+        () -> HoodieSchemaConverter.convertToSchema(rowType, "test_record", 
"embedding:4"));
+  }
+
   @Test
   public void testConvertVectorColumnsValidation() {
     RowType rowType = (RowType) DataTypes.ROW(
diff --git 
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestVectorConversionUtils.java
 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestVectorConversionUtils.java
index e5a2e8478fd2..fbbde80a8051 100644
--- 
a/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestVectorConversionUtils.java
+++ 
b/hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/util/TestVectorConversionUtils.java
@@ -69,11 +69,11 @@ class TestVectorConversionUtils {
     DataType requested = DataTypes.ROW(
         DataTypes.FIELD("codes", 
DataTypes.ARRAY(DataTypes.TINYINT()).notNull()),
         DataTypes.FIELD("id", DataTypes.INT().notNull()),
-        DataTypes.FIELD("float_vec", 
DataTypes.ARRAY(DataTypes.FLOAT()))).notNull();
+        DataTypes.FIELD("Float_Vec", 
DataTypes.ARRAY(DataTypes.FLOAT()))).notNull();
     HoodieSchema projectedSchema = HoodieSchema.createRecord("projected", 
null, null, Arrays.asList(
         HoodieSchemaField.of("codes", INT8_VECTOR),
         HoodieSchemaField.of("id", HoodieSchema.create(HoodieSchemaType.INT)),
-        HoodieSchemaField.of("float_vec", FLOAT_VECTOR)));
+        HoodieSchemaField.of("Float_Vec", FLOAT_VECTOR)));
     DataType physical = VectorConversionUtils.getParquetReadDataType(
         requested, projectedSchema, detected);
     RowType physicalRow = (RowType) physical.getLogicalType();
@@ -92,6 +92,14 @@ class TestVectorConversionUtils {
     assertEquals(LogicalTypeRoot.VARBINARY, 
physicalTypes[1].getLogicalType().getTypeRoot());
     assertEquals(LogicalTypeRoot.VARBINARY, 
physicalTypes[2].getLogicalType().getTypeRoot());
     assertEquals(LogicalTypeRoot.VARBINARY, 
physicalTypes[3].getLogicalType().getTypeRoot());
+
+    String[] mismatchedNames = {"id", "float_vec", "double_vec", "codes"};
+    Map<Integer, HoodieSchema.Vector> mismatched =
+        VectorConversionUtils.detectVectorColumns(mismatchedNames, selected, 
schema);
+    assertFalse(mismatched.containsKey(2));
+    DataType[] mismatchedPhysicalTypes =
+        VectorConversionUtils.getParquetReadFieldTypes(mismatchedNames, 
fieldTypes, schema);
+    assertEquals(LogicalTypeRoot.ARRAY, 
mismatchedPhysicalTypes[1].getLogicalType().getTypeRoot());
   }
 
   @Test
@@ -149,7 +157,7 @@ class TestVectorConversionUtils {
   private static HoodieSchema vectorRecordSchema() {
     return HoodieSchema.createRecord("vectors", null, null, Arrays.asList(
         HoodieSchemaField.of("id", HoodieSchema.create(HoodieSchemaType.INT)),
-        HoodieSchemaField.of("float_vec", FLOAT_VECTOR),
+        HoodieSchemaField.of("Float_Vec", FLOAT_VECTOR),
         HoodieSchemaField.of("double_vec", DOUBLE_VECTOR),
         HoodieSchemaField.of("codes", INT8_VECTOR)));
   }
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/FormatUtils.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/FormatUtils.java
index ac067d36f412..636a79d00440 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/FormatUtils.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/FormatUtils.java
@@ -87,15 +87,22 @@ public class FormatUtils {
       List<DataType> fieldTypes,
       int[] selectedFields,
       Configuration hadoopConf) {
-    DataType selectedDataType = DataTypes.ROW(Arrays.stream(selectedFields)
+    HoodieSchema requestedSchema = HoodieSchemaConverter.convertToSchema(
+        DataTypes.ROW(Arrays.stream(selectedFields)
             .mapToObj(i -> DataTypes.FIELD(fieldNames.get(i), 
fieldTypes.get(i)))
             .toArray(DataTypes.Field[]::new))
-        .bridgedTo(RowData.class);
-    HoodieSchema requestedSchema = 
HoodieSchemaConverter.convertToSchema(selectedDataType.getLogicalType());
+            .getLogicalType());
+    return getLanceRecordIterator(path, requestedSchema, hadoopConf);
+  }
+
+  public static ClosableIterator<RowData> getLanceRecordIterator(
+      String path,
+      HoodieSchema requestedSchema,
+      Configuration hadoopConf) {
     HoodieRowDataLanceReader reader = new HoodieRowDataLanceReader(
         new StoragePath(path), StreamerUtil.getLanceReadConfig(hadoopConf));
     try {
-      return reader.getRowDataIterator(selectedDataType, requestedSchema);
+      return reader.getRowDataIterator(requestedSchema);
     } catch (RuntimeException e) {
       reader.close();
       throw new HoodieException("Failed to get iterator from Lance reader: " + 
path, e);
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 02d1f3c073f3..27b13af8fadc 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
@@ -26,6 +26,8 @@ import org.apache.hudi.common.config.HoodieConfig;
 import org.apache.hudi.common.config.HoodieStorageConfig;
 import org.apache.hudi.common.model.HoodieRecord;
 import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaField;
+import org.apache.hudi.common.schema.HoodieSchemaType;
 import org.apache.hudi.common.schema.HoodieSchemaUtils;
 import org.apache.hudi.common.util.collection.ClosableIterator;
 import org.apache.hudi.common.util.collection.CloseableMappingIterator;
@@ -44,9 +46,11 @@ import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.vector.FieldVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
 import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.types.FloatingPointPrecision;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
 import org.apache.arrow.vector.types.pojo.Schema;
 import org.apache.flink.table.data.RowData;
-import org.apache.flink.table.types.DataType;
 import org.apache.flink.table.types.logical.RowType;
 import org.lance.file.LanceFileReader;
 
@@ -54,6 +58,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -142,14 +147,14 @@ public class HoodieRowDataLanceReader implements 
HoodieRowDataFileReader {
 
   @Override
   public ClosableIterator<HoodieRecord<RowData>> 
getRecordIterator(HoodieSchema readerSchema, HoodieSchema requestedSchema) 
throws IOException {
-    ClosableIterator<RowData> rowDataItr = 
getRowDataIterator(RowDataQueryContexts.fromSchema(requestedSchema).getRowType(),
 requestedSchema);
+    ClosableIterator<RowData> rowDataItr = getRowDataIterator(requestedSchema);
     return new CloseableMappingIterator<>(rowDataItr, HoodieFlinkRecord::new);
   }
 
   @Override
   public ClosableIterator<String> getRecordKeyIterator() throws IOException {
     HoodieSchema schema = HoodieSchemaUtils.getRecordKeySchema();
-    ClosableIterator<RowData> rowDataItr = 
getRowDataIterator(RowDataQueryContexts.fromSchema(schema).getRowType(), 
schema);
+    ClosableIterator<RowData> rowDataItr = getRowDataIterator(schema);
     return new CloseableMappingIterator<>(rowDataItr, rowData -> 
rowData.getString(0).toString());
   }
 
@@ -164,11 +169,13 @@ public class HoodieRowDataLanceReader implements 
HoodieRowDataFileReader {
         && 
!internalSchemaManager.getMergeSchema(path.getName()).isEmptySchema()) {
       throw new HoodieValidationException("Flink Lance base-file support does 
not support schema evolution.");
     }
-    return 
getRowDataIterator(RowDataQueryContexts.fromSchema(requiredSchema).getRowType(),
 requiredSchema);
+    return getRowDataIterator(requiredSchema);
   }
 
-  public ClosableIterator<RowData> getRowDataIterator(DataType dataType, 
HoodieSchema requestedSchema) {
-    RowType rowType = (RowType) dataType.getLogicalType();
+  public ClosableIterator<RowData> getRowDataIterator(HoodieSchema 
requestedSchema) {
+    validateRequestedVectors(requestedSchema);
+    RowType rowType = (RowType) 
RowDataQueryContexts.fromSchema(requestedSchema)
+        .getRowType().getLogicalType();
     List<String> columnNames = new ArrayList<>(rowType.getFieldCount());
     for (RowType.RowField field : rowType.getFields()) {
       columnNames.add(field.getName());
@@ -204,7 +211,64 @@ public class HoodieRowDataLanceReader implements 
HoodieRowDataFileReader {
   @Override
   public HoodieSchema getSchema() {
     RowType rowType = HoodieFlinkLanceArrowUtils.toRowType(arrowSchema);
-    return HoodieSchemaConverter.convertToSchema(rowType);
+    Map<String, String> metadata = arrowSchema.getCustomMetadata();
+    Set<String> vectorColumnNames = HoodieSchema.parseVectorColumnNames(
+        metadata == null ? null : 
metadata.get(HoodieSchema.VECTOR_COLUMNS_METADATA_KEY));
+    if (vectorColumnNames.isEmpty()) {
+      return HoodieSchemaConverter.convertToSchema(rowType);
+    }
+    Map<String, Integer> vectorColumns = new LinkedHashMap<>();
+    vectorColumnNames.forEach(name -> vectorColumns.put(
+        name, 
vectorSchemaFromField(arrowSchema.findField(name)).getDimension()));
+    return HoodieSchemaConverter.convertToSchema(rowType, "record", 
vectorColumns);
+  }
+
+  private void validateRequestedVectors(HoodieSchema requestedSchema) {
+    for (HoodieSchemaField field : 
requestedSchema.getNonNullType().getFields()) {
+      HoodieSchema fieldSchema = field.schema().getNonNullType();
+      if (fieldSchema.getType() != HoodieSchemaType.VECTOR) {
+        continue;
+      }
+      HoodieSchema.Vector expected = (HoodieSchema.Vector) fieldSchema;
+      HoodieSchema.Vector actual = 
vectorSchemaFromField(arrowSchema.findField(field.name()));
+      if (actual.getDimension() != expected.getDimension()
+          || actual.getVectorElementType() != expected.getVectorElementType()) 
{
+        throw new HoodieValidationException(
+            "Incompatible Lance VECTOR encoding for column '" + field.name()
+                + "': requested " + expected.toTypeDescriptor()
+                + " but file contains " + actual.toTypeDescriptor());
+      }
+    }
+  }
+
+  private static HoodieSchema.Vector vectorSchemaFromField(Field field) {
+    // Spark Lance currently writes VECTOR columns as FixedSizeList only for 
FLOAT/DOUBLE.
+    // Restrict restoration to the same types so files have one cross-engine 
VECTOR contract.
+    if (!(field.getType() instanceof ArrowType.FixedSizeList)
+        || field.getChildren().size() != 1
+        || !(field.getChildren().get(0).getType() instanceof 
ArrowType.FloatingPoint)) {
+      throw new HoodieValidationException(
+          "Invalid Lance VECTOR encoding for column '" + field.getName()
+              + "': expected FixedSizeList<Float32|Float64> but found " + 
field);
+    }
+
+    ArrowType.FixedSizeList listType = (ArrowType.FixedSizeList) 
field.getType();
+    FloatingPointPrecision precision =
+        ((ArrowType.FloatingPoint) 
field.getChildren().get(0).getType()).getPrecision();
+    HoodieSchema.Vector.VectorElementType vectorElementType;
+    switch (precision) {
+      case SINGLE:
+        vectorElementType = HoodieSchema.Vector.VectorElementType.FLOAT;
+        break;
+      case DOUBLE:
+        vectorElementType = HoodieSchema.Vector.VectorElementType.DOUBLE;
+        break;
+      default:
+        throw new HoodieValidationException(
+            "Invalid Lance VECTOR encoding for column '" + field.getName()
+                + "': expected Float32 or Float64 elements but found " + 
precision);
+    }
+    return HoodieSchema.createVector(listType.getListSize(), 
vectorElementType);
   }
 
   @Override
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cow/CopyOnWriteInputFormat.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cow/CopyOnWriteInputFormat.java
index a4d1be2258c9..33de1db7125a 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cow/CopyOnWriteInputFormat.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cow/CopyOnWriteInputFormat.java
@@ -27,6 +27,7 @@ import org.apache.hudi.table.format.FilePathUtils;
 import org.apache.hudi.table.format.FormatUtils;
 import org.apache.hudi.table.format.InternalSchemaManager;
 import org.apache.hudi.table.format.RecordIterators;
+import org.apache.hudi.util.DataTypeUtils;
 import org.apache.hudi.util.VectorConversionUtils;
 
 import lombok.extern.slf4j.Slf4j;
@@ -37,8 +38,10 @@ import 
org.apache.flink.api.common.io.compression.InflaterInputStreamFactory;
 import org.apache.flink.core.fs.FileInputSplit;
 import org.apache.flink.core.fs.Path;
 import org.apache.flink.formats.parquet.utils.SerializableConfiguration;
+import org.apache.flink.table.api.DataTypes;
 import org.apache.flink.table.data.RowData;
 import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.RowType;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.BlockLocation;
 import org.apache.hadoop.fs.FileStatus;
@@ -76,6 +79,7 @@ public class CopyOnWriteInputFormat extends 
FileInputFormat<RowData> {
   private final DataType[] readFieldTypes;
   private final int[] selectedFields;
   private final Map<Integer, HoodieSchema.Vector> vectorColumnInfo;
+  private final HoodieSchema tableSchema;
   private final String partDefaultName;
   private final String partPathField;
   private final boolean hiveStylePartitioning;
@@ -86,6 +90,7 @@ public class CopyOnWriteInputFormat extends 
FileInputFormat<RowData> {
 
   private transient ClosableIterator<RowData> itr;
   private transient long currentReadCount;
+  private transient HoodieSchema requestedSchema;
 
   /**
    * Files filter for determining what files/directories should be included.
@@ -119,6 +124,7 @@ public class CopyOnWriteInputFormat extends 
FileInputFormat<RowData> {
     this.readFieldTypes = 
VectorConversionUtils.getParquetReadFieldTypes(fullFieldNames, fullFieldTypes, 
tableSchema);
     this.selectedFields = selectedFields;
     this.vectorColumnInfo = 
VectorConversionUtils.detectVectorColumns(fullFieldNames, selectedFields, 
tableSchema);
+    this.tableSchema = tableSchema;
     this.conf = new SerializableConfiguration(conf);
     this.utcTimestamp = utcTimestamp;
     this.internalSchemaManager = internalSchemaManager;
@@ -158,8 +164,7 @@ public class CopyOnWriteInputFormat extends 
FileInputFormat<RowData> {
   }
 
   private ClosableIterator<RowData> getLanceRecordIterator(Path path) {
-    return FormatUtils.getLanceRecordIterator(
-        path.toString(), Arrays.asList(fullFieldNames), 
Arrays.asList(fullFieldTypes), selectedFields, conf.conf());
+    return FormatUtils.getLanceRecordIterator(path.toString(), 
getRequestedSchema(), conf.conf());
   }
 
   @Override
@@ -419,4 +424,15 @@ public class CopyOnWriteInputFormat extends 
FileInputFormat<RowData> {
     }
   }
 
+  private HoodieSchema getRequestedSchema() {
+    if (requestedSchema == null) {
+      RowType requestedRowType = (RowType) 
DataTypes.ROW(Arrays.stream(selectedFields)
+              .mapToObj(i -> DataTypes.FIELD(fullFieldNames[i], 
fullFieldTypes[i]))
+              .toArray(DataTypes.Field[]::new))
+          .notNull()
+          .getLogicalType();
+      requestedSchema = DataTypeUtils.toHoodieSchema(requestedRowType, 
tableSchema);
+    }
+    return requestedSchema;
+  }
 }
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestVectorDataSource.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestVectorDataSource.java
index 77416c59184f..d97e5a6994b8 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestVectorDataSource.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/ITTestVectorDataSource.java
@@ -51,6 +51,7 @@ import org.apache.hudi.util.HoodieSchemaConverter;
 import org.apache.hudi.util.StreamerUtil;
 import org.apache.hudi.utils.FlinkMiniCluster;
 import org.apache.hudi.utils.TestTableEnvs;
+import org.apache.hudi.utils.TestUtils;
 
 import org.apache.flink.api.common.typeinfo.TypeInformation;
 import org.apache.flink.configuration.Configuration;
@@ -166,6 +167,65 @@ public class ITTestVectorDataSource {
     assertFloatArray(vectorProjection.get(0).getField(1), new float[] {-1.0f, 
-2.0f, -3.0f, -4.0f});
   }
 
+  @ParameterizedTest
+  @EnumSource(value = HoodieTableType.class)
+  public void testLanceVectorUpsertRead(HoodieTableType tableType) throws 
Exception {
+    TableEnvironment tableEnv = TestTableEnvs.getBatchTableEnv();
+    String tablePath = tempDir.resolve("lance_upsert_" + 
tableType.name()).toUri().toString();
+    Map<String, String> extraOptions = new LinkedHashMap<>();
+    extraOptions.put("hoodie.table.base.file.format", "LANCE");
+    if (tableType == HoodieTableType.MERGE_ON_READ) {
+      extraOptions.put(FlinkOptions.COMPACTION_SCHEDULE_ENABLED.key(), "true");
+      extraOptions.put(FlinkOptions.COMPACTION_ASYNC_ENABLED.key(), "true");
+      extraOptions.put(FlinkOptions.COMPACTION_DELTA_COMMITS.key(), "1");
+    }
+    createVectorTable(
+        tableEnv,
+        "vector_table",
+        tablePath,
+        tableType,
+        "embedding:2,features:3,nullable_embedding:2",
+        null,
+        extraOptions);
+
+    execInsertSql(tableEnv,
+        "INSERT INTO vector_table(id, embedding, features, nullable_embedding, 
label, tags, ts) VALUES "
+            + "('id1', ARRAY[CAST(1.0 AS FLOAT), CAST(1.5 AS FLOAT)], "
+            + " ARRAY[CAST(10.0 AS DOUBLE), CAST(10.5 AS DOUBLE), CAST(11.0 AS 
DOUBLE)], "
+            + " ARRAY[CAST(7.0 AS FLOAT), CAST(7.5 AS FLOAT)], 'old1', 
ARRAY['red', 'blue'], 1), "
+            + "('id2', ARRAY[CAST(2.0 AS FLOAT), CAST(2.5 AS FLOAT)], "
+            + " ARRAY[CAST(20.0 AS DOUBLE), CAST(20.5 AS DOUBLE), CAST(21.0 AS 
DOUBLE)], "
+            + " CAST(NULL AS ARRAY<FLOAT>), 'old2', ARRAY['green'], 2)");
+    execInsertSql(tableEnv,
+        "INSERT INTO vector_table(id, embedding, features, nullable_embedding, 
label, tags, ts) VALUES "
+            + "('id1', ARRAY[CAST(9.0 AS FLOAT), CAST(9.5 AS FLOAT)], "
+            + " ARRAY[CAST(90.0 AS DOUBLE), CAST(90.5 AS DOUBLE), CAST(91.0 AS 
DOUBLE)], "
+            + " CAST(NULL AS ARRAY<FLOAT>), 'new1', ARRAY['black'], 10)");
+
+    if (tableType == HoodieTableType.MERGE_ON_READ) {
+      assertTrue(TestUtils.hasCompleteCompactionInstant(tablePath));
+    }
+
+    List<Row> rows = collect(tableEnv,
+        "SELECT tags, features, id, nullable_embedding, embedding, label "
+            + "FROM vector_table ORDER BY id");
+    assertEquals(2, rows.size());
+
+    assertObjectArray(rows.get(0).getField(0), new Object[] {"black"});
+    assertDoubleArray(rows.get(0).getField(1), new double[] {90.0D, 90.5D, 
91.0D});
+    assertEquals("id1", rows.get(0).getField(2));
+    assertNull(rows.get(0).getField(3));
+    assertFloatArray(rows.get(0).getField(4), new float[] {9.0F, 9.5F});
+    assertEquals("new1", rows.get(0).getField(5));
+
+    assertObjectArray(rows.get(1).getField(0), new Object[] {"green"});
+    assertDoubleArray(rows.get(1).getField(1), new double[] {20.0D, 20.5D, 
21.0D});
+    assertEquals("id2", rows.get(1).getField(2));
+    assertNull(rows.get(1).getField(3));
+    assertFloatArray(rows.get(1).getField(4), new float[] {2.0F, 2.5F});
+    assertEquals("old2", rows.get(1).getField(5));
+  }
+
   @Test
   public void testColumnProjectionContainsVectorColumn() throws Exception {
     TableEnvironment tableEnv = TestTableEnvs.getBatchTableEnv();
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/TestHoodieRowDataLanceReader.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/TestHoodieRowDataLanceReader.java
index 453cdbe4d508..fc39ea84fb3c 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/TestHoodieRowDataLanceReader.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/TestHoodieRowDataLanceReader.java
@@ -21,14 +21,20 @@ package org.apache.hudi.table.format;
 
 import org.apache.hudi.common.bloom.SimpleBloomFilter;
 import org.apache.hudi.common.config.HoodieConfig;
+import org.apache.hudi.common.engine.TaskContextSupplier;
 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.common.schema.internal.InternalSchema;
+import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.collection.ClosableIterator;
 import org.apache.hudi.common.util.collection.Pair;
 import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieValidationException;
+import org.apache.hudi.io.storage.row.HoodieRowDataLanceWriter;
 import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.util.HoodieSchemaConverter;
 import org.apache.hudi.util.RowDataQueryContexts;
 
 import org.apache.arrow.memory.BufferAllocator;
@@ -36,15 +42,30 @@ import org.apache.arrow.memory.RootAllocator;
 import org.apache.arrow.vector.VarCharVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
 import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.types.FloatingPointPrecision;
+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.GenericArrayData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
 import org.apache.flink.table.types.DataType;
+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.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 org.mockito.MockedStatic;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -77,6 +98,177 @@ import static org.mockito.Mockito.when;
 class TestHoodieRowDataLanceReader {
   private static final StoragePath PATH = new StoragePath("/tmp/test.lance");
 
+  @TempDir
+  Path tempDir;
+
+  @Test
+  void testRestoresMixedCaseVectorFieldName() throws Exception {
+    HoodieSchema hoodieSchema = HoodieSchema.createRecord(
+        "mixed_case_record",
+        null,
+        null,
+        Collections.singletonList(HoodieSchemaField.of(
+            "Embedding",
+            HoodieSchema.createNullable(HoodieSchema.createVector(
+                2, HoodieSchema.Vector.VectorElementType.FLOAT)),
+            null,
+            HoodieSchema.NULL_VALUE)));
+    StoragePath path = new 
StoragePath(tempDir.resolve("mixed-case-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(
+          new GenericArrayData(new Object[] {1.25F, 2.5F})));
+    }
+
+    try (HoodieRowDataLanceReader reader = new HoodieRowDataLanceReader(path, 
new HoodieConfig())) {
+      HoodieSchema restoredSchema = reader.getSchema().getNonNullType();
+      assertTrue(restoredSchema.getField("Embedding").isPresent());
+      assertFalse(restoredSchema.getField("embedding").isPresent());
+      HoodieSchema.Vector vector = (HoodieSchema.Vector) 
restoredSchema.getField("Embedding")
+          .get().schema().getNonNullType();
+      assertEquals(2, vector.getDimension());
+      assertEquals(HoodieSchema.Vector.VectorElementType.FLOAT, 
vector.getVectorElementType());
+    }
+  }
+
+  @Test
+  void testReadsVectorsAndRestoresSchemaIdentity() throws Exception {
+    RowType rowType = RowType.of(
+        new LogicalType[] {
+            new IntType(false),
+            new ArrayType(true, new FloatType(false)),
+            new ArrayType(false, new DoubleType(false)),
+            new ArrayType(false, new IntType(false))
+        },
+        new String[] {"id", "embedding", "features", "values"});
+    HoodieSchema hoodieSchema = HoodieSchemaConverter.convertToSchema(
+        rowType, "vector_record", "embedding:2,features:3");
+    StoragePath path = new 
StoragePath(tempDir.resolve("vectors.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}),
+          new GenericArrayData(new Object[] {3.5D, 4.5D, 5.5D}),
+          new GenericArrayData(new Object[] {10, 20})));
+      writer.writeRow("key2", GenericRowData.of(
+          2,
+          null,
+          new GenericArrayData(new Object[] {6.5D, 7.5D, 8.5D}),
+          new GenericArrayData(new Object[] {30})));
+    }
+
+    try (HoodieRowDataLanceReader reader = new HoodieRowDataLanceReader(path, 
new HoodieConfig())) {
+      HoodieSchema readSchema = reader.getSchema().getNonNullType();
+      HoodieSchema.Vector floatVector = (HoodieSchema.Vector) 
readSchema.getField("embedding")
+          .get().schema().getNonNullType();
+      HoodieSchema.Vector doubleVector = (HoodieSchema.Vector) 
readSchema.getField("features")
+          .get().schema().getNonNullType();
+      assertEquals(HoodieSchemaType.VECTOR, floatVector.getType());
+      assertEquals(2, floatVector.getDimension());
+      assertEquals(HoodieSchema.Vector.VectorElementType.FLOAT, 
floatVector.getVectorElementType());
+      assertEquals(3, doubleVector.getDimension());
+      assertEquals(HoodieSchema.Vector.VectorElementType.DOUBLE, 
doubleVector.getVectorElementType());
+      assertEquals(HoodieSchemaType.ARRAY,
+          
readSchema.getField("values").get().schema().getNonNullType().getType());
+
+      try (ClosableIterator<RowData> rows = 
reader.getRowDataIterator(hoodieSchema)) {
+        RowData first = rows.next();
+        assertEquals(1, first.getInt(0));
+        assertFloatArray(first.getArray(1), 1.25F, 2.5F);
+        assertDoubleArray(first.getArray(2), 3.5D, 4.5D, 5.5D);
+        assertIntArray(first.getArray(3), 10, 20);
+
+        RowData second = rows.next();
+        assertEquals(2, second.getInt(0));
+        assertTrue(second.isNullAt(1));
+        assertDoubleArray(second.getArray(2), 6.5D, 7.5D, 8.5D);
+        assertIntArray(second.getArray(3), 30);
+        assertFalse(rows.hasNext());
+      }
+    }
+
+    RowType projectedRowType = RowType.of(
+        new LogicalType[] {
+            new ArrayType(false, new IntType(false)),
+            new ArrayType(false, new DoubleType(false)),
+            new IntType(false),
+            new ArrayType(true, new FloatType(false))
+        },
+        new String[] {"values", "features", "id", "embedding"});
+    HoodieSchema projectedSchema = HoodieSchemaConverter.convertToSchema(
+        projectedRowType, "projected_record", "features:3,embedding:2");
+    try (HoodieRowDataLanceReader reader = new HoodieRowDataLanceReader(path, 
new HoodieConfig());
+         ClosableIterator<RowData> rows = 
reader.getRowDataIterator(projectedSchema)) {
+      RowData first = rows.next();
+      assertIntArray(first.getArray(0), 10, 20);
+      assertDoubleArray(first.getArray(1), 3.5D, 4.5D, 5.5D);
+      assertEquals(1, first.getInt(2));
+      assertFloatArray(first.getArray(3), 1.25F, 2.5F);
+    }
+
+    RowType incompatibleRowType = RowType.of(
+        new LogicalType[] {new ArrayType(true, new FloatType(false))},
+        new String[] {"embedding"});
+    HoodieSchema incompatibleSchema = HoodieSchemaConverter.convertToSchema(
+        incompatibleRowType, "incompatible_record", "embedding:3");
+    try (HoodieRowDataLanceReader reader = new HoodieRowDataLanceReader(path, 
new HoodieConfig())) {
+      HoodieValidationException exception = assertThrows(
+          HoodieValidationException.class,
+          () -> reader.getRowDataIterator(incompatibleSchema));
+      assertTrue(exception.getMessage().contains("requested VECTOR(3)"));
+      assertTrue(exception.getMessage().contains("file contains VECTOR(2)"));
+    }
+  }
+
+  @Test
+  void testRejectsInvalidVectorArrowTypes() throws Exception {
+    RowType rowType = RowType.of(
+        new LogicalType[] {new ArrayType(false, new FloatType(false))},
+        new String[] {"embedding"});
+    HoodieSchema requestedSchema = HoodieSchemaConverter.convertToSchema(
+        rowType, "vector_record", "embedding:2");
+
+    Field element = new Field(
+        "element", FieldType.notNullable(new 
ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE)), null);
+    assertInvalidVectorEncoding(
+        requestedSchema,
+        new Field("embedding", FieldType.nullable(new ArrowType.List()), 
Collections.singletonList(element)),
+        "expected FixedSizeList<Float32|Float64>");
+
+    Field halfElement = new Field(
+        "element", FieldType.notNullable(new 
ArrowType.FloatingPoint(FloatingPointPrecision.HALF)), null);
+    assertInvalidVectorEncoding(
+        requestedSchema,
+        new Field(
+            "embedding",
+            FieldType.nullable(new ArrowType.FixedSizeList(2)),
+            Collections.singletonList(halfElement)),
+        "expected Float32 or Float64 elements");
+  }
+
   @Test
   void testReadsMetadataAndClosesIdempotently() throws Exception {
     SimpleBloomFilter bloomFilter = new SimpleBloomFilter(100, 0.01, 
MURMUR_HASH);
@@ -196,19 +388,52 @@ class TestHoodieRowDataLanceReader {
 
     try (MockedStatic<LanceFileReader> mocked = mockLanceOpen(metadataReader, 
dataReader)) {
       HoodieRowDataLanceReader reader = new HoodieRowDataLanceReader(PATH, new 
HoodieConfig());
-      assertThrows(HoodieException.class, () -> reader.getRowDataIterator(
-          RowDataQueryContexts.fromSchema(schema).getRowType(), schema));
+      assertThrows(HoodieException.class, () -> 
reader.getRowDataIterator(schema));
       verify(dataReader).close();
       reader.close();
     }
   }
 
+  private static void assertFloatArray(ArrayData array, float... expected) {
+    assertEquals(expected.length, array.size());
+    for (int i = 0; i < expected.length; i++) {
+      assertEquals(expected[i], array.getFloat(i));
+    }
+  }
+
+  private static void assertDoubleArray(ArrayData array, double... expected) {
+    assertEquals(expected.length, array.size());
+    for (int i = 0; i < expected.length; i++) {
+      assertEquals(expected[i], array.getDouble(i));
+    }
+  }
+
+  private static void assertIntArray(ArrayData array, int... expected) {
+    assertEquals(expected.length, array.size());
+    for (int i = 0; i < expected.length; i++) {
+      assertEquals(expected[i], array.getInt(i));
+    }
+  }
+
   private static LanceFileReader metadataReader() throws Exception {
     LanceFileReader reader = mock(LanceFileReader.class);
     when(reader.schema()).thenReturn(new Schema(Collections.emptyList()));
     return reader;
   }
 
+  private static void assertInvalidVectorEncoding(
+      HoodieSchema requestedSchema, Field field, String expectedMessage) 
throws Exception {
+    LanceFileReader metadataReader = mock(LanceFileReader.class);
+    when(metadataReader.schema()).thenReturn(new 
Schema(Collections.singletonList(field)));
+    try (MockedStatic<LanceFileReader> mocked = mockLanceOpen(metadataReader);
+         HoodieRowDataLanceReader reader = new HoodieRowDataLanceReader(PATH, 
new HoodieConfig())) {
+      HoodieValidationException exception = assertThrows(
+          HoodieValidationException.class,
+          () -> reader.getRowDataIterator(requestedSchema));
+      assertTrue(exception.getMessage().contains(expectedMessage));
+    }
+  }
+
   private static MockedStatic<LanceFileReader> 
mockLanceOpen(LanceFileReader... readers) {
     MockedStatic<LanceFileReader> mocked = mockStatic(LanceFileReader.class);
     AtomicInteger readerIndex = new AtomicInteger();
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cow/TestCopyOnWriteInputFormat.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cow/TestCopyOnWriteInputFormat.java
index c8210d170652..3355888c5c5d 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cow/TestCopyOnWriteInputFormat.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cow/TestCopyOnWriteInputFormat.java
@@ -19,8 +19,15 @@
 
 package org.apache.hudi.table.format.cow;
 
+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.common.util.collection.ClosableIterator;
 import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.exception.HoodieValidationException;
+import org.apache.hudi.io.storage.row.HoodieRowDataLanceWriter;
+import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.table.format.InternalSchemaManager;
 import org.apache.hudi.util.HoodieSchemaConverter;
 import org.apache.hudi.utils.TestConfigurations;
@@ -28,10 +35,15 @@ import org.apache.hudi.utils.TestConfigurations;
 import org.apache.flink.api.common.io.FilePathFilter;
 import org.apache.flink.core.fs.FileInputSplit;
 import org.apache.flink.core.fs.Path;
+import org.apache.flink.table.data.GenericArrayData;
 import org.apache.flink.table.data.GenericRowData;
 import org.apache.flink.table.data.RowData;
 import org.apache.flink.table.data.StringData;
 import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.ArrayType;
+import org.apache.flink.table.types.logical.FloatType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
 import org.apache.hadoop.fs.FileStatus;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -103,6 +115,57 @@ class TestCopyOnWriteInputFormat {
     assertEquals(-1L, splits[0].getLength());
   }
 
+  @Test
+  void testLanceVectorSchemaMismatch() throws Exception {
+    RowType rowType = RowType.of(false,
+        new LogicalType[] {new ArrayType(false, new FloatType(false))}, new 
String[] {"embedding"});
+    HoodieSchema fileSchema = HoodieSchemaConverter.convertToSchema(
+        rowType, "file_record", "embedding:2");
+    HoodieSchema tableSchema = HoodieSchemaConverter.convertToSchema(
+        rowType, "table_record", "embedding:3");
+    StoragePath storagePath = new 
StoragePath(tempDir.resolve("vector.lance").toUri());
+
+    try (HoodieRowDataLanceWriter writer = new HoodieRowDataLanceWriter(
+        storagePath,
+        fileSchema,
+        "001",
+        mock(TaskContextSupplier.class),
+        Option.empty(),
+        128 * 1024 * 1024L,
+        64 * 1024 * 1024L,
+        16 * 1024 * 1024L,
+        true,
+        false,
+        false)) {
+      writer.writeRow("key1", GenericRowData.of(
+          new GenericArrayData(new Object[] {1.0F, 2.0F})));
+    }
+
+    DataType rowDataType = 
HoodieSchemaConverter.convertToDataType(tableSchema);
+    CopyOnWriteInputFormat inputFormat = new CopyOnWriteInputFormat(
+        new Path[] {new Path(storagePath.toUri())},
+        new String[] {"embedding"},
+        rowDataType.getChildren().toArray(new DataType[0]),
+        new int[] {0},
+        FlinkOptions.PARTITION_DEFAULT_NAME.defaultValue(),
+        FlinkOptions.PARTITION_PATH_FIELD.defaultValue(),
+        false,
+        Collections.emptyList(),
+        Long.MAX_VALUE,
+        new org.apache.hadoop.conf.Configuration(),
+        true,
+        InternalSchemaManager.DISABLED,
+        tableSchema);
+
+    HoodieException exception = assertThrows(
+        HoodieException.class,
+        () -> inputFormat.open(new FileInputSplit(
+            0, new Path(storagePath.toUri()), 0, -1, new String[0])));
+    assertTrue(exception.getCause() instanceof HoodieValidationException);
+    assertTrue(exception.getCause().getMessage().contains("requested 
VECTOR(3)"));
+    assertTrue(exception.getCause().getMessage().contains("file contains 
VECTOR(2)"));
+  }
+
   @Test
   void testAcceptFileUsesBuiltInAndCustomFilters() {
     CopyOnWriteInputFormat inputFormat = inputFormat(

Reply via email to