feat: Support difference_cols for upsert update detection - #3867
Open
hedger9487 wants to merge 1 commit into
Open
feat: Support difference_cols for upsert update detection#3867hedger9487 wants to merge 1 commit into
hedger9487 wants to merge 1 commit into
Conversation
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes #3598
Rationale & Motivation
During
Table.upsert(), matched rows are compared cell-by-cell in pure Python (upsert_util.get_rows_to_update) to determine whether any non-key columns have changed, avoiding unnecessary rewrite IO.However, for wide tables (e.g. 50–200 columns), comparing every single non-key column across matched rows incurs substantial CPU overhead:$M$ non-key columns ($O(N \times M)$).
In production, the vast majority of matched rows are unchanged. Determining that a row has not changed requires scanning all
In workflows where users maintain dedicated change-tracking columns in their tables (such as an upstream row hash, update timestamp, or version column), or only care about changes to specific business fields, comparing every other column is unnecessary.
This PR introduces the optional
difference_colsparameter to allow users to specify which non-key columns determine whether a matched row is considered changed. Changes to columns outsidedifference_colsare intentionally ignored for update detection.Key Invariants & Design
Detection Authority with Full-Row Writes:
difference_colsonly restricts which columns are inspected to determine if a matched row has changed. When a row is detected as changed, all columns of the source row are written, preserving complete row data integrity.Fail-Fast Validation (
validate_difference_cols):difference_cols=[]) with:"difference_cols must contain at least one column; use None to compare all non-key columns".join_cols, since join columns are guaranteed to match for matched rows and therefore cannot provide meaningful change detection.Transaction.upsertto fail fast before performing storage or network operations.Backward Compatibility:
difference_cols=None, which preserves the existing change-detection behavior of checking all non-key columns.difference_colsrequire no changes.Benchmark Results (10 Measured Trials)
Measured using in-memory Arrow tables isolated to
get_rows_to_update.Note: The benchmark measures change-detection overhead only and excludes catalog, filesystem, and Parquet I/O.
col_0was modified so that the same 500 rows were identified as changed under both configurations.difference_cols=["col_0"]Proposed Changes
1.
pyiceberg/table/upsert_util.pyvalidate_difference_cols(column_names, join_cols, difference_cols): validates non-key constraint, non-emptiness, and column existence.get_rows_to_update: usesdifference_colswhen provided; otherwise falls back to existinglist(all_columns - join_cols_set).2.
pyiceberg/table/__init__.pydifference_cols: list[str] | None = Noneparameter and docstrings toTable.upsertandTransaction.upsert.validate_difference_colsbefore remote data scan execution.3.
tests/table/test_upsert.pyAdded 9 comprehensive unit and integration tests:
test_get_rows_to_update_with_difference_cols: Verifies that onlydifference_colsare used for change detection and that changed rows retain all source columns.test_get_rows_to_update_difference_cols_validation: VerifiesValueErroron empty list, unknown column, and join key overlap.test_get_rows_to_update_with_multiple_difference_cols: Verifies composite difference columns (["col1", "col2"]).test_get_rows_to_update_difference_cols_with_nulls: VerifiesNonetest_get_rows_to_update_difference_cols_ignore_other_columns: Verifies that changes to non-difference columns are ignored for update detection (NOOP).test_upsert_with_difference_cols: End-to-end catalog upsert test verifying rows skipped, updated, and inserted with actual Parquet files, confirming full-row writes.test_upsert_schema_evolution_with_difference_cols: Verifies interaction between table schema evolution anddifference_cols.test_upsert_with_invalid_difference_cols: Verifies fail-fast rejection viatable.upsert.test_upsert_transaction_with_difference_cols: Verifies transaction support.Verification Checklist
uv run prek run -apasses cleanly.tests/table/test_upsert.pypass.tests/table/pass locally.