This is an automated email from the ASF dual-hosted git repository.
fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git
The following commit(s) were added to refs/heads/main by this push:
new b8b8c539 Add tests for `update_column` changing list/map optionality
(#2950)
b8b8c539 is described below
commit b8b8c539ec156c9dbf30f13bcc2ae1a0e74e81e2
Author: Kevin Liu <[email protected]>
AuthorDate: Sun Jan 25 16:16:15 2026 -0500
Add tests for `update_column` changing list/map optionality (#2950)
<!--
Thanks for opening a pull request!
-->
<!-- In the case this PR will resolve an issue, please replace
${GITHUB_ISSUE_ID} below with the actual Github issue id. -->
<!-- Closes #${GITHUB_ISSUE_ID} -->
# Rationale for this change
Follow up to #2798
More test coverages
## Are these changes tested?
## Are there any user-facing changes?
<!-- In the case of user-facing changes, please add the changelog label.
-->
---------
Co-authored-by: Copilot <[email protected]>
---
tests/table/test_init.py | 177 ++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 169 insertions(+), 8 deletions(-)
diff --git a/tests/table/test_init.py b/tests/table/test_init.py
index 5487008c..d677d42b 100644
--- a/tests/table/test_init.py
+++ b/tests/table/test_init.py
@@ -513,8 +513,7 @@ def test_update_list_element_required(table_v2: Table) ->
None:
"""Test that update_column can change list element's required property."""
# Add a list column with optional elements
update = UpdateSchema(transaction=table_v2.transaction())
- list_type = ListType(element_id=1, element_type=StringType(),
element_required=False)
- update.add_column(path="tags", field_type=list_type)
+ update.add_column(path="tags", field_type=ListType(element_id=1,
element_type=StringType(), element_required=False))
schema_with_list = update._apply()
# Verify initial state
@@ -523,8 +522,7 @@ def test_update_list_element_required(table_v2: Table) ->
None:
assert field.field_type.element_required is False
# Update element to required
- update2 = UpdateSchema(transaction=table_v2.transaction(),
schema=schema_with_list)
- update2._allow_incompatible_changes = True # Allow optional -> required
+ update2 = UpdateSchema(transaction=table_v2.transaction(),
allow_incompatible_changes=True, schema=schema_with_list)
new_schema = update2.update_column(("tags", "element"),
required=True)._apply()
# Verify the update
@@ -537,8 +535,10 @@ def test_update_map_value_required(table_v2: Table) ->
None:
"""Test that update_column can change map value's required property."""
# Add a map column with optional values
update = UpdateSchema(transaction=table_v2.transaction())
- map_type = MapType(key_id=1, key_type=StringType(), value_id=2,
value_type=IntegerType(), value_required=False)
- update.add_column(path="metadata", field_type=map_type)
+ update.add_column(
+ path="metadata",
+ field_type=MapType(key_id=1, key_type=StringType(), value_id=2,
value_type=IntegerType(), value_required=False),
+ )
schema_with_map = update._apply()
# Verify initial state
@@ -547,8 +547,7 @@ def test_update_map_value_required(table_v2: Table) -> None:
assert field.field_type.value_required is False
# Update value to required
- update2 = UpdateSchema(transaction=table_v2.transaction(),
schema=schema_with_map)
- update2._allow_incompatible_changes = True # Allow optional -> required
+ update2 = UpdateSchema(transaction=table_v2.transaction(),
allow_incompatible_changes=True, schema=schema_with_map)
new_schema = update2.update_column(("metadata", "value"),
required=True)._apply()
# Verify the update
@@ -557,6 +556,168 @@ def test_update_map_value_required(table_v2: Table) ->
None:
assert field.field_type.value_required is True
+def test_update_list_element_required_to_optional(table_v2: Table) -> None:
+ """Test that update_column can change list element from required to
optional (safe direction)."""
+ # Add a list column with required elements
+ update = UpdateSchema(transaction=table_v2.transaction())
+ update.add_column(path="tags", field_type=ListType(element_id=1,
element_type=StringType(), element_required=True))
+ schema_with_list = update._apply()
+
+ # Verify initial state
+ field = schema_with_list.find_field("tags")
+ assert isinstance(field.field_type, ListType)
+ assert field.field_type.element_required is True
+
+ # Update element to optional - should work without
allow_incompatible_changes
+ update2 = UpdateSchema(transaction=table_v2.transaction(),
schema=schema_with_list)
+ new_schema = update2.update_column(("tags", "element"),
required=False)._apply()
+
+ # Verify the update
+ field = new_schema.find_field("tags")
+ assert isinstance(field.field_type, ListType)
+ assert field.field_type.element_required is False
+
+
+def
test_update_list_element_required_fails_without_allow_incompatible(table_v2:
Table) -> None:
+ """Test that optional -> required fails without
allow_incompatible_changes."""
+ # Add a list column with optional elements
+ update = UpdateSchema(transaction=table_v2.transaction())
+ update.add_column(path="tags", field_type=ListType(element_id=1,
element_type=StringType(), element_required=False))
+ schema_with_list = update._apply()
+
+ # Try to update element to required without allow_incompatible_changes -
should fail
+ update2 = UpdateSchema(transaction=table_v2.transaction(),
schema=schema_with_list)
+ with pytest.raises(ValueError, match="Cannot change column nullability"):
+ update2.update_column(("tags", "element"), required=True)._apply()
+
+
+def test_update_map_value_required_fails_without_allow_incompatible(table_v2:
Table) -> None:
+ """Test that optional -> required for map value fails without
allow_incompatible_changes."""
+ # Add a map column with optional values
+ update = UpdateSchema(transaction=table_v2.transaction())
+ update.add_column(
+ path="metadata",
+ field_type=MapType(key_id=1, key_type=StringType(), value_id=2,
value_type=IntegerType(), value_required=False),
+ )
+ schema_with_map = update._apply()
+
+ # Try to update value to required without allow_incompatible_changes -
should fail
+ update2 = UpdateSchema(transaction=table_v2.transaction(),
schema=schema_with_map)
+ with pytest.raises(ValueError, match="Cannot change column nullability"):
+ update2.update_column(("metadata", "value"), required=True)._apply()
+
+
+def test_update_map_key_fails(table_v2: Table) -> None:
+ """Test that updating map keys is not allowed."""
+ # Add a map column
+ update = UpdateSchema(transaction=table_v2.transaction())
+ update.add_column(
+ path="metadata",
+ field_type=MapType(key_id=1, key_type=StringType(), value_id=2,
value_type=IntegerType(), value_required=False),
+ )
+ schema_with_map = update._apply()
+
+ # Try to update the key - should fail even with allow_incompatible_changes
+ update2 = UpdateSchema(transaction=table_v2.transaction(),
allow_incompatible_changes=True, schema=schema_with_map)
+ with pytest.raises(ValueError, match="Cannot update map keys"):
+ update2.update_column(("metadata", "key"), required=False)._apply()
+
+
+def test_update_map_value_required_to_optional(table_v2: Table) -> None:
+ """Test that update_column can change map value from required to optional
(safe direction)."""
+ # Add a map column with required values
+ update = UpdateSchema(transaction=table_v2.transaction())
+ update.add_column(
+ path="metadata",
+ field_type=MapType(key_id=1, key_type=StringType(), value_id=2,
value_type=IntegerType(), value_required=True),
+ )
+ schema_with_map = update._apply()
+
+ # Verify initial state
+ field = schema_with_map.find_field("metadata")
+ assert isinstance(field.field_type, MapType)
+ assert field.field_type.value_required is True
+
+ # Update value to optional - should work without allow_incompatible_changes
+ update2 = UpdateSchema(transaction=table_v2.transaction(),
schema=schema_with_map)
+ new_schema = update2.update_column(("metadata", "value"),
required=False)._apply()
+
+ # Verify the update
+ field = new_schema.find_field("metadata")
+ assert isinstance(field.field_type, MapType)
+ assert field.field_type.value_required is False
+
+
+def test_update_list_and_map_in_single_schema_change(table_v2: Table) -> None:
+ """Test updating both list element and map value required properties in a
single schema change."""
+ # Add both a list and a map column with optional elements/values
+ update = UpdateSchema(transaction=table_v2.transaction())
+ update.add_column(path="tags", field_type=ListType(element_id=1,
element_type=StringType(), element_required=False))
+ update.add_column(
+ path="metadata",
+ field_type=MapType(key_id=1, key_type=StringType(), value_id=2,
value_type=IntegerType(), value_required=False),
+ )
+ schema_with_both = update._apply()
+
+ # Verify initial state
+ list_field = schema_with_both.find_field("tags")
+ assert isinstance(list_field.field_type, ListType)
+ assert list_field.field_type.element_required is False
+
+ map_field = schema_with_both.find_field("metadata")
+ assert isinstance(map_field.field_type, MapType)
+ assert map_field.field_type.value_required is False
+
+ # Update both in a single schema change
+ update2 = UpdateSchema(transaction=table_v2.transaction(),
allow_incompatible_changes=True, schema=schema_with_both)
+ update2.update_column(("tags", "element"), required=True)
+ update2.update_column(("metadata", "value"), required=True)
+ new_schema = update2._apply()
+
+ # Verify both updates
+ list_field = new_schema.find_field("tags")
+ assert isinstance(list_field.field_type, ListType)
+ assert list_field.field_type.element_required is True
+
+ map_field = new_schema.find_field("metadata")
+ assert isinstance(map_field.field_type, MapType)
+ assert map_field.field_type.value_required is True
+
+
+def test_update_nested_list_in_struct_required(table_v2: Table) -> None:
+ """Test updating nested list element required property inside a struct."""
+
+ # Add a struct column containing a list
+ update = UpdateSchema(transaction=table_v2.transaction())
+ struct_type = StructType(
+ NestedField(
+ field_id=1,
+ name="coordinates",
+ field_type=ListType(element_id=2, element_type=DoubleType(),
element_required=False),
+ required=False,
+ )
+ )
+ update.add_column(path="location", field_type=struct_type)
+ schema_with_nested = update._apply()
+
+ # Verify initial state
+ field = schema_with_nested.find_field("location")
+ assert isinstance(field.field_type, StructType)
+ nested_list = field.field_type.fields[0].field_type
+ assert isinstance(nested_list, ListType)
+ assert nested_list.element_required is False
+
+ # Update nested list element to required
+ update2 = UpdateSchema(transaction=table_v2.transaction(),
allow_incompatible_changes=True, schema=schema_with_nested)
+ new_schema = update2.update_column(("location", "coordinates", "element"),
required=True)._apply()
+
+ # Verify the update
+ field = new_schema.find_field("location")
+ nested_list = field.field_type.fields[0].field_type
+ assert isinstance(nested_list, ListType)
+ assert nested_list.element_required is True
+
+
def test_apply_set_properties_update(table_v2: Table) -> None:
base_metadata = table_v2.metadata