This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new 4856ee8d27 [Cherry-pick to branch-1.3] [#14705] fix(spark): accept
table comments, no-op alters and add-column defaults (#14708) (#14731)
4856ee8d27 is described below
commit 4856ee8d2795ecdd95260893ab391c832de0e8f7
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Nov 6 21:54:03 2026 +0800
[Cherry-pick to branch-1.3] [#14705] fix(spark): accept table comments,
no-op alters and add-column defaults (#14708) (#14731)
**Cherry-pick Information:**
- Original commit: 2779f6b0e6449644dea0454291ca87ff45baf399
- Target branch: `branch-1.3`
- Status: ✅ Clean cherry-pick (no conflicts)
---------
Co-authored-by: Qi Yu <[email protected]>
Co-authored-by: yuqi <[email protected]>
---
.../spark/connector/SparkTableChangeConverter.java | 22 ++-
.../spark/connector/catalog/BaseCatalog.java | 15 +-
.../catalog/TestBaseCatalogTableChanges.java | 180 +++++++++++++++++++++
.../catalog/TestTransformTableChange.java | 3 +
.../connector/SparkTableChangeConverter34.java | 13 ++
.../catalog/TestBaseCatalogDefaults34.java | 68 ++++++++
6 files changed, 296 insertions(+), 5 deletions(-)
diff --git
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/SparkTableChangeConverter.java
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/SparkTableChangeConverter.java
index 06fef33be3..66e5f4f3e5 100644
---
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/SparkTableChangeConverter.java
+++
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/SparkTableChangeConverter.java
@@ -21,6 +21,8 @@ package org.apache.gravitino.spark.connector;
import com.google.common.base.Preconditions;
import java.util.function.Supplier;
import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.rel.types.Type;
import org.apache.spark.sql.connector.catalog.TableChange;
public class SparkTableChangeConverter {
@@ -47,12 +49,15 @@ public class SparkTableChangeConverter {
return
org.apache.gravitino.rel.TableChange.removeProperty(removeProperty.property());
} else if (change instanceof TableChange.AddColumn) {
TableChange.AddColumn addColumn = (TableChange.AddColumn) change;
+ Type dataType = sparkTypeConverter.toGravitinoType(addColumn.dataType());
return org.apache.gravitino.rel.TableChange.addColumn(
addColumn.fieldNames(),
- sparkTypeConverter.toGravitinoType(addColumn.dataType()),
+ dataType,
addColumn.comment(),
transformColumnPosition(addColumn.position()),
- addColumn.isNullable());
+ addColumn.isNullable(),
+ false,
+ convertAddColumnDefaultValue(addColumn, dataType));
} else if (change instanceof TableChange.DeleteColumn) {
TableChange.DeleteColumn deleteColumn = (TableChange.DeleteColumn)
change;
return org.apache.gravitino.rel.TableChange.deleteColumn(
@@ -110,6 +115,19 @@ public class SparkTableChangeConverter {
return toGravitinoTableChange(change);
}
+ /**
+ * Converts an added column's default when supported by the Spark version.
Spark 3.3 exposes no
+ * add-column default API, so the base converter leaves the default unset.
+ *
+ * @param addColumn the Spark add-column change
+ * @param dataType the new column's Gravitino type
+ * @return the converted default, or {@link Column#DEFAULT_VALUE_NOT_SET}
when absent
+ */
+ protected Expression convertAddColumnDefaultValue(
+ TableChange.AddColumn addColumn, Type dataType) {
+ return Column.DEFAULT_VALUE_NOT_SET;
+ }
+
private org.apache.gravitino.rel.TableChange.ColumnPosition
transformColumnPosition(
TableChange.ColumnPosition columnPosition) {
if (null == columnPosition) {
diff --git
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
index 90e2ff8ae1..b10bcfdc53 100644
---
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
+++
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
@@ -233,10 +233,14 @@ public abstract class BaseCatalog implements
TableCatalog, SupportsNamespaces, F
.map(structField -> createGravitinoColumn(structField))
.toArray(org.apache.gravitino.rel.Column[]::new);
+ // Spark stores the table comment in the table properties. Take it out
before converting the
+ // properties, because it is passed to Gravitino as the comment, not as a
table property, and
+ // a catalog that accepts no table properties would otherwise refuse it.
+ Map<String, String> sparkProperties =
+ properties == null ? new HashMap<>() : new HashMap<>(properties);
+ String comment = sparkProperties.remove(ConnectorConstants.COMMENT);
Map<String, String> gravitinoProperties =
- propertiesConverter.toGravitinoTableProperties(properties);
- // Spark store comment in properties, we should retrieve it and pass to
Gravitino explicitly.
- String comment = gravitinoProperties.remove(ConnectorConstants.COMMENT);
+ propertiesConverter.toGravitinoTableProperties(sparkProperties);
DistributionAndSortOrdersInfo distributionAndSortOrdersInfo =
sparkTransformConverter.toGravitinoDistributionAndSortOrders(transforms);
@@ -277,6 +281,11 @@ public abstract class BaseCatalog implements TableCatalog,
SupportsNamespaces, F
@Override
public Table alterTable(Identifier ident, TableChange... changes) throws
NoSuchTableException {
+ if (changes == null || changes.length == 0) {
+ // Spark sends no change when every requested change is a no-op, for
example DROP COLUMN IF
+ // EXISTS on an absent column. There is nothing to apply.
+ return loadTable(ident);
+ }
try {
Supplier<Column[]> columns =
Suppliers.memoize(
diff --git
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogTableChanges.java
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogTableChanges.java
new file mode 100644
index 0000000000..fbc9d1cfcb
--- /dev/null
+++
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogTableChanges.java
@@ -0,0 +1,180 @@
+/*
+ * 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.gravitino.spark.connector.catalog;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.TableCatalog;
+import org.apache.gravitino.spark.connector.PropertiesConverter;
+import org.apache.gravitino.spark.connector.SparkTableChangeConverter;
+import org.apache.gravitino.spark.connector.SparkTransformConverter;
+import org.apache.gravitino.spark.connector.SparkTypeConverter;
+import org.apache.gravitino.spark.connector.jdbc.JdbcPropertiesConverter;
+import org.apache.spark.sql.connector.catalog.Identifier;
+import org.apache.spark.sql.connector.catalog.Table;
+import org.apache.spark.sql.connector.expressions.Transform;
+import org.apache.spark.sql.types.DataTypes;
+import org.apache.spark.sql.types.StructType;
+import org.apache.spark.sql.util.CaseInsensitiveStringMap;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.mockito.ArgumentCaptor;
+
+/** Table create and alter requests that Spark sends to a Gravitino catalog. */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestBaseCatalogTableChanges {
+
+ private static final Identifier IDENT = Identifier.of(new String[]
{"schema"}, "t");
+ private static final NameIdentifier GRAVITINO_IDENT =
NameIdentifier.of("schema", "t");
+
+ private TableCatalog gravitinoTableCatalog;
+ private org.apache.spark.sql.connector.catalog.TableCatalog sparkCatalog;
+ private TestCatalog catalog;
+
+ @BeforeAll
+ void initCatalogManager() {
+ GravitinoCatalogManager.create("user", () -> mock(GravitinoClient.class));
+ }
+
+ @AfterAll
+ void cleanupCatalogManager() {
+ GravitinoCatalogManager.get().close();
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ Catalog gravitinoCatalog = mock(Catalog.class);
+ gravitinoTableCatalog = mock(TableCatalog.class);
+ sparkCatalog =
mock(org.apache.spark.sql.connector.catalog.TableCatalog.class);
+ when(gravitinoCatalog.asTableCatalog()).thenReturn(gravitinoTableCatalog);
+ catalog = new TestCatalog(gravitinoCatalog, sparkCatalog);
+ SparkTypeConverter typeConverter = new SparkTypeConverter();
+ setField("sparkTypeConverter", typeConverter);
+ setField("sparkTableChangeConverter", new
SparkTableChangeConverter(typeConverter));
+ }
+
+ @Test
+ void testCreateTableCommentIsNotTreatedAsTableProperty() throws Exception {
+ // PostgreSQL rejects table options such as comment; pass the table
comment separately.
+ PropertiesConverter jdbcConverter =
JdbcPropertiesConverter.getPGInstance();
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () ->
jdbcConverter.toGravitinoTableProperties(ImmutableMap.of("comment", "c")));
+
+ StructType schema = new StructType().add("c", DataTypes.IntegerType, true,
"column comment");
+ catalog.createGravitinoTable(
+ IDENT, schema, new Transform[0], ImmutableMap.of("comment", "tbl"));
+
+ ArgumentCaptor<Map<String, String>> properties =
ArgumentCaptor.forClass(Map.class);
+ verify(gravitinoTableCatalog)
+ .createTable(
+ eq(GRAVITINO_IDENT),
+ any(Column[].class),
+ eq("tbl"),
+ properties.capture(),
+ any(),
+ any(),
+ any());
+ Assertions.assertTrue(properties.getValue().isEmpty());
+ }
+
+ @Test
+ void testCreateTableDoesNotChangeCallerProperties() throws Exception {
+ Map<String, String> sparkProperties = Collections.singletonMap("comment",
"tbl");
+ StructType schema = new StructType().add("c", DataTypes.IntegerType);
+ catalog.createGravitinoTable(IDENT, schema, new Transform[0],
sparkProperties);
+ // The caller's map is read, never changed.
+ Assertions.assertEquals("tbl", sparkProperties.get("comment"));
+ }
+
+ @Test
+ void testAlterWithoutChangesDoesNotWrite() throws Exception {
+ org.apache.gravitino.rel.Table gravitinoTable =
mock(org.apache.gravitino.rel.Table.class);
+ Table sparkTable = mock(Table.class);
+
when(gravitinoTableCatalog.loadTable(GRAVITINO_IDENT)).thenReturn(gravitinoTable);
+ when(sparkCatalog.loadTable(IDENT)).thenReturn(sparkTable);
+
+ Assertions.assertSame(sparkTable, catalog.alterTable(IDENT));
+
+ verify(gravitinoTableCatalog, never()).alterTable(any(), any());
+ }
+
+ private void setField(String name, Object value) throws Exception {
+ Field field = BaseCatalog.class.getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(catalog, value);
+ }
+
+ private static class TestCatalog extends BaseCatalog {
+
+ private TestCatalog(
+ Catalog gravitinoCatalog,
+ org.apache.spark.sql.connector.catalog.TableCatalog sparkCatalog) {
+ this.gravitinoCatalogClient = gravitinoCatalog;
+ this.sparkCatalog = sparkCatalog;
+ this.propertiesConverter = JdbcPropertiesConverter.getPGInstance();
+ this.sparkTransformConverter = new SparkTransformConverter(false);
+ }
+
+ @Override
+ protected org.apache.spark.sql.connector.catalog.TableCatalog
createAndInitSparkCatalog(
+ String name, CaseInsensitiveStringMap options, Map<String, String>
properties) {
+ return sparkCatalog;
+ }
+
+ @Override
+ protected Table createSparkTable(
+ Identifier identifier,
+ org.apache.gravitino.rel.Table gravitinoTable,
+ Table sparkTable,
+ org.apache.spark.sql.connector.catalog.TableCatalog sparkCatalog,
+ PropertiesConverter propertiesConverter,
+ SparkTransformConverter sparkTransformConverter,
+ SparkTypeConverter sparkTypeConverter) {
+ return sparkTable;
+ }
+
+ @Override
+ protected PropertiesConverter getPropertiesConverter() {
+ return JdbcPropertiesConverter.getPGInstance();
+ }
+
+ @Override
+ protected SparkTransformConverter getSparkTransformConverter() {
+ return new SparkTransformConverter(false);
+ }
+ }
+}
diff --git
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestTransformTableChange.java
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestTransformTableChange.java
index 1dc0e93970..c26d56e2c8 100644
---
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestTransformTableChange.java
+++
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestTransformTableChange.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.spark.connector.catalog;
+import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.TableChange.UpdateComment;
import org.apache.gravitino.spark.connector.ConnectorConstants;
import org.apache.gravitino.spark.connector.SparkTableChangeConverter;
@@ -142,6 +143,8 @@ public class TestTransformTableChange {
Assertions.assertArrayEquals(
sparkAddColumnFirst.fieldNames(), gravitinoAddColumnFirst.fieldName());
+ Assertions.assertEquals(
+ Column.DEFAULT_VALUE_NOT_SET,
gravitinoAddColumnFirst.getDefaultValue());
Assertions.assertTrue(
"string".equalsIgnoreCase(gravitinoAddColumnFirst.getDataType().simpleString()));
Assertions.assertTrue(
diff --git
a/spark-connector/v3.4/spark/src/main/java/org/apache/gravitino/spark/connector/SparkTableChangeConverter34.java
b/spark-connector/v3.4/spark/src/main/java/org/apache/gravitino/spark/connector/SparkTableChangeConverter34.java
index fa95206864..489b59b788 100644
---
a/spark-connector/v3.4/spark/src/main/java/org/apache/gravitino/spark/connector/SparkTableChangeConverter34.java
+++
b/spark-connector/v3.4/spark/src/main/java/org/apache/gravitino/spark/connector/SparkTableChangeConverter34.java
@@ -74,6 +74,19 @@ public class SparkTableChangeConverter34 extends
SparkTableChangeConverter {
return super.toGravitinoTableChange(change);
}
+ /** {@inheritDoc} */
+ @Override
+ protected Expression convertAddColumnDefaultValue(
+ TableChange.AddColumn addColumn, Type dataType) {
+ // The new column is not in the table yet, so its own type is the target
of its default.
+ return addColumn.defaultValue() == null
+ ? Column.DEFAULT_VALUE_NOT_SET
+ : convertDefaultValue(
+ addColumn.defaultValue().getSql(),
+ addColumn.fieldNames(),
+ () -> new Column[] {Column.of(addColumn.fieldNames()[0],
dataType)});
+ }
+
private Expression convertDefaultValue(
String sql, String[] fieldNames, Supplier<Column[]> columns) {
// Spark uses an empty SQL string for DROP DEFAULT; a string literal
containing no characters
diff --git
a/spark-connector/v3.4/spark/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogDefaults34.java
b/spark-connector/v3.4/spark/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogDefaults34.java
index a1c3b0d956..a38adee813 100644
---
a/spark-connector/v3.4/spark/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogDefaults34.java
+++
b/spark-connector/v3.4/spark/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogDefaults34.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.spark.connector.catalog;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -40,12 +41,15 @@ import
org.apache.gravitino.spark.connector.SparkTableChangeConverter34;
import org.apache.gravitino.spark.connector.SparkTransformConverter;
import org.apache.gravitino.spark.connector.SparkTypeConverter;
import org.apache.gravitino.spark.connector.SparkTypeConverter34;
+import org.apache.spark.sql.connector.catalog.ColumnDefaultValue;
import org.apache.spark.sql.connector.catalog.Identifier;
import org.apache.spark.sql.connector.catalog.Table;
import org.apache.spark.sql.connector.catalog.TableChange;
+import org.apache.spark.sql.connector.expressions.LiteralValue;
import org.apache.spark.sql.internal.SQLConf;
import org.apache.spark.sql.types.DataTypes;
import org.apache.spark.sql.util.CaseInsensitiveStringMap;
+import org.apache.spark.unsafe.types.UTF8String;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
@@ -244,6 +248,70 @@ public class TestBaseCatalogDefaults34 {
verify(table).columns();
}
+ @Test
+ void testAddColumnDefaultIsWritten() throws Exception {
+ Identifier identifier = Identifier.of(new String[] {"schema"}, "table_a");
+ NameIdentifier gravitinoIdentifier = NameIdentifier.of("schema",
"table_a");
+ when(sparkCatalog.loadTable(identifier)).thenReturn(mock(Table.class));
+
+ catalog.alterTable(
+ identifier,
+ TableChange.addColumn(
+ new String[] {"c_int"},
+ DataTypes.IntegerType,
+ true,
+ null,
+ null,
+ new ColumnDefaultValue("5", LiteralValue.apply(5,
DataTypes.IntegerType))),
+ TableChange.addColumn(
+ new String[] {"c_str"},
+ DataTypes.StringType,
+ true,
+ "with comment",
+ null,
+ new ColumnDefaultValue(
+ "'abc'", LiteralValue.apply(UTF8String.fromString("abc"),
DataTypes.StringType))),
+ TableChange.addColumn(new String[] {"c_none"}, DataTypes.IntegerType));
+
+ ArgumentCaptor<org.apache.gravitino.rel.TableChange> changes =
+ ArgumentCaptor.forClass(org.apache.gravitino.rel.TableChange.class);
+ verify(gravitinoTableCatalog).alterTable(eq(gravitinoIdentifier),
changes.capture());
+ org.apache.gravitino.rel.TableChange.AddColumn intColumn =
+ (org.apache.gravitino.rel.TableChange.AddColumn)
changes.getAllValues().get(0);
+ org.apache.gravitino.rel.TableChange.AddColumn stringColumn =
+ (org.apache.gravitino.rel.TableChange.AddColumn)
changes.getAllValues().get(1);
+ org.apache.gravitino.rel.TableChange.AddColumn plainColumn =
+ (org.apache.gravitino.rel.TableChange.AddColumn)
changes.getAllValues().get(2);
+ Assertions.assertEquals(Literals.integerLiteral(5),
intColumn.getDefaultValue());
+ Assertions.assertEquals(Literals.stringLiteral("abc"),
stringColumn.getDefaultValue());
+ Assertions.assertEquals("with comment", stringColumn.getComment());
+ Assertions.assertEquals(Types.StringType.get(),
stringColumn.getDataType());
+ Assertions.assertEquals(Column.DEFAULT_VALUE_NOT_SET,
plainColumn.getDefaultValue());
+ // A new column's default does not need the existing table schema.
+ verify(gravitinoTableCatalog, never()).loadTable(gravitinoIdentifier);
+ }
+
+ @Test
+ void testAddColumnUnsupportedDefaultIsRejectedBeforeWriting() {
+ Identifier identifier = Identifier.of(new String[] {"schema"}, "table_a");
+ IllegalArgumentException error =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ catalog.alterTable(
+ identifier,
+ TableChange.addColumn(
+ new String[] {"c"},
+ DataTypes.IntegerType,
+ true,
+ null,
+ null,
+ new ColumnDefaultValue(
+ "abs(-1)", LiteralValue.apply(1,
DataTypes.IntegerType)))));
+ Assertions.assertTrue(error.getMessage().contains("abs(-1)"),
error.getMessage());
+ Mockito.verifyNoInteractions(gravitinoTableCatalog, sparkCatalog);
+ }
+
private static class TestCatalog extends BaseCatalog {
private TestCatalog(