This is an automated email from the ASF dual-hosted git repository.
mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 0b352a2c67 [#10920] fix(flink): Fix MULTISET type mismatch when
writing to Paimon table via Gravitino Flink Connector (#10927)
0b352a2c67 is described below
commit 0b352a2c67062e5d537d5135f82ac1ea539731d4
Author: geyanggang <[email protected]>
AuthorDate: Mon May 11 10:42:45 2026 +0800
[#10920] fix(flink): Fix MULTISET type mismatch when writing to Paimon
table via Gravitino Flink Connector (#10927)
### What changes were proposed in this pull request?
Use `ExternalType` to preserve Paimon's MULTISET type information
through Gravitino's type system, instead of converting it to `MapType`
which causes a schema mismatch error.
Changes:
- **Paimon catalog backend (`TypeUtils.java`)**: `visit(MultisetType)`
now returns `ExternalType` with the original SQL string instead of
`MapType`. Added `EXTERNAL` case in `GravitinoToPaimonTypeVisitor` to
restore `MultisetType` using `DataTypeJsonParser`.
- **Flink Connector (`TypeUtils.java`)**: `toGravitinoType` MULTISET
case now returns `ExternalType`. Added `EXTERNAL` case in `toFlinkType`
using Flink's `LogicalTypeParser` to restore the original Flink type.
- **Unit test**: Added `testMultisetTypeConversion` to verify round-trip
conversion.
### Why are the changes needed?
When a Paimon table contains a `MULTISET<STRING>` column and is accessed
through the Gravitino Flink Connector, writing to the table fails with:
IllegalArgumentException: Flink schema and store schema are not the same
store schema: field_multiset MULTISET<STRING> Flink schema:
field_multiset MAP<STRING, INT NOT NULL>
Root cause: Gravitino's type system has no native MULTISET type. The
Paimon catalog backend converted `MULTISET<T>` to `MapType<T, INT>`,
which was then converted to Flink's `MAP<T, INT NOT NULL>`. Paimon's
`FlinkTableFactory` performs a strict schema comparison and rejects the
mismatch.
Fix: #10920
### Does this PR introduce _any_ user-facing change?
No. This is a bug fix that restores correct behavior for Paimon tables
with MULTISET columns.
### How was this patch tested?
- Added unit test `testMultisetTypeConversion` in `TestTypeUtils`
covering both `toGravitinoType(MULTISET)` and
`toFlinkType(ExternalType)` round-trip.
- All existing unit tests pass for both `flink-connector` and
`catalog-lakehouse-paimon` modules.
---
.../catalog/lakehouse/paimon/utils/TypeUtils.java | 25 ++++++---
.../gravitino/flink/connector/utils/TypeUtils.java | 15 +++++
.../test/paimon/FlinkPaimonCatalogIT.java | 65 ++++++++++++++++++++++
.../flink/connector/utils/TestTypeUtils.java | 32 +++++++++++
4 files changed, 129 insertions(+), 8 deletions(-)
diff --git
a/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/utils/TypeUtils.java
b/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/utils/TypeUtils.java
index 0ee547f21e..b3543c595f 100644
---
a/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/utils/TypeUtils.java
+++
b/catalogs/catalog-lakehouse-paimon/src/main/java/org/apache/gravitino/catalog/lakehouse/paimon/utils/TypeUtils.java
@@ -28,6 +28,7 @@ import org.apache.paimon.types.BooleanType;
import org.apache.paimon.types.CharType;
import org.apache.paimon.types.DataType;
import org.apache.paimon.types.DataTypeDefaultVisitor;
+import org.apache.paimon.types.DataTypeJsonParser;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.DateType;
import org.apache.paimon.types.DecimalType;
@@ -192,14 +193,10 @@ public class TypeUtils {
@Override
public Type visit(MultisetType multisetType) {
- // Unlike a Java Set, MultisetType allows for multiple instances for
each of its
- // elements with a common subtype. And a conversion is possible through
a map
- // that assigns each value to an integer to represent the multiplicity
of the values.
- // For example, a `MULTISET<INT>` is converted to a `MAP<Integer,
Integer>`, the key of the
- // map represents the elements of the Multiset and the value represents
the multiplicity of
- // the elements in the Multiset.
- return Types.MapType.of(
- multisetType.getElementType().accept(this), Types.IntegerType.get(),
false);
+ // Gravitino's type system does not have a native MULTISET type. We use
ExternalType to
+ // preserve the original Paimon MULTISET type information, so it can be
correctly restored
+ // when converting back to Paimon or Flink types.
+ return Types.ExternalType.of(multisetType.asSQLString());
}
@Override
@@ -280,6 +277,18 @@ public class TypeUtils {
builder.field(field.name(), dataTypeWithNullable,
field.comment());
});
return builder.build();
+ case EXTERNAL:
+ Types.ExternalType externalType = (Types.ExternalType) type;
+ String catalogString = externalType.catalogString();
+ if (catalogString.startsWith("MULTISET<") &&
catalogString.endsWith(">")) {
+ // Restore MULTISET type: extract element type SQL string and
parse it
+ String elementTypeSql =
+ catalogString.substring("MULTISET<".length(),
catalogString.length() - 1);
+ DataType elementDataType =
DataTypeJsonParser.parseAtomicTypeSQLString(elementTypeSql);
+ return new MultisetType(elementDataType);
+ }
+ throw new UnsupportedOperationException(
+ String.format("Paimon does not support Gravitino external type:
%s", catalogString));
default:
throw new UnsupportedOperationException(
String.format(
diff --git
a/flink-connector/flink/src/main/java/org/apache/gravitino/flink/connector/utils/TypeUtils.java
b/flink-connector/flink/src/main/java/org/apache/gravitino/flink/connector/utils/TypeUtils.java
index e988d082b1..2b032dac7e 100644
---
a/flink-connector/flink/src/main/java/org/apache/gravitino/flink/connector/utils/TypeUtils.java
+++
b/flink-connector/flink/src/main/java/org/apache/gravitino/flink/connector/utils/TypeUtils.java
@@ -30,7 +30,10 @@ import org.apache.flink.table.types.logical.CharType;
import org.apache.flink.table.types.logical.DecimalType;
import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.table.types.logical.MapType;
+import org.apache.flink.table.types.logical.MultisetType;
import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.table.types.logical.utils.LogicalTypeParser;
+import org.apache.flink.table.types.utils.TypeConversions;
import org.apache.gravitino.rel.types.Type;
import org.apache.gravitino.rel.types.Types;
@@ -130,6 +133,10 @@ public class TypeUtils {
case NULL:
return Types.NullType.get();
case MULTISET:
+ MultisetType multisetType = (MultisetType) logicalType;
+ // Gravitino's type system does not have a native MULTISET type. We
use ExternalType to
+ // preserve the original type information so it can be correctly
restored.
+ return Types.ExternalType.of(multisetType.asSerializableString());
case STRUCTURED_TYPE:
case UNRESOLVED:
case DISTINCT_TYPE:
@@ -236,6 +243,14 @@ public class TypeUtils {
return DataTypes.INTERVAL(DataTypes.YEAR());
case INTERVAL_DAY:
return DataTypes.INTERVAL(DataTypes.DAY());
+ case EXTERNAL:
+ Types.ExternalType externalType = (Types.ExternalType) gravitinoType;
+ String catalogString = externalType.catalogString();
+ // Parse the external catalog type string back to Flink LogicalType.
+ // This is used to restore types like MULTISET that Gravitino doesn't
natively support.
+ LogicalType parsedType =
+ LogicalTypeParser.parse(catalogString,
TypeUtils.class.getClassLoader());
+ return TypeConversions.fromLogicalToDataType(parsedType);
default:
throw new UnsupportedOperationException("Not support " +
gravitinoType.toString());
}
diff --git
a/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/integration/test/paimon/FlinkPaimonCatalogIT.java
b/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/integration/test/paimon/FlinkPaimonCatalogIT.java
index adb8bc836e..2002909914 100644
---
a/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/integration/test/paimon/FlinkPaimonCatalogIT.java
+++
b/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/integration/test/paimon/FlinkPaimonCatalogIT.java
@@ -18,18 +18,29 @@
*/
package org.apache.gravitino.flink.connector.integration.test.paimon;
+import static
org.apache.gravitino.flink.connector.integration.test.utils.TestUtils.toFlinkPhysicalColumn;
+import static org.junit.jupiter.api.Assertions.fail;
+
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.TableResult;
+import org.apache.flink.table.catalog.CatalogBaseTable;
+import org.apache.flink.table.catalog.ObjectPath;
+import org.apache.flink.table.catalog.exceptions.TableNotExistException;
import org.apache.flink.types.Row;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.flink.connector.integration.test.FlinkCommonIT;
+import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.Table;
import org.apache.gravitino.rel.expressions.distributions.Distributions;
import org.apache.gravitino.rel.expressions.distributions.Strategy;
+import org.apache.gravitino.rel.types.Types;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
@@ -224,4 +235,58 @@ public abstract class FlinkPaimonCatalogIT extends
FlinkCommonIT {
Assertions.assertEquals(
numCatalogs, tableEnv.listCatalogs().length, "The created catalog
should be dropped.");
}
+
+ @Test
+ public void testMultisetType() {
+ String databaseName = "test_multiset_type_db";
+ String tableName = "test_multiset_table";
+
+ doWithSchema(
+ currentCatalog(),
+ databaseName,
+ catalog -> {
+ // Create a table with MULTISET column using ExternalType through
Gravitino API.
+ // This simulates the scenario where a Paimon table with MULTISET
type is registered
+ // in Gravitino (e.g., created by native Paimon and then managed by
Gravitino).
+ Column[] columns =
+ new Column[] {
+ Column.of("id", Types.LongType.get(), "id"),
+ Column.of(
+ "field_multiset",
Types.ExternalType.of("MULTISET<STRING>"), "multiset field")
+ };
+ catalog
+ .asTableCatalog()
+ .createTable(
+ NameIdentifier.of(databaseName, tableName),
+ columns,
+ "test multiset table",
+ ImmutableMap.of());
+
+ // Verify that getTable through Gravitino Flink Connector returns
MULTISET type
+ Optional<org.apache.flink.table.catalog.Catalog> flinkCatalog =
+ tableEnv.getCatalog(catalog.name());
+ Assertions.assertTrue(flinkCatalog.isPresent());
+ try {
+ CatalogBaseTable table =
+ flinkCatalog.get().getTable(new ObjectPath(databaseName,
tableName));
+ Assertions.assertNotNull(table);
+
+ org.apache.flink.table.catalog.Column[] expected =
+ new org.apache.flink.table.catalog.Column[] {
+ org.apache.flink.table.catalog.Column.physical("id",
DataTypes.BIGINT())
+ .withComment("id"),
+ org.apache.flink.table.catalog.Column.physical(
+ "field_multiset",
DataTypes.MULTISET(DataTypes.STRING()))
+ .withComment("multiset field")
+ };
+ org.apache.flink.table.catalog.Column[] actual =
+
toFlinkPhysicalColumn(table.getUnresolvedSchema().getColumns());
+ Assertions.assertArrayEquals(expected, actual);
+ } catch (TableNotExistException e) {
+ fail(e);
+ }
+ },
+ true,
+ supportDropCascade());
+ }
}
diff --git
a/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/utils/TestTypeUtils.java
b/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/utils/TestTypeUtils.java
index a7e94533b2..9d0c71ec84 100644
---
a/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/utils/TestTypeUtils.java
+++
b/flink-connector/flink/src/test/java/org/apache/gravitino/flink/connector/utils/TestTypeUtils.java
@@ -33,6 +33,7 @@ import org.apache.flink.table.types.logical.DoubleType;
import org.apache.flink.table.types.logical.IntType;
import org.apache.flink.table.types.logical.LocalZonedTimestampType;
import org.apache.flink.table.types.logical.MapType;
+import org.apache.flink.table.types.logical.MultisetType;
import org.apache.flink.table.types.logical.NullType;
import org.apache.flink.table.types.logical.RowType;
import org.apache.flink.table.types.logical.SmallIntType;
@@ -253,4 +254,35 @@ public class TestTypeUtils {
UnsupportedOperationException.class,
() -> TypeUtils.toFlinkType(Types.TimestampType.withTimeZone(10)));
}
+
+ @Test
+ public void testMultisetTypeConversion() {
+ // MULTISET<STRING> (VARCHAR(MAX)) should be preserved as ExternalType
+ Assertions.assertEquals(
+ Types.ExternalType.of("MULTISET<VARCHAR(2147483647)>"),
+ TypeUtils.toGravitinoType(new MultisetType(new
VarCharType(Integer.MAX_VALUE))));
+
+ // MULTISET<INT> should be preserved as ExternalType
+ Assertions.assertEquals(
+ Types.ExternalType.of("MULTISET<INT>"),
+ TypeUtils.toGravitinoType(new MultisetType(new IntType())));
+
+ // MULTISET<BIGINT> should be preserved as ExternalType
+ Assertions.assertEquals(
+ Types.ExternalType.of("MULTISET<BIGINT>"),
+ TypeUtils.toGravitinoType(new MultisetType(new BigIntType())));
+
+ // ExternalType with MULTISET should be converted back to Flink MULTISET
+ Assertions.assertEquals(
+ DataTypes.MULTISET(DataTypes.STRING()),
+
TypeUtils.toFlinkType(Types.ExternalType.of("MULTISET<VARCHAR(2147483647)>")));
+
+ Assertions.assertEquals(
+ DataTypes.MULTISET(DataTypes.INT()),
+ TypeUtils.toFlinkType(Types.ExternalType.of("MULTISET<INT>")));
+
+ Assertions.assertEquals(
+ DataTypes.MULTISET(DataTypes.BIGINT()),
+ TypeUtils.toFlinkType(Types.ExternalType.of("MULTISET<BIGINT>")));
+ }
}