-
Notifications
You must be signed in to change notification settings - Fork 541
Fix: Group transaction changes by table #3360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
singhpk234
merged 3 commits into
apache:main
from
singhpk234:feature/optimize-transaction
Mar 6, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ | |
| import java.util.Arrays; | ||
| import java.util.EnumSet; | ||
| import java.util.HashSet; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
@@ -1033,60 +1034,82 @@ public void commitTransaction(CommitTransactionRequest commitTransactionRequest) | |
| new TransactionWorkspaceMetaStoreManager(diagnostics(), metaStoreManager()); | ||
| ((IcebergCatalog) baseCatalog).setMetaStoreManager(transactionMetaStoreManager); | ||
|
|
||
| // Group all changes by table identifier to handle them atomically. | ||
| // This prevents conflicts when multiple changes target the same table entity. | ||
| // LinkedHashMap preserves insertion order for deterministic processing. | ||
| Map<TableIdentifier, List<UpdateTableRequest>> changesByTable = new LinkedHashMap<>(); | ||
| for (UpdateTableRequest change : commitTransactionRequest.tableChanges()) { | ||
| if (CatalogHandlerUtils.isCreate(change)) { | ||
| throw new BadRequestException( | ||
| "Unsupported operation: commitTranaction with updateForStagedCreate: %s", change); | ||
| } | ||
| changesByTable.computeIfAbsent(change.identifier(), k -> new ArrayList<>()).add(change); | ||
|
singhpk234 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // Process each table's changes in order. | ||
| // Note: All UpdateTableRequests for a given table are coalesced into a single metadata | ||
| // update and a single tableOps.commit(), which results in one Polaris entity update per | ||
| // table. This is subtly different from applying each UpdateTableRequest as an independent | ||
| // commit (as if each were under a lock). Requirements are still validated sequentially | ||
| // against the evolving metadata, so conflicts are detected correctly. | ||
| // See also the TODO in TransactionWorkspaceMetaStoreManager for a more general (but more | ||
| // complex) alternative that would intercept at the MetaStoreManager layer. | ||
| List<TableMetadata> tableMetadataObjs = new ArrayList<>(); | ||
| commitTransactionRequest.tableChanges().stream() | ||
| .forEach( | ||
| change -> { | ||
| Table table = baseCatalog.loadTable(change.identifier()); | ||
| if (!(table instanceof BaseTable baseTable)) { | ||
| throw new IllegalStateException( | ||
| "Cannot wrap catalog that does not produce BaseTable"); | ||
| } | ||
| if (CatalogHandlerUtils.isCreate(change)) { | ||
| throw new BadRequestException( | ||
| "Unsupported operation: commitTranaction with updateForStagedCreate: %s", | ||
| change); | ||
| changesByTable.forEach( | ||
|
singhpk234 marked this conversation as resolved.
|
||
| (tableIdentifier, changes) -> { | ||
| Table table = baseCatalog.loadTable(tableIdentifier); | ||
| if (!(table instanceof BaseTable baseTable)) { | ||
| throw new IllegalStateException("Cannot wrap catalog that does not produce BaseTable"); | ||
| } | ||
|
|
||
| TableOperations tableOps = baseTable.operations(); | ||
| TableMetadata baseMetadata = tableOps.current(); | ||
|
|
||
| // Apply each change sequentially: validate requirements against current state, | ||
| // then apply updates. This ensures conflicts are detected (e.g., if two changes | ||
| // both expect schema ID 0, the second will fail after the first increments it). | ||
| TableMetadata currentMetadata = baseMetadata; | ||
| for (UpdateTableRequest change : changes) { | ||
| // Validate requirements against the current metadata state | ||
| final TableMetadata metadataForValidation = currentMetadata; | ||
| change | ||
| .requirements() | ||
| .forEach(requirement -> requirement.validate(metadataForValidation)); | ||
|
|
||
| // TODO: Refactor to share/reconcile the update-application logic below with | ||
| // CatalogHandlerUtils to avoid divergence as complexity grows. | ||
| TableMetadata.Builder metadataBuilder = TableMetadata.buildFrom(currentMetadata); | ||
| for (MetadataUpdate singleUpdate : change.updates()) { | ||
| // Note: If location-overlap checking is refactored to be atomic, we could | ||
| // support validation within a single multi-table transaction as well, but | ||
| // will need to update the TransactionWorkspaceMetaStoreManager to better | ||
| // expose the concept of being able to read uncommitted updates. | ||
| if (singleUpdate instanceof MetadataUpdate.SetLocation setLocation) { | ||
| if (!currentMetadata.location().equals(setLocation.location()) | ||
| && !realmConfig() | ||
| .getConfig(FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) { | ||
|
Comment on lines
+1089
to
+1090
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check allows the operation |
||
| throw new BadRequestException( | ||
| "Unsupported operation: commitTransaction containing SetLocation" | ||
| + " for table '%s' and new location '%s'", | ||
| change.identifier(), ((MetadataUpdate.SetLocation) singleUpdate).location()); | ||
| } | ||
| } | ||
|
|
||
| TableOperations tableOps = baseTable.operations(); | ||
| TableMetadata currentMetadata = tableOps.current(); | ||
|
|
||
| // Validate requirements; any CommitFailedExceptions will fail the overall request | ||
| change.requirements().forEach(requirement -> requirement.validate(currentMetadata)); | ||
|
|
||
| // Apply changes | ||
| TableMetadata.Builder metadataBuilder = TableMetadata.buildFrom(currentMetadata); | ||
| change.updates().stream() | ||
| .forEach( | ||
| singleUpdate -> { | ||
| // Note: If location-overlap checking is refactored to be atomic, we could | ||
| // support validation within a single multi-table transaction as well, but | ||
| // will need to update the TransactionWorkspaceMetaStoreManager to better | ||
| // expose the concept of being able to read uncommitted updates. | ||
| if (singleUpdate instanceof MetadataUpdate.SetLocation setLocation) { | ||
| if (!currentMetadata.location().equals(setLocation.location()) | ||
| && !realmConfig() | ||
| .getConfig( | ||
| FeatureConfiguration.ALLOW_NAMESPACE_LOCATION_OVERLAP)) { | ||
| throw new BadRequestException( | ||
| "Unsupported operation: commitTransaction containing SetLocation" | ||
| + " for table '%s' and new location '%s'", | ||
| change.identifier(), setLocation.location()); | ||
| } | ||
| } | ||
|
|
||
| // Apply updates to builder | ||
| singleUpdate.applyTo(metadataBuilder); | ||
| }); | ||
|
|
||
| // Commit into transaction workspace we swapped the baseCatalog to use | ||
| TableMetadata updatedMetadata = metadataBuilder.build(); | ||
| if (!updatedMetadata.changes().isEmpty()) { | ||
| tableOps.commit(currentMetadata, updatedMetadata); | ||
| } | ||
| // Apply updates to builder | ||
|
singhpk234 marked this conversation as resolved.
|
||
| singleUpdate.applyTo(metadataBuilder); | ||
| } | ||
|
|
||
| // Update currentMetadata to reflect this change for subsequent requirement validation | ||
| currentMetadata = metadataBuilder.build(); | ||
| } | ||
|
|
||
| // Commit all accumulated changes for this table in a single atomic operation | ||
| if (!currentMetadata.changes().isEmpty()) { | ||
| tableOps.commit(baseMetadata, currentMetadata); | ||
| } | ||
|
|
||
| tableMetadataObjs.add(updatedMetadata); | ||
| }); | ||
| tableMetadataObjs.add(currentMetadata); | ||
| }); | ||
|
|
||
| // Commit the collected updates in a single atomic operation | ||
| List<EntityWithPath> pendingUpdates = transactionMetaStoreManager.getPendingUpdates(); | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is behavior deterministic when we have two property updates like this?
If not, we may check with the Iceberg community to clarify the behavior. Not a blocker.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
within a transaction (taking iceberg's client side transcation as an example) it will be consistent as if prop-key value would be prop-val2 as we create a new metadata locally and keep on apply update on top of last updated metadata, is my understanding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are updateRequests ordered? If not, the final result could be either val2 or val1.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes they are (atleast in java impl) I believe spec has an implicit assumption on this, we can iron the spec more bcz otherwise it will not be a true transaction if they are not ordered, is my take.
https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java#L1320
The serializer and deserializer both respect the order :
https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequestParser.java#L42
https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/rest/requests/CommitTransactionRequestParser.java#L62
an E2E tests for this
https://github.com/apache/iceberg/blob/main/core/src/test/java/org/apache/iceberg/rest/requests/TestCommitTransactionRequestParser.java#L162
plz let me know how feel about it