Skip to content

Commit 18da50c

Browse files
committed
[python] Guard nullable to not-null in update column type
UpdateColumnType carries its own target nullability, but with keep_nullability false the handler applied it without the null-to-not-null guard that UpdateColumnNullability enforces. A type change such as BIGINT NOT NULL on a nullable column therefore succeeded under the default table options, while Java SchemaManager#updateColumnType rejects it unless alter-column-null-to-not-null.disabled=false. Thread disable_null_to_not_null into _handle_update_column_type and call _assert_nullability_change when keep_nullability is false.
1 parent a3bb6a6 commit 18da50c

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

paimon-python/pypaimon/schema/schema_manager.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ def update_func(field: DataField, depth: int) -> DataField:
216216

217217

218218
def _handle_update_column_type(
219-
change: UpdateColumnType, new_fields: List[DataField]
219+
change: UpdateColumnType, new_fields: List[DataField],
220+
disable_null_to_not_null: bool
220221
):
221222
from pypaimon.schema.data_types import DataTypeParser
222223
field_names = change.field_names
@@ -227,6 +228,13 @@ def update_func(field: DataField, depth: int) -> DataField:
227228
target_root = DataTypeParser.parse_data_type(change.new_data_type.to_dict())
228229
if change.keep_nullability:
229230
target_root.nullable = source_root.nullable
231+
else:
232+
# A type change carries its own nullability; guard nullable ->
233+
# not null just like UpdateColumnNullability (mirrors Java
234+
# SchemaManager#updateColumnType).
235+
_assert_nullability_change(
236+
source_root.nullable, target_root.nullable,
237+
'.'.join(field_names), disable_null_to_not_null)
230238
if not supports_cast(source_root, target_root):
231239
raise ValueError(
232240
"Column type {}[{}] cannot be converted to {} without losing information."
@@ -701,7 +709,8 @@ def _generate_table_schema(
701709
_assert_not_updating_primary_keys(
702710
old_table_schema, change.field_names, "update"
703711
)
704-
_handle_update_column_type(change, new_fields)
712+
_handle_update_column_type(
713+
change, new_fields, disable_null_to_not_null)
705714
elif isinstance(change, UpdateColumnNullability):
706715
if change.new_nullability:
707716
_assert_not_updating_primary_keys(

paimon-python/pypaimon/tests/filesystem_catalog_test.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,44 @@ def test_alter_table(self):
219219
table = catalog.get_table(identifier)
220220
self.assertEqual(len(table.fields), 2)
221221

222+
def test_update_column_type_guards_null_to_not_null(self):
223+
catalog = CatalogFactory.create({"warehouse": self.warehouse})
224+
catalog.create_database("test_db_guard", False)
225+
226+
def _make_table(name, options):
227+
identifier = "test_db_guard.{}".format(name)
228+
schema = Schema(
229+
fields=[
230+
DataField.from_dict({"id": 0, "name": "k", "type": "INT"}),
231+
DataField.from_dict({"id": 1, "name": "v", "type": "BIGINT"}),
232+
],
233+
partition_keys=[], primary_keys=[], options=options, comment="",
234+
)
235+
catalog.create_table(identifier, schema, False)
236+
return identifier
237+
238+
# Default option (disabled=true) rejects nullable -> not null, mirroring
239+
# Java SchemaManager#updateColumnType.
240+
default_id = _make_table("default_opt", {})
241+
with self.assertRaises(RuntimeError) as ctx:
242+
catalog.alter_table(
243+
default_id,
244+
[SchemaChange.update_column_type(
245+
"v", AtomicType("BIGINT", nullable=False))],
246+
False)
247+
self.assertIn("nullable to non nullable", str(ctx.exception))
248+
249+
# Opting out via the table option allows the transition.
250+
allowed_id = _make_table(
251+
"allow_opt", {"alter-column-null-to-not-null.disabled": "false"})
252+
catalog.alter_table(
253+
allowed_id,
254+
[SchemaChange.update_column_type(
255+
"v", AtomicType("BIGINT", nullable=False))],
256+
False)
257+
table = catalog.get_table(allowed_id)
258+
self.assertFalse(table.fields[1].type.nullable)
259+
222260
def test_add_column_before_partition(self):
223261
catalog = CatalogFactory.create({
224262
"warehouse": self.warehouse

0 commit comments

Comments
 (0)