Repository navigation
Conversation
A stale UpdateSortOrder could append a sort order whose id already exists, producing metadata that Java cannot load. Mirror Java's addSortOrder: reuse the id of an equal sort order, assign a fresh id when the requested one is taken, and resolve the last-added sort order from the update context. Closes apache#4202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| update.add_column(path="c", field_type=IntegerType()) | ||
|
|
||
|
|
||
| def test_stale_update_sort_order_reuses_equal_sort_order( |
There was a problem hiding this comment.
Try running this test. I had AI whip up something to show an issue around reassigned IDs + wrong orders becoming the default:
def test_stale_transaction_keeps_reassigned_sort_order_as_default(
catalog: Catalog, table_schema_nested: Schema, test_table_identifier: Identifier
) -> None:
namespace = Catalog.namespace_from(test_table_identifier)
catalog.create_namespace(namespace)
table_a = catalog.create_table(test_table_identifier, table_schema_nested)
table_b = catalog.load_table(test_table_identifier)
with table_a.update_sort_order() as update:
update.asc("foo", IdentityTransform())
with table_a.update_sort_order():
pass # Reset to unsorted
# Stale handle stages bar (id 1), baz (id 2), then bar again, which it sends as an explicit SetDefault(1)
with table_b.transaction() as transaction:
with transaction.update_sort_order() as update:
update.asc("bar", IdentityTransform())
with transaction.update_sort_order() as update:
update.asc("baz", IdentityTransform())
with transaction.update_sort_order() as update:
update.asc("bar", IdentityTransform())
# bar and baz are reassigned to 2 and 3, so the default must follow bar rather than foo's id 1
metadata = catalog.load_table(test_table_identifier).metadata
assert [sort_order.order_id for sort_order in metadata.sort_orders] == [0, 1, 2, 3]
assert metadata.default_sort_order_id == 2
assert metadata.sort_order().fields[0].source_id == 2
rambleraptor
left a comment
There was a problem hiding this comment.
Whoops, hit the wrong button accidentally.
A stale transaction that re-stages an existing sort order sends an explicit SetDefaultSortOrderUpdate with the stale id. When the added order was reassigned a fresh id, or matched an equal existing order under a different id, the default ended up pointing at the wrong sort order. Record the id reassignment in the update context and follow it for explicit set-default updates. Adds the stale-transaction regression test from review. Generated-by: an LLM-based AI coding assistant, reviewed and tested by me
dragonknightk
left a comment
There was a problem hiding this comment.
Thanks for picking this up ʕ•ᴥ•ʔ The reuse path, the taken-id fallback, and following the reassigned ids all line up with what Java does, and both the catalog behavior tests and the new transaction test cover the repro from the issue nicely.
Left a couple of nits inline, both about what the new tests would still let through — no behavior changes.
| ), | ||
| ) | ||
| assert new_metadata.sort_orders == table_v2.metadata.sort_orders | ||
| assert new_metadata.default_sort_order_id == existing_sort_order.order_id |
There was a problem hiding this comment.
nit: the reused ids here are all the current default, so the suite stays green if the reuse path starts writing a metadata version. Asserting the timestamp keeps it honest.
| assert new_metadata.default_sort_order_id == existing_sort_order.order_id | |
| assert new_metadata.default_sort_order_id == existing_sort_order.order_id | |
| assert new_metadata.last_updated_ms == table_v2.metadata.last_updated_ms |
| assert len(new_metadata.sort_orders) == len(table_v2.metadata.sort_orders) + 1 | ||
| assert new_metadata.sort_orders[-1].fields == new_sort_order.fields | ||
| assert new_metadata.sort_orders[-1].order_id == existing_sort_order.order_id + 1 | ||
| assert new_metadata.default_sort_order_id == existing_sort_order.order_id + 1 |
There was a problem hiding this comment.
nit: the reused ids here are always the highest one, so recording max(...) instead of the matched sort.order_id would still pass. The reuse path also has to resolve to the matched order itself when its id is no longer the highest — a writer that read metadata before a newer sort order appeared asks for the older one with the id that newer order took.
| assert new_metadata.default_sort_order_id == existing_sort_order.order_id + 1 | |
| assert new_metadata.default_sort_order_id == existing_sort_order.order_id + 1 | |
| def test_update_metadata_add_existing_sort_order_reuses_its_own_id(table_v2: Table) -> None: | |
| existing_sort_order = table_v2.sort_order() | |
| newer_sort_order = SortOrder(SortField(source_id=1, transform=IdentityTransform()), order_id=existing_sort_order.order_id + 1) | |
| metadata = update_table_metadata( | |
| table_v2.metadata, | |
| (AddSortOrderUpdate(sort_order=newer_sort_order), SetDefaultSortOrderUpdate(sort_order_id=-1)), | |
| ) | |
| # A writer that read metadata before newer_sort_order was added picks its own next id, which newer_sort_order took. | |
| # Reusing existing_sort_order has to resolve to its own id, which is no longer the highest one. | |
| new_metadata = update_table_metadata( | |
| metadata, | |
| ( | |
| AddSortOrderUpdate(sort_order=SortOrder(*existing_sort_order.fields, order_id=newer_sort_order.order_id + 1)), | |
| SetDefaultSortOrderUpdate(sort_order_id=-1), | |
| ), | |
| ) | |
| assert new_metadata.sort_orders == metadata.sort_orders | |
| assert new_metadata.default_sort_order_id == existing_sort_order.order_id |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
@dragonknightk, thanks for reviewing. Just pushed few fixes, please check |
|
@tomron |
Closes #4202
Rationale for this change
UpdateSortOrderonly asserts the default sort order id, so a stale handle can commit anAddSortOrderUpdatewhose id already exists. When the orders are equal, the metadata ends up with two sort orders sharing an id, which Java'sTableMetadataParserrejects. When they differ, it raises a non-retryableValueError.This mirrors Java's
TableMetadata.Builder.addSortOrder:SetDefaultSortOrderUpdate(-1)resolves to it, rather than the highest id.Are these changes tested?
Yes. New catalog behavior tests cover the stale-handle scenarios from the issue and multiple sort order changes in one transaction. There are also unit tests for
update_table_metadata.Are there any user-facing changes?
No API changes. Stale sort order updates no longer produce invalid metadata.