fix(warehouse-sources): remove the old key when a cdc update changes it - #108577
danielcarletti wants to merge 6 commits into
Conversation
An UPDATE that changes a primary key merged only on the new key, so the
old key's row stayed live in the consolidated table and open in the
history table. The decoder now reports the old values from the old key
tuple (or the old row under REPLICA IDENTITY FULL), and capture turns a
key-changing update into a delete of the old key and an insert of the
new one.
The old key tuple sends non-key columns as NULL; those NULLs no longer
fill unchanged TOAST columns.
The decoder compared the relation's replica identity with 2, but
pgoutput sends it as a character ('f' for FULL), so FULL tables reported
every column as key and logged cdc_pk_columns_diverged on every run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…egacy-lane' into claude/cdc-pk-update-deletes-old-key
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 CI report
|
| File | Comment lines | Added lines |
|---|---|---|
products/warehouse_sources/backend/temporal/data_imports/sources/postgres/cdc/decoder.py |
7 | 27 |
products/warehouse_sources/backend/temporal/data_imports/cdc/types.py |
3 | 7 |
products/warehouse_sources/backend/temporal/data_imports/cdc/activities.py |
2 | 61 |
products/warehouse_sources/backend/temporal/data_imports/sources/postgres/cdc/tests/test_decoder.py |
2 | 66 |
This check does not block merging. It updates on every push and clears when the share drops.
⚠️ Backend coverage — 98.0% of changed backend lines covered — 12 uncovered
🧪 Backend test coverage
Patch coverage — changed backend lines (products + core): ████████████████████ 98.0% (703 / 715)
| File | Patch | Uncovered changed lines |
|---|---|---|
products/warehouse_sources/backend/temporal/data_imports/sources/postgres/postgres.py |
16.7% | 1028–1031, 1044 |
products/warehouse_sources/backend/temporal/data_imports/sources/postgres/cdc/stream_reader.py |
25.0% | 348–350 |
products/warehouse_sources/backend/management/commands/repair_stalled_schema_schedules.py |
50.0% | 160 |
products/warehouse_sources/backend/temporal/data_imports/cdc/legacy_conversion.py |
97.6% | 71, 73 |
products/warehouse_sources/backend/temporal/data_imports/cdc/activities.py |
98.8% | 1481 |
🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 404535705342689 -n patch-coverage), or the coverage-data block at the end of this comment.
Per-product line coverage (touched products)
| Product | Coverage | Lines |
|---|---|---|
demo |
███████████░░░░░░░░░ 52.8% |
1,411 / 2,673 |
batch_exports |
████████████████░░░░ 81.2% |
21,527 / 26,502 |
cdp |
██████████████████░░ 88.2% |
4,545 / 5,155 |
mcp_analytics |
██████████████████░░ 89.2% |
5,038 / 5,651 |
product_tours |
██████████████████░░ 89.3% |
1,331 / 1,491 |
dashboards |
██████████████████░░ 89.5% |
6,904 / 7,714 |
data_warehouse |
██████████████████░░ 90.0% |
14,009 / 15,570 |
notebooks |
██████████████████░░ 90.2% |
15,297 / 16,964 |
signals |
██████████████████░░ 90.2% |
57,838 / 64,097 |
cohorts |
██████████████████░░ 90.4% |
8,420 / 9,316 |
streamlit_apps |
██████████████████░░ 90.8% |
2,684 / 2,956 |
managed_warehouse |
██████████████████░░ 91.0% |
10,252 / 11,263 |
tasks |
██████████████████░░ 91.1% |
75,525 / 82,874 |
data_modeling |
██████████████████░░ 91.4% |
10,554 / 11,543 |
exports |
██████████████████░░ 91.6% |
9,680 / 10,562 |
engineering_analytics |
██████████████████░░ 91.7% |
11,032 / 12,030 |
business_knowledge |
██████████████████░░ 92.2% |
7,684 / 8,330 |
conversations |
███████████████████░ 92.5% |
28,734 / 31,062 |
early_access_features |
███████████████████░ 92.6% |
1,332 / 1,439 |
stamphog |
███████████████████░ 92.8% |
8,109 / 8,742 |
canvas |
███████████████████░ 92.8% |
6,873 / 7,405 |
approvals |
███████████████████░ 93.0% |
3,974 / 4,271 |
mcp_registry |
███████████████████░ 93.1% |
1,670 / 1,794 |
notifications |
███████████████████░ 93.2% |
1,145 / 1,229 |
error_tracking |
███████████████████░ 93.2% |
16,359 / 17,547 |
surveys |
███████████████████░ 93.3% |
6,571 / 7,040 |
slack_app |
███████████████████░ 93.4% |
13,995 / 14,989 |
autoresearch |
███████████████████░ 93.6% |
8,481 / 9,061 |
context_layer |
███████████████████░ 93.9% |
3,415 / 3,638 |
web_analytics |
███████████████████░ 94.0% |
21,815 / 23,218 |
alerts |
███████████████████░ 94.0% |
8,569 / 9,114 |
billing_alerts |
███████████████████░ 94.1% |
2,094 / 2,226 |
mcp_store |
███████████████████░ 94.4% |
8,940 / 9,472 |
ai_observability |
███████████████████░ 94.5% |
24,473 / 25,896 |
workflows |
███████████████████░ 94.6% |
15,222 / 16,093 |
wizard |
███████████████████░ 94.7% |
6,150 / 6,496 |
reminders |
███████████████████░ 94.8% |
760 / 802 |
review_hog |
███████████████████░ 95.0% |
11,507 / 12,119 |
endpoints |
███████████████████░ 95.1% |
9,206 / 9,681 |
annotations |
███████████████████░ 95.1% |
817 / 859 |
customer_analytics |
███████████████████░ 95.2% |
25,636 / 26,938 |
marketing_analytics |
███████████████████░ 95.3% |
19,566 / 20,528 |
posthog_ai |
███████████████████░ 95.3% |
2,488 / 2,610 |
experiments |
███████████████████░ 95.4% |
32,457 / 34,020 |
actions |
███████████████████░ 95.5% |
756 / 792 |
logs |
███████████████████░ 95.5% |
15,399 / 16,130 |
data_catalog |
███████████████████░ 95.5% |
4,401 / 4,606 |
tracing |
███████████████████░ 95.6% |
3,536 / 3,699 |
replay_vision |
███████████████████░ 95.6% |
27,675 / 28,939 |
growth |
███████████████████░ 95.7% |
11,228 / 11,734 |
messaging |
███████████████████░ 95.8% |
3,798 / 3,963 |
skills |
███████████████████░ 95.8% |
6,972 / 7,274 |
product_analytics |
███████████████████░ 96.0% |
28,470 / 29,647 |
access_control |
███████████████████░ 96.3% |
7,113 / 7,386 |
revenue_analytics |
███████████████████░ 96.4% |
1,876 / 1,946 |
user_interviews |
███████████████████░ 96.5% |
2,867 / 2,971 |
feature_flags |
███████████████████░ 96.5% |
25,588 / 26,509 |
warehouse_sources |
███████████████████░ 97.3% |
456,596 / 469,402 |
data_quality |
████████████████████ 97.6% |
7,587 / 7,774 |
metrics |
████████████████████ 98.0% |
4,084 / 4,166 |
analytics_platform |
████████████████████ 98.3% |
2,784 / 2,833 |
pulse |
████████████████████ 98.5% |
2,046 / 2,078 |
live_debugger |
████████████████████ 99.2% |
626 / 631 |
Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 📝 WalkthroughWalkthroughPostgreSQL CDC update decoding now records differing old values and preserves them through transaction spill and replay. CDC extraction uses those values to emit a delete for the old key followed by an insert when a primary-key value changes. Deferrable-key updates remain single events. Updates without a changed primary-key value also remain single events. Priority: ➖ Normal Merge Risk: 🔵 Low · up to For most tables, primary-key changes now correctly remove the old row. A table that combines a regular primary key with an unrelated deferrable unique constraint still leaves the old row in place, as it did before this change. The PR can merge once the owner is aware of this gap, but tightening the constraint check would complete the fix. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to A broad exception for tables with deferrable constraints can leave an old row visible after an ordinary key change. The effect is limited to affected replicated tables, but it undermines the cleanup this change is intended to provide. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
Review profile: QUIET
Plan: Enterprise
Run ID: 54ba9e4a-d49b-434f-97d8-3b27771bdb9e
📒 Files selected for processing (5)
products/warehouse_sources/backend/temporal/data_imports/cdc/activities.pyproducts/warehouse_sources/backend/temporal/data_imports/cdc/tests/test_extract_activity.pyproducts/warehouse_sources/backend/temporal/data_imports/cdc/types.pyproducts/warehouse_sources/backend/temporal/data_imports/sources/postgres/cdc/decoder.pyproducts/warehouse_sources/backend/temporal/data_imports/sources/postgres/cdc/tests/test_decoder.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
|
[Medium risk] Changes how CDC updates with key changes are handled. The PR is not safe to merge until it preserves both rows during a same-transaction key swap and satisfies the repository requirements. Reviews (1) · Last reviewed commit: "chore(warehouse-sources): annotate the k..." |
Two rows can swap keys in one transaction only when the key constraint is DEFERRABLE. Splitting each update into a delete of the old key and an insert of the new one would then drop a row: the second row's delete removes the key the first row's insert took. Capture now reads which tables have a deferrable primary key or unique constraint and leaves their key updates as upserts on the new key, as before. Also: `Relation` becomes a frozen dataclass, `previous_values` is typed without `Any`, and a test helper docstring line is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
products/warehouse_sources/backend/temporal/data_imports/cdc/tests/test_extract_activity.py (1)
1509-1510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAnnotate the new reader-stub signature.
Add types for
schema,tables, and the return value. Match theCDCStreamReader.get_deferrable_key_tablescontract. As per coding guidelines, “Write as if mypy--strictwere on” and “Annotate every signature.”Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
Review profile: QUIET
Plan: Enterprise
Run ID: f3989f5b-44c2-441e-bbcb-7f62cb508485
📒 Files selected for processing (7)
products/warehouse_sources/backend/temporal/data_imports/cdc/activities.pyproducts/warehouse_sources/backend/temporal/data_imports/cdc/tests/test_extract_activity.pyproducts/warehouse_sources/backend/temporal/data_imports/cdc/types.pyproducts/warehouse_sources/backend/temporal/data_imports/sources/postgres/cdc/decoder.pyproducts/warehouse_sources/backend/temporal/data_imports/sources/postgres/cdc/stream_reader.pyproducts/warehouse_sources/backend/temporal/data_imports/sources/postgres/cdc/tests/test_decoder.pyproducts/warehouse_sources/backend/temporal/data_imports/sources/postgres/postgres.py
💤 Files with no reviewable changes (1)
- products/warehouse_sources/backend/temporal/data_imports/sources/postgres/cdc/tests/test_decoder.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 8 remain after this review.
The deferrable-constraint check asked the wrong question. It kept every table with any deferrable constraint as upserts, even when the merge key itself was a plain primary key. It also split tables whose merge key nothing enforces, such as a key the user chose, where two rows can hold one key and the delete would remove a row that still exists. Capture now reads each table's unique indexes that Postgres checks on every row (not deferrable, not partial, no expressions) and splits only when one of them is covered by the merge key. It hands those keys to the decoder, which keeps old values only for them. Under REPLICA IDENTITY FULL the old row carries every column, and keeping the rest grew every spilled transaction for values nothing read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
UPDATEchanges that row's primary key._cdchistory table.REPLICA IDENTITY FULLalso log a falsecdc_pk_columns_divergedwarning on every capture run.Changes
ChangeEvent.previous_values). They come from the old key tuple (K) or the full old row (O).2, but pgoutput sends a character ('f'for FULL). The fix silences the false warning.pg_indexon each run.previous_valuesrides through the transaction spill file, and capture drops it before the batcher holds the event.Relationbecomes a frozen dataclass.Known limits:
How did you test this code?
Kwith the key changed or unchanged,O, none). They fail on the base branch.previous_values; a FULL relation decoded from the wire yields no key.D(old)thenI(new)at one position, also for one column of a composite key. A non-key change, or a key change on a deferrable-key table, stays oneU. It fails on the base branch.TestGetEnforcedUniqueKeysruns thepg_indexquery on the test database. It keeps a plain primary key and the key columns of a covering index, and skips a deferrable primary key, a deferrable unique constraint, a partial index and an expression index.👉 Stay up-to-date with PostHog coding conventions for a smoother review.
Release status
Automatic notifications
Docs update
None. No doc describes key changes or the replica identity check.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5
/writing-tests,/writing-code-comments,/reviewing-with-coderabbit,/writing-pr-descriptions.cdc primary key,replica identity): no open PR fixes this.previous_valuesis nowMapping[str, object].🤖 Generated with Claude Code