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 5899bf81e2 [test] Add more tests for schema update
5899bf81e2 is described below
commit 5899bf81e2b2a122daad126419d4f380e79263e4
Author: JingsongLi <[email protected]>
AuthorDate: Tue May 26 18:23:49 2026 +0800
[test] Add more tests for schema update
---
.../paimon/schema/NestedSchemaUtilsTest.java | 240 +++++++++++++++++++++
.../apache/paimon/table/SchemaEvolutionTest.java | 172 +++++++++++++++
2 files changed, 412 insertions(+)
diff --git
a/paimon-core/src/test/java/org/apache/paimon/schema/NestedSchemaUtilsTest.java
b/paimon-core/src/test/java/org/apache/paimon/schema/NestedSchemaUtilsTest.java
index ae5f7ada39..c5beadc75a 100644
---
a/paimon-core/src/test/java/org/apache/paimon/schema/NestedSchemaUtilsTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/schema/NestedSchemaUtilsTest.java
@@ -85,6 +85,35 @@ public class NestedSchemaUtilsTest {
assertThat(nullabilityChange.newNullability()).isFalse();
}
+ @Test
+ public void testNullabilityChangeNotNullToNullable() {
+ List<String> fieldNames = Arrays.asList("column1");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ // Test non-nullable to nullable
+ NestedSchemaUtils.generateNestedColumnUpdates(
+ fieldNames, DataTypes.INT().notNull(),
DataTypes.INT().nullable(), schemaChanges);
+
+ assertThat(schemaChanges).hasSize(1);
+
assertThat(schemaChanges.get(0)).isInstanceOf(SchemaChange.UpdateColumnNullability.class);
+ SchemaChange.UpdateColumnNullability nullabilityChange =
+ (SchemaChange.UpdateColumnNullability) schemaChanges.get(0);
+ assertThat(nullabilityChange.fieldNames()).containsExactly("column1");
+ assertThat(nullabilityChange.newNullability()).isTrue();
+ }
+
+ @Test
+ public void testSameNullabilityNoChange() {
+ List<String> fieldNames = Arrays.asList("column1");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ // Same nullability should not generate nullability change
+ NestedSchemaUtils.generateNestedColumnUpdates(
+ fieldNames, DataTypes.INT().nullable(),
DataTypes.INT().nullable(), schemaChanges);
+
+ assertThat(schemaChanges).isEmpty();
+ }
+
@Test
public void testTypeAndNullabilityChange() {
List<String> fieldNames = Arrays.asList("column1");
@@ -265,6 +294,217 @@ public class NestedSchemaUtilsTest {
.hasMessageContaining("can only be updated to multiset type");
}
+ @Test
+ public void testMultisetTypeUpdateNullability() {
+ List<String> fieldNames = Arrays.asList("multiset_column");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ MultisetType oldType = new MultisetType(true, DataTypes.INT());
+ MultisetType newType = new MultisetType(false, DataTypes.INT());
+
+ NestedSchemaUtils.generateNestedColumnUpdates(fieldNames, oldType,
newType, schemaChanges);
+
+ assertThat(schemaChanges).hasSize(1);
+
assertThat(schemaChanges.get(0)).isInstanceOf(SchemaChange.UpdateColumnNullability.class);
+ SchemaChange.UpdateColumnNullability nullabilityChange =
+ (SchemaChange.UpdateColumnNullability) schemaChanges.get(0);
+
assertThat(nullabilityChange.fieldNames()).containsExactly("multiset_column");
+ assertThat(nullabilityChange.newNullability()).isFalse();
+ }
+
+ @Test
+ public void testArrayElementNullabilityChange() {
+ List<String> fieldNames = Arrays.asList("arr_column");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ // Array element changes from nullable INT to non-nullable INT
+ ArrayType oldType = new ArrayType(true, DataTypes.INT().nullable());
+ ArrayType newType = new ArrayType(true, DataTypes.INT().notNull());
+
+ NestedSchemaUtils.generateNestedColumnUpdates(fieldNames, oldType,
newType, schemaChanges);
+
+ assertThat(schemaChanges).hasSize(1);
+
assertThat(schemaChanges.get(0)).isInstanceOf(SchemaChange.UpdateColumnNullability.class);
+ SchemaChange.UpdateColumnNullability nullabilityChange =
+ (SchemaChange.UpdateColumnNullability) schemaChanges.get(0);
+
assertThat(nullabilityChange.fieldNames()).containsExactly("arr_column",
"element");
+ assertThat(nullabilityChange.newNullability()).isFalse();
+ }
+
+ @Test
+ public void testArrayElementTypeAndNullabilityChange() {
+ List<String> fieldNames = Arrays.asList("arr_column");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ // Array element changes from nullable INT to non-nullable BIGINT
+ ArrayType oldType = new ArrayType(true, DataTypes.INT().nullable());
+ ArrayType newType = new ArrayType(true, DataTypes.BIGINT().notNull());
+
+ NestedSchemaUtils.generateNestedColumnUpdates(fieldNames, oldType,
newType, schemaChanges);
+
+ assertThat(schemaChanges).hasSize(2);
+ assertThat(schemaChanges)
+ .anyMatch(change -> change instanceof
SchemaChange.UpdateColumnType);
+ assertThat(schemaChanges)
+ .anyMatch(change -> change instanceof
SchemaChange.UpdateColumnNullability);
+
+ SchemaChange.UpdateColumnType typeChange =
+ schemaChanges.stream()
+ .filter(c -> c instanceof
SchemaChange.UpdateColumnType)
+ .map(c -> (SchemaChange.UpdateColumnType) c)
+ .findFirst()
+ .orElse(null);
+ assertThat(typeChange).isNotNull();
+ assertThat(typeChange.fieldNames()).containsExactly("arr_column",
"element");
+
+ SchemaChange.UpdateColumnNullability nullabilityChange =
+ schemaChanges.stream()
+ .filter(c -> c instanceof
SchemaChange.UpdateColumnNullability)
+ .map(c -> (SchemaChange.UpdateColumnNullability) c)
+ .findFirst()
+ .orElse(null);
+ assertThat(nullabilityChange).isNotNull();
+
assertThat(nullabilityChange.fieldNames()).containsExactly("arr_column",
"element");
+ assertThat(nullabilityChange.newNullability()).isFalse();
+ }
+
+ @Test
+ public void testArrayNullabilityAndElementNullabilityChange() {
+ List<String> fieldNames = Arrays.asList("arr_column");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ // Both array-level and element-level nullability change
+ ArrayType oldType = new ArrayType(true, DataTypes.INT().nullable());
+ ArrayType newType = new ArrayType(false, DataTypes.INT().notNull());
+
+ NestedSchemaUtils.generateNestedColumnUpdates(fieldNames, oldType,
newType, schemaChanges);
+
+ assertThat(schemaChanges).hasSize(2);
+
+ // element-level nullability change
+ SchemaChange.UpdateColumnNullability elementNullability =
+ schemaChanges.stream()
+ .filter(c -> c instanceof
SchemaChange.UpdateColumnNullability)
+ .map(c -> (SchemaChange.UpdateColumnNullability) c)
+ .filter(c -> c.fieldNames().length == 2)
+ .findFirst()
+ .orElse(null);
+ assertThat(elementNullability).isNotNull();
+
assertThat(elementNullability.fieldNames()).containsExactly("arr_column",
"element");
+ assertThat(elementNullability.newNullability()).isFalse();
+
+ // array-level nullability change
+ SchemaChange.UpdateColumnNullability arrayNullability =
+ schemaChanges.stream()
+ .filter(c -> c instanceof
SchemaChange.UpdateColumnNullability)
+ .map(c -> (SchemaChange.UpdateColumnNullability) c)
+ .filter(c -> c.fieldNames().length == 1)
+ .findFirst()
+ .orElse(null);
+ assertThat(arrayNullability).isNotNull();
+
assertThat(arrayNullability.fieldNames()).containsExactly("arr_column");
+ assertThat(arrayNullability.newNullability()).isFalse();
+ }
+
+ @Test
+ public void testMapValueNullabilityChange() {
+ List<String> fieldNames = Arrays.asList("map_column");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ // Map value changes from nullable to non-nullable
+ MapType oldType = new MapType(true, DataTypes.STRING(),
DataTypes.INT().nullable());
+ MapType newType = new MapType(true, DataTypes.STRING(),
DataTypes.INT().notNull());
+
+ NestedSchemaUtils.generateNestedColumnUpdates(fieldNames, oldType,
newType, schemaChanges);
+
+ assertThat(schemaChanges).hasSize(1);
+
assertThat(schemaChanges.get(0)).isInstanceOf(SchemaChange.UpdateColumnNullability.class);
+ SchemaChange.UpdateColumnNullability nullabilityChange =
+ (SchemaChange.UpdateColumnNullability) schemaChanges.get(0);
+
assertThat(nullabilityChange.fieldNames()).containsExactly("map_column",
"value");
+ assertThat(nullabilityChange.newNullability()).isFalse();
+ }
+
+ @Test
+ public void testMapValueTypeAndNullabilityChange() {
+ List<String> fieldNames = Arrays.asList("map_column");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ // Map value changes both type and nullability
+ MapType oldType = new MapType(true, DataTypes.STRING(),
DataTypes.INT().nullable());
+ MapType newType = new MapType(true, DataTypes.STRING(),
DataTypes.BIGINT().notNull());
+
+ NestedSchemaUtils.generateNestedColumnUpdates(fieldNames, oldType,
newType, schemaChanges);
+
+ assertThat(schemaChanges).hasSize(2);
+ assertThat(schemaChanges)
+ .anyMatch(change -> change instanceof
SchemaChange.UpdateColumnType);
+ assertThat(schemaChanges)
+ .anyMatch(change -> change instanceof
SchemaChange.UpdateColumnNullability);
+
+ SchemaChange.UpdateColumnType typeChange =
+ schemaChanges.stream()
+ .filter(c -> c instanceof
SchemaChange.UpdateColumnType)
+ .map(c -> (SchemaChange.UpdateColumnType) c)
+ .findFirst()
+ .orElse(null);
+ assertThat(typeChange).isNotNull();
+ assertThat(typeChange.fieldNames()).containsExactly("map_column",
"value");
+
+ SchemaChange.UpdateColumnNullability nullabilityChange =
+ schemaChanges.stream()
+ .filter(c -> c instanceof
SchemaChange.UpdateColumnNullability)
+ .map(c -> (SchemaChange.UpdateColumnNullability) c)
+ .findFirst()
+ .orElse(null);
+ assertThat(nullabilityChange).isNotNull();
+
assertThat(nullabilityChange.fieldNames()).containsExactly("map_column",
"value");
+ assertThat(nullabilityChange.newNullability()).isFalse();
+ }
+
+ @Test
+ public void testMultisetElementNullabilityChange() {
+ List<String> fieldNames = Arrays.asList("multiset_column");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ // Multiset element changes from nullable to non-nullable
+ MultisetType oldType = new MultisetType(true,
DataTypes.INT().nullable());
+ MultisetType newType = new MultisetType(true,
DataTypes.INT().notNull());
+
+ NestedSchemaUtils.generateNestedColumnUpdates(fieldNames, oldType,
newType, schemaChanges);
+
+ assertThat(schemaChanges).hasSize(1);
+
assertThat(schemaChanges.get(0)).isInstanceOf(SchemaChange.UpdateColumnNullability.class);
+ SchemaChange.UpdateColumnNullability nullabilityChange =
+ (SchemaChange.UpdateColumnNullability) schemaChanges.get(0);
+
assertThat(nullabilityChange.fieldNames()).containsExactly("multiset_column",
"element");
+ assertThat(nullabilityChange.newNullability()).isFalse();
+ }
+
+ @Test
+ public void testRowFieldNullabilityChange() {
+ List<String> fieldNames = Arrays.asList("row_column");
+ List<SchemaChange> schemaChanges = new ArrayList<>();
+
+ RowType oldType =
+ RowType.of(
+ new DataField(0, "f1", DataTypes.INT().nullable()),
+ new DataField(1, "f2", DataTypes.STRING()));
+ RowType newType =
+ RowType.of(
+ new DataField(0, "f1", DataTypes.INT().notNull()),
+ new DataField(1, "f2", DataTypes.STRING()));
+
+ NestedSchemaUtils.generateNestedColumnUpdates(fieldNames, oldType,
newType, schemaChanges);
+
+ assertThat(schemaChanges).hasSize(1);
+
assertThat(schemaChanges.get(0)).isInstanceOf(SchemaChange.UpdateColumnNullability.class);
+ SchemaChange.UpdateColumnNullability nullabilityChange =
+ (SchemaChange.UpdateColumnNullability) schemaChanges.get(0);
+
assertThat(nullabilityChange.fieldNames()).containsExactly("row_column", "f1");
+ assertThat(nullabilityChange.newNullability()).isFalse();
+ }
+
@Test
public void testRowTypeAddField() {
List<String> fieldNames = Arrays.asList("row_column");
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/SchemaEvolutionTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/SchemaEvolutionTest.java
index 8b118e0562..58d82b0bc9 100644
--- a/paimon-core/src/test/java/org/apache/paimon/table/SchemaEvolutionTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/table/SchemaEvolutionTest.java
@@ -243,6 +243,178 @@ public class SchemaEvolutionTest {
.hasMessageContaining("Cannot update primary key");
}
+ @Test
+ public void testUpdateColumnNullability() throws Exception {
+ Schema schema =
+ Schema.newBuilder()
+ .column("f0", DataTypes.INT())
+ .column("f1", DataTypes.BIGINT())
+ .build();
+ schemaManager.createTable(schema);
+
+ // nullable -> NOT NULL (disabled by default)
+ assertThatThrownBy(
+ () ->
+ schemaManager.commitChanges(
+ Collections.singletonList(
+
SchemaChange.updateColumnNullability("f0", false))))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining("nullable to non nullable");
+
+ // enable null-to-not-null
+ schemaManager.commitChanges(
+ Collections.singletonList(
+ SchemaChange.setOption(
+
CoreOptions.DISABLE_ALTER_COLUMN_NULL_TO_NOT_NULL.key(), "false")));
+
+ // nullable -> NOT NULL
+ TableSchema tableSchema =
+ schemaManager.commitChanges(
+ Collections.singletonList(
+ SchemaChange.updateColumnNullability("f0",
false)));
+ assertThat(tableSchema.fields().get(0).type().isNullable()).isFalse();
+
+ // NOT NULL -> nullable
+ tableSchema =
+ schemaManager.commitChanges(
+ Collections.singletonList(
+ SchemaChange.updateColumnNullability("f0",
true)));
+ assertThat(tableSchema.fields().get(0).type().isNullable()).isTrue();
+ }
+
+ @Test
+ public void testUpdatePrimaryKeyNullability() throws Exception {
+ Schema schema =
+ Schema.newBuilder()
+ .column("k", DataTypes.INT())
+ .column("v", DataTypes.BIGINT())
+ .primaryKey("k")
+ .build();
+ schemaManager.createTable(schema);
+
+ List<SchemaChange> changes =
+
Collections.singletonList(SchemaChange.updateColumnNullability("k", true));
+ assertThatThrownBy(() -> schemaManager.commitChanges(changes))
+ .hasMessageContaining("Cannot change nullability of primary
key");
+ }
+
+ @Test
+ public void testUpdateFieldTypeWithNullabilityChange() throws Exception {
+ Schema schema =
+ Schema.newBuilder()
+ .column("f0", DataTypes.INT())
+ .column("f1", DataTypes.BIGINT())
+ .build();
+ schemaManager.createTable(schema);
+
+ // enable null-to-not-null
+ schemaManager.commitChanges(
+ Collections.singletonList(
+ SchemaChange.setOption(
+
CoreOptions.DISABLE_ALTER_COLUMN_NULL_TO_NOT_NULL.key(), "false")));
+
+ // updateColumnType with keepNullability=true should preserve
nullability
+ TableSchema tableSchema =
+ schemaManager.commitChanges(
+ Collections.singletonList(
+ SchemaChange.updateColumnType(
+ "f0", DataTypes.BIGINT().notNull(),
true)));
+
assertThat(tableSchema.fields().get(0).type()).isEqualTo(DataTypes.BIGINT());
+ assertThat(tableSchema.fields().get(0).type().isNullable()).isTrue();
+
+ // updateColumnType with keepNullability=false should update
nullability
+ tableSchema =
+ schemaManager.commitChanges(
+ Collections.singletonList(
+ SchemaChange.updateColumnType(
+ "f0", DataTypes.DOUBLE().notNull(),
false)));
+
assertThat(tableSchema.fields().get(0).type()).isEqualTo(DataTypes.DOUBLE().notNull());
+ assertThat(tableSchema.fields().get(0).type().isNullable()).isFalse();
+ }
+
+ @Test
+ public void testUpdateNestedColumnNullability() throws Exception {
+ RowType innerType =
+ RowType.of(
+ new DataField(2, "f1", DataTypes.INT()),
+ new DataField(3, "f2", DataTypes.BIGINT()));
+ RowType outerType =
+ RowType.of(
+ new DataField(0, "k", DataTypes.INT()), new
DataField(1, "v", innerType));
+
+ Schema schema =
+ new Schema(
+ outerType.getFields(),
+ Collections.singletonList("k"),
+ Collections.emptyList(),
+ new HashMap<>(),
+ "");
+ schemaManager.createTable(schema);
+
+ // enable null-to-not-null
+ schemaManager.commitChanges(
+ Collections.singletonList(
+ SchemaChange.setOption(
+
CoreOptions.DISABLE_ALTER_COLUMN_NULL_TO_NOT_NULL.key(), "false")));
+
+ // update nested field nullability
+ TableSchema tableSchema =
+ schemaManager.commitChanges(
+ Collections.singletonList(
+ SchemaChange.updateColumnNullability(
+ new String[] {"v", "f1"}, false)));
+ RowType resultType = tableSchema.logicalRowType();
+ RowType nestedType = (RowType) resultType.getFields().get(1).type();
+
assertThat(nestedType.getFields().get(0).type().isNullable()).isFalse();
+ assertThat(nestedType.getFields().get(1).type().isNullable()).isTrue();
+ }
+
+ @Test
+ public void testUpdateColumnNullabilityWithData() throws Exception {
+ Schema schema =
+ new Schema(
+ RowType.of(DataTypes.INT(),
DataTypes.BIGINT()).getFields(),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ new HashMap<>(),
+ "");
+ schemaManager.createTable(schema);
+
+ FileStoreTable table =
FileStoreTableFactory.create(LocalFileIO.create(), tablePath);
+
+ StreamTableWrite write = table.newWrite(commitUser);
+ write.write(GenericRow.of(1, 1L));
+ write.write(GenericRow.of(2, null));
+ TableCommitImpl commit = table.newCommit(commitUser);
+ commit.commit(0, write.prepareCommit(true, 0));
+ write.close();
+ commit.close();
+
+ // enable null-to-not-null
+ schemaManager.commitChanges(
+ SchemaChange.setOption(
+
CoreOptions.DISABLE_ALTER_COLUMN_NULL_TO_NOT_NULL.key(), "false"));
+ // change f0 to NOT NULL
+ schemaManager.commitChanges(SchemaChange.updateColumnNullability("f0",
false));
+
+ table = FileStoreTableFactory.create(LocalFileIO.create(), tablePath);
+
+ // data written before nullability change should still be readable
+ List<String> rows = readRecords(table, null);
+ assertThat(rows).containsExactlyInAnyOrder("1, 1", "2, NULL");
+
+ // write new data after nullability change
+ write = table.newWrite(commitUser);
+ write.write(GenericRow.of(3, 3L));
+ commit = table.newCommit(commitUser);
+ commit.commit(1, write.prepareCommit(true, 1));
+ write.close();
+ commit.close();
+
+ rows = readRecords(table, null);
+ assertThat(rows).containsExactlyInAnyOrder("1, 1", "2, NULL", "3, 3");
+ }
+
@Test
public void testRenameField() throws Exception {
Schema schema =