fix(sync-service): handle PK updates without Materializer key lookup crash#3949
fix(sync-service): handle PK updates without Materializer key lookup crash#3949
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3949 +/- ##
===========================================
+ Coverage 75.75% 88.77% +13.02%
===========================================
Files 11 25 +14
Lines 693 2415 +1722
Branches 172 604 +432
===========================================
+ Hits 525 2144 +1619
- Misses 167 269 +102
- Partials 1 2 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6762daaba8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| |> remove_row_from_tag_indices(old_key, removed_move_tags) | ||
| |> add_row_to_tag_indices(key, move_tags) |
There was a problem hiding this comment.
Drop old key from unchanged tags on PK updates
When an UpdatedRecord changes the primary key but keeps the same move tag(s), removed_move_tags is empty, so this branch never removes old_key from tag_indices and only adds key. That leaves stale keys in the tag index while index has already been rewritten to the new key (Map.pop!/Map.put below), so a later move-out event can attempt Map.pop!(index, old_key) and crash the materializer. This affects shapes with dependency tags where PK updates occur without tag changes.
Useful? React with 👍 / 👎.
Claude Code ReviewSummaryThis PR fixes a crash in the What's Working Well
Issues FoundCritical (Must Fix)None. Important (Should Fix)None. Suggestions (Nice to Have)Potential index inconsistency for tag-only PK updates File: When if columns_present or has_tag_updates do
tag_indices =
tag_indices
|> remove_row_from_tag_indices(old_key, removed_move_tags)
|> add_row_to_tag_indices(key, move_tags) # ← tag_indices updated with new key
if columns_present do
{old_value, index} = Map.pop!(index, old_key)
index = Map.put(index, key, value) # ← index updated with new key
...
else
{{index, tag_indices}, counts_and_events} # ← index still has old_key!
end
If this path is taken and a subsequent In practice this is unlikely to be reachable — Postgres WAL Missing test for tag-change + PK-change combination The two new tests cover Issue ConformanceThe implementation precisely matches what issue #3948 requested. The issue even included the exact diff as a suggested fix, and the PR faithfully implements it — including the CI NoteThe one failing CI test ( Review iteration: 1 | 2026-03-03 |
Summary
Fixes a Materializer crash that occurred when applying an update which changes a row's primary key value.
Fixes #3948
Problem
When a row's primary key column was updated in Postgres (for example
task_idintask_user_acl), Materializer could crash with a key lookup error:** (KeyError) key "...new_key..." not found in: %{ "...old_key..." => ... }Root Cause
apply_changes/2inmaterializer.exlooked up the previous row bykey(the new key after update), while the index still stored the previous row underold_key(the key before update).Fix
This PR makes three changes in
materializer.ex:old_keyfromUpdatedRecordin the pattern match.old_key = old_key || keyto handle updates where PK does not actually change.old_keyfor index removal (Map.pop!(index, old_key)) and related tag-index updates.Tests
Added coverage for PK-update behavior:
update that changes the primary key is handled correctlyupdate that changes the primary key but keeps the same valueThese verify both correct handling of PK-change updates and no spurious events when only the key identity changes while tracked values remain equal.