Skip to content

Fix non-sequential _id gaps on mixed upsert batches - #23

Merged
sagargg merged 1 commit into
mainfrom
fix/sequential-id-on-upsert
Sep 24, 2026
Merged

sagargg merged 1 commit into
mainfrom
fix/sequential-id-on-upsert

Conversation

@luccasmmg

Copy link
Copy Markdown
Member

Summary

  • Client-reported issue: after upserting a batch that mixes already-present (unchanged) rows with new rows under a composite primary key, _id values for the newly inserted rows were non-sequential, with permanent gaps.
  • Root cause: merge_sql()'s USING subquery numbered every incoming row with a single ROW_NUMBER() OVER () spanning the whole batch, but _id = MAX(_id) + _rn is only ever applied on WHEN NOT MATCHED. Unchanged matched rows (no-op'd via the IS DISTINCT FROM diff guard) still consumed _rn slots that nothing ever inserted, so subsequent new rows jumped ahead by however many unchanged rows preceded them in that chunk.
  • Fix: partition the ROW_NUMBER() window on whether each row's primary key is already present in the target table (via a LEFT JOIN self-probe on the PK), so _rn restarts at 1 within the "genuinely new" partition — independent of how many unchanged rows are interleaved in the same batch. _id assignment for WHEN NOT MATCHED rows stays MAX(_id) + _rn, now gap-free.
  • WHEN MATCHED semantics (diff-gated update, _updated_at only bumps on real changes) are unchanged.

Test plan

  • Updated test_merge_sql_renders_typed_extractors_on_match_update_no_match_insert to assert the new partitioned ROW_NUMBER() + self-join shape.
  • Added test_merge_sql_numbers_unmatched_rows_within_their_own_partition as a regression test documenting the gap-avoidance intent.
  • python3 -m pytest tests/engines/bigquery/test_tables.py — 88 passed.
  • Full suite python3 -m pytest — 496 passed.

Note: adds one extra self-join (target table probed on the PK) per upsert chunk on top of the existing MERGE's own join — worth checking PK-column clustering/partitioning on large resource tables if this shows up in BigQuery cost/latency.

🤖 Generated with Claude Code

merge_sql() numbered every incoming row with a single ROW_NUMBER()
OVER() spanning the whole batch, but only unmatched (new) rows ever
consumed that number for their _id (MAX(_id) + _rn). A batch mixing
already-present unchanged rows with new rows left gaps: unchanged
rows' _rn slots were never inserted anywhere.

Partition the window on whether the row's primary key is found in
the target table (via a self-join probe) so _rn is numbered 1..k
within just the unmatched rows, independent of their position among
matched rows in the same batch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 23, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 2f2009a4-d578-4abb-8f4a-33d2f1b3f6c7


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@luccasmmg
luccasmmg requested a review from sagargg September 23, 2026 10:19
@sagargg

sagargg commented Sep 24, 2026

Copy link
Copy Markdown
Member

LGTM

@sagargg
sagargg merged commit 03829a2 into main Sep 24, 2026
2 checks passed
@sagargg

sagargg commented Sep 24, 2026

Copy link
Copy Markdown
Member

@luccasmmg i merged this but i guess we also need to fix on the aircan DAG isn't it ?

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