Skip to content

feat(table): add Transaction.ReplaceSortOrder (Java Table.replaceSortOrder parity) - #1833

Merged
zeroshade merged 1 commit into
apache:mainfrom
ryanworl:pr/replace-sort-order
Aug 14, 2026
Merged

feat(table): add Transaction.ReplaceSortOrder (Java Table.replaceSortOrder parity)#1833
zeroshade merged 1 commit into
apache:mainfrom
ryanworl:pr/replace-sort-order

Conversation

@ryanworl

Copy link
Copy Markdown
Contributor

What

Adds Transaction.ReplaceSortOrder(order): the Go analogue of Java's Table.replaceSortOrder() / BaseReplaceSortOrder, which replaces the table's default write order. It applies AddSortOrder + SetDefaultSortOrder(-1), and no-ops when the staged default already has identical fields (field-only compare — the incoming order's id must not affect the result). Java's version is a fluent asc()/desc() builder; taking a complete SortOrder is the idiomatic Go adaptation of the same operation.

It also exports FileToDataFile, the parquet-footer-to-DataFile conversion backing AddFiles, with a sortOrderID parameter. #1184 removed that parameter from the internal function so AddFiles would not claim sort_order_id on files it did not write — correct for that caller, but it left no way for a caller who does know a foreign file's sort layout to convey it (data-file field sort_order_id in the spec). AddFiles/filesToDataFiles still pass 0, preserving #1184's behavior.

The two ship together because a rewrite/compaction flow uses both: ReplaceSortOrder installs the table's write order, and FileToDataFile stamps that order id on the rewritten files it registers.

Behavioral note: re-selecting an order that already exists in base metadata should emit only set-default-sort-order with the concrete id — that is #1831's fix; without it, strict REST catalogs reject the reuse commit's add-sort-order. The diffs are independent; this PR does not depend on it textually.

Tests

  • Replace unsorted→new order; field-identical replace is a no-op; sequential replaces assign fresh ids.
  • FileToDataFile: stats round-trip; sortOrderID 0 leaves the field unset, non-zero is stamped; missing file returns an error.
  • go test ./table/... and golangci-lint run are clean.

Made with Cursor

…Order parity)

Java's Table.replaceSortOrder / BaseReplaceSortOrder replaces the
default write order and UpdateRequirements.forUpdateTable fences the
commit with AssertDefaultSortOrderID. This adds the same operation:
AddSortOrder + SetDefaultSortOrder(-1), no-oping when the staged
default already has identical fields.

FileToDataFile exports the parquet-footer-to-DataFile conversion used
by AddFiles. Upstream's unexported fileToDataFile dropped the
sortOrderID parameter and claimed no sort_order_id; the export restores
it so callers converting foreign parquet footers can convey the file's
sort layout (spec data-file field sort_order_id). AddFiles still
passes 0 so it does not claim sort on files it did not write.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryanworl
ryanworl requested a review from zeroshade as a code owner August 13, 2026 20:20

@zeroshade zeroshade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff, cross-checked against the metadata builder and requirement-dedup logic, and ran the full ./table suite on the PR head — everything passes and the description's claims match the code.

A few things I verified because they're subtle:

  • The sequential-replace flow (table/transaction.go:526-531) only works because apply dedups the second AssertDefaultSortOrderID(base) by semantic key before validation — a duplicated requirement is conflict-checked against its kept twin rather than re-validated against staged metadata (which has already moved past the base default). That's correct and matches Java's UpdateRequirements semantics (requirements assert base state), but it's load-bearing; if the dedup-before-validate ordering in apply ever changes, the sequential replaces assign fresh ids test will catch it, which is good.
  • The no-op check compares against the staged default via meta.defaultSortOrderID, not the base table, so replace-then-replace-back within one transaction behaves correctly, and the incoming order's ID is ignored as advertised (sameSortFields is field-only, mirroring reuseOrCreateNewSortOrderID's comparison).
  • FileToDataFile's panic-recover wrapper matches the existing pattern in filesToDataFiles, and AddFiles still passes 0, preserving #1184's behavior.

Two minor notes, neither blocking:

  1. table/transaction.go:518-524 — the no-op check reads t.meta without holding t.mx (only apply locks). This is consistent with the other transaction entry points, and the worst case is a redundant-but-idempotent apply, so I'm fine with it; just noting the transaction type isn't actually safe for concurrent mutators despite the mutex.
  2. FileToDataFile treats sortOrderID == 0 as "no claim", so a caller can't explicitly stamp sort_order_id = 0 (the spec's reserved unsorted order). Since unset ≈ unknown and a 0 claim carries no real information, I think this is the right trade-off, and the doc comment states it clearly.

Small test-coverage nit: there's no case replacing a sorted default back to UnsortedSortOrder (the no-op test starts from unsorted). Not required for merge.

@zeroshade
zeroshade merged commit 1eed1af into apache:main Aug 14, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants