This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 72d9b2cf52 [spark][flink] Fix vector-field column conversion in
catalogs (#9014)
72d9b2cf52 is described below
commit 72d9b2cf52f8d434fea4f3e764f606eb7d7c814e
Author: jackylee <[email protected]>
AuthorDate: Fri Aug 7 13:20:46 2026 +0800
[spark][flink] Fix vector-field column conversion in catalogs (#9014)
---
.../apache/paimon/flink/LogicalTypeConversion.java | 17 ++---
.../java/org/apache/paimon/spark/SparkCatalog.java | 52 +++++++++------
.../org/apache/paimon/spark/sql/DDLTestBase.scala | 74 +++++++++++++++++++++-
3 files changed, 116 insertions(+), 27 deletions(-)
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/LogicalTypeConversion.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/LogicalTypeConversion.java
index 0ba8dc3954..04e9acb5ce 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/LogicalTypeConversion.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/LogicalTypeConversion.java
@@ -24,6 +24,7 @@ import org.apache.paimon.types.DataType;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.RowType;
import org.apache.paimon.types.VectorType;
+import org.apache.paimon.utils.StringUtils;
import org.apache.flink.table.types.logical.BinaryType;
import org.apache.flink.table.types.logical.LogicalType;
@@ -107,21 +108,21 @@ public class LogicalTypeConversion {
String dimKey = String.format("field.%s.vector-dim", fieldName);
checkArgument(
options.containsKey(dimKey),
- "When setting '"
- + CoreOptions.VECTOR_FIELD.key()
- + "', you must also set 'field.%s.vector-dim',"
- + " where %s is the name of the vector field.");
+ "When setting '%s', you must also set '%s'.",
+ CoreOptions.VECTOR_FIELD.key(),
+ dimKey);
String vectorDim = options.get(dimKey);
checkArgument(
- !vectorDim.trim().isEmpty(),
- "Expected an integer for vector-dim, but got empty value.");
+ !StringUtils.isNullOrWhitespaceOnly(vectorDim),
+ "Expected an integer for '%s', but got empty value.",
+ dimKey);
try {
- int dim = Integer.parseInt(vectorDim);
+ int dim = Integer.parseInt(vectorDim.trim());
return DataTypes.VECTOR(dim, toDataType(elementType));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
- "Expected an integer for vector-dim, but got: " +
vectorDim);
+ String.format("Expected an integer for '%s', but got:
%s.", dimKey, vectorDim));
}
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java
index 4e18c2318a..16fc1806d1 100644
---
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java
+++
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java
@@ -44,9 +44,9 @@ import org.apache.paimon.table.object.ObjectTable;
import org.apache.paimon.types.BlobType;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataType;
-import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.VectorType;
import org.apache.paimon.utils.ExceptionUtils;
-import org.apache.paimon.utils.Preconditions;
+import org.apache.paimon.utils.StringUtils;
import org.apache.spark.sql.PaimonSparkSession$;
import org.apache.spark.sql.SparkSession;
@@ -611,22 +611,8 @@ public class SparkCatalog extends SparkBaseCatalog
type = toBlobType(field, false);
} else if (blobFields.contains(name)) {
type = toBlobType(field, true);
- } else if (vectorFields.contains(field.name())) {
- Preconditions.checkArgument(
- field.dataType() instanceof ArrayType,
- "The type of blob field must be array");
- ArrayType arrayType = (ArrayType) field.dataType();
- String dimKey = String.format("field.%s.vector-dim",
field.name());
- Preconditions.checkArgument(
- properties.containsKey(dimKey),
- "When setting '"
- + CoreOptions.VECTOR_FIELD.key()
- + "', you must also set 'field.%s.vector-dim',"
- + " where %s is the name of the vector
field.");
- type =
- DataTypes.VECTOR(
- Integer.parseInt(properties.get(dimKey)),
- toPaimonType(arrayType.elementType()));
+ } else if (vectorFields.contains(name)) {
+ type = toVectorType(field, properties);
} else {
type = toPaimonType(field.dataType()).copy(field.nullable());
}
@@ -642,6 +628,36 @@ public class SparkCatalog extends SparkBaseCatalog
return schemaBuilder.build();
}
+ private static DataType toVectorType(StructField field, Map<String,
String> properties) {
+ checkArgument(
+ field.dataType() instanceof ArrayType,
+ "The type of vector field '%s' must be array, but is %s.",
+ field.name(),
+ field.dataType().catalogString());
+ ArrayType arrayType = (ArrayType) field.dataType();
+
+ String dimKey = String.format("field.%s.vector-dim", field.name());
+ checkArgument(
+ properties.containsKey(dimKey),
+ "When setting '%s', you must also set '%s'.",
+ CoreOptions.VECTOR_FIELD.key(),
+ dimKey);
+ String vectorDim = properties.get(dimKey);
+ checkArgument(
+ !StringUtils.isNullOrWhitespaceOnly(vectorDim),
+ "Expected an integer for '%s', but got empty value.",
+ dimKey);
+
+ int dim;
+ try {
+ dim = Integer.parseInt(vectorDim.trim());
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ String.format("Expected an integer for '%s', but got:
%s.", dimKey, vectorDim));
+ }
+ return new VectorType(field.nullable(), dim,
toPaimonType(arrayType.elementType()));
+ }
+
private static DataType toBlobType(StructField field, boolean allowNested)
{
org.apache.spark.sql.types.DataType sparkType = field.dataType();
if (sparkType instanceof BinaryType) {
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DDLTestBase.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DDLTestBase.scala
index fa895d698d..2b5b217837 100644
---
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DDLTestBase.scala
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DDLTestBase.scala
@@ -21,7 +21,7 @@ package org.apache.paimon.spark.sql
import org.apache.paimon.catalog.Identifier
import org.apache.paimon.schema.Schema
import org.apache.paimon.spark.PaimonSparkTestBase
-import org.apache.paimon.types.DataTypes
+import org.apache.paimon.types.{DataTypes, VectorType}
import org.apache.spark.sql.{AnalysisException, Row}
import org.apache.spark.sql.catalyst.analysis.NoSuchPartitionsException
@@ -910,6 +910,78 @@ abstract class DDLTestBase extends PaimonSparkTestBase {
assert(error.contains("Unsupported partition transform"))
}
+ test("Paimon DDL: create table with vector-field") {
+ withTable("T") {
+ sql("""
+ |CREATE TABLE T (id BIGINT, embed ARRAY<FLOAT> NOT NULL)
+ |TBLPROPERTIES (
+ | 'vector-field' = 'embed',
+ | 'field.embed.vector-dim' = '3',
+ | 'row-tracking.enabled' = 'true',
+ | 'data-evolution.enabled' = 'true')
+ |""".stripMargin)
+
+ val rowType = loadTable("T").rowType()
+ val embedType = rowType.getTypeAt(rowType.getFieldIndex("embed"))
+ assert(embedType.isInstanceOf[VectorType])
+ val vectorType = embedType.asInstanceOf[VectorType]
+ assert(vectorType.getLength == 3)
+ assert(vectorType.getElementType == DataTypes.FLOAT())
+ // NOT NULL declared on the Spark column must survive the conversion.
+ assert(!vectorType.isNullable)
+ }
+ }
+
+ test("Paimon DDL: create table with invalid vector-field") {
+ def createVectorTable(column: String, options: String): Unit = {
+ sql(s"""
+ |CREATE TABLE T (id BIGINT, $column)
+ |TBLPROPERTIES (
+ | 'vector-field' = 'embed',
+ | 'row-tracking.enabled' = 'true',
+ | 'data-evolution.enabled' = 'true'
+ | $options)
+ |""".stripMargin)
+ }
+
+ // A vector column must be declared as an array.
+ withTable("T") {
+ val error = intercept[Exception] {
+ createVectorTable("embed FLOAT", ", 'field.embed.vector-dim' = '3'")
+ }
+ assert(
+ error.getMessage.contains("The type of vector field 'embed' must be
array, but is float"))
+ }
+
+ // The dimension option is required, and the message must name the real
option key.
+ withTable("T") {
+ val error = intercept[Exception] {
+ createVectorTable("embed ARRAY<FLOAT>", "")
+ }
+ assert(
+ error.getMessage.contains(
+ "When setting 'vector-field', you must also set
'field.embed.vector-dim'."))
+ }
+
+ // An empty or non-integer dimension must fail with a readable message.
+ withTable("T") {
+ val error = intercept[Exception] {
+ createVectorTable("embed ARRAY<FLOAT>", ", 'field.embed.vector-dim' =
' '")
+ }
+ assert(
+ error.getMessage.contains(
+ "Expected an integer for 'field.embed.vector-dim', but got empty
value."))
+ }
+ withTable("T") {
+ val error = intercept[Exception] {
+ createVectorTable("embed ARRAY<FLOAT>", ", 'field.embed.vector-dim' =
'abc'")
+ }
+ assert(
+ error.getMessage.contains(
+ "Expected an integer for 'field.embed.vector-dim', but got: abc."))
+ }
+ }
+
test("Fix partition column generate wrong partition spec") {
Seq(true, false).foreach {
legacyPartName =>