Skip to content

chore: prevent crash on WAL UPDATE without old tuple#1700

Merged
MicBun merged 2 commits into
mainfrom
decoderReplit
May 4, 2026
Merged

chore: prevent crash on WAL UPDATE without old tuple#1700
MicBun merged 2 commits into
mainfrom
decoderReplit

Conversation

@MicBun
Copy link
Copy Markdown
Collaborator

@MicBun MicBun commented May 4, 2026

resolves: https://github.com/truflation/website/issues/3757

already live on mainnet

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of PostgreSQL replication messages when certain data is absent, preventing potential crashes during DELETE and UPDATE operations.
    • Enhanced value deduplication logic for UPDATE operations.
  • Tests

    • Added regression tests for edge cases in replication message handling.

@MicBun MicBun requested a review from pr-time-tracker May 4, 2026 10:20
@MicBun MicBun self-assigned this May 4, 2026
@MicBun MicBun added the chore label May 4, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@MicBun has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 46 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1780bf66-0bd7-4678-9486-1c00fd2966be

📥 Commits

Reviewing files that changed from the base of the PR and between aa0e906 and 6f827c7.

📒 Files selected for processing (2)
  • node/pg/repl_changeset.go
  • node/pg/repl_changeset_test.go
📝 Walkthrough

Walkthrough

This PR adds defensive nil checks to PostgreSQL replication changeset decoding. decodeUpdate and decodeDelete now conditionally handle missing tuple data, preventing panics when old tuples are absent. New regression tests verify correct behavior for nil tuple cases.

Changes

Replication Changeset Nil-Tuple Handling

Layer / File(s) Summary
Core Decoding Logic
node/pg/repl_changeset.go
decodeUpdate conditionally decodes update.OldTuple only when non-nil; unchanged-value dedup is gated on OldTuple presence. decodeDelete now safely checks delete.OldTuple != nil before decoding and setting it.
Test Harness & Helpers
node/pg/repl_changeset_test.go (lines 6–42)
Added newTestChangesetWriter() constructor and testRelation() helper to build minimal test fixtures for replication message decoding.
Regression Test Cases
node/pg/repl_changeset_test.go (lines 44–175)
Four new tests cover edge cases: nil OldTuple in updates, full replica identity dedup, nil OldTuple in deletes, and normal delete flow with OldTuple present.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested Reviewers

  • pr-time-tracker

Poem

A rabbit hops through tuple trails,
Where nil and data dance on rails,
No panic now when old ones hide—
New tests ensure safe decoding tide! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the primary change: preventing a crash when WAL UPDATE messages lack an old tuple.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch decoderReplit

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
Review rate limit: 0/1 reviews remaining, refill in 43 minutes and 46 seconds.

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

@holdex
Copy link
Copy Markdown

holdex Bot commented May 4, 2026

Time Submission Status

Member Status Time Action Last Update
MicBun ✅ Submitted 4h Update time May 4, 2026, 10:35 AM

You can submit time with the command. Example:

@holdex pr submit-time 15m

See available commands to help comply with our Guidelines.

@MicBun
Copy link
Copy Markdown
Collaborator Author

MicBun commented May 4, 2026

@holdex pr submit-time 4h

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
node/pg/repl_changeset_test.go (1)

15-82: 💤 Low value

New test harness and regression tests look correct and well-structured.

The newTestChangesetWriter helper is appropriate — the comment explaining that SerializeChangeset is never invoked for NullValue columns is accurate and important. The regression test TestDecodeUpdate_NilOldTuple cleanly covers the crash scenario.

One additional assertion worth adding to TestDecodeUpdate_NilOldTuple is ce.Kind() to lock in the current (Insert) classification and make the behavioral trade-off visible and intentional:

 assert.Nil(t, ce.OldTuple, "OldTuple must remain unset when WAL omits it")
 require.Len(t, ce.NewTuple, 1)
 assert.Equal(t, NullValue, ce.NewTuple[0].ValueType)
+// Kind() returns Insert (not Update) because OldTuple is nil/empty.
+// This means ApplyChangesetEntry will call applyInserts (INSERT ON CONFLICT DO NOTHING),
+// effectively discarding the update if the row already exists.
+assert.Equal(t, CSEntryKindInsert, ce.Kind(), "nil-OldTuple UPDATE is currently classified as Insert")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@node/pg/repl_changeset_test.go` around lines 15 - 82, Add an assertion in
TestDecodeUpdate_NilOldTuple that verifies the changeset entry kind is the
expected Insert classification: after retrieving ce :=
(<-csChan).(*ChangesetEntry) assert ce.Kind() == Insert (or the enum/value used
for insert in ChangesetEntry.Kind) to lock in the current behavior; reference
the ChangesetEntry type and its Kind() method so the test explicitly documents
the Insert classification choice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@node/pg/repl_changeset.go`:
- Around line 551-560: The emitted ChangesetEntry can have both OldTuple and
NewTuple nil causing DecodeTuples to return nil and applyDeletes to build a
"DELETE ... WHERE " SQL with no predicates; update applyDeletes to defensively
check the decoded record from ChangesetEntry.DecodeTuples(rel) (or check
ce.OldTuple/ce.NewTuple) and if the record is nil/empty return a clear error (or
skip the entry) instead of constructing a WHERE-less DELETE; reference the
applyDeletes function and ChangesetEntry/DecodeTuples/Kind so you add a guard
early that prevents building the malformed SQL and surfaces a descriptive error
to the caller.
- Around line 507-534: The code treats a nil update.OldTuple as an insert
(Kind() sees len(ce.OldTuple)==0) which causes WAL UPDATEs with no old tuple to
be silently handled as inserts; to fix, detect the update.OldTuple == nil case
right after the existing if update.OldTuple != nil block (where convertPgxTuple
is called) and emit a clear warning log (using the same logger/context used in
this file) stating that an UPDATE with nil OldTuple will be treated as an insert
and may be dropped by applyInserts; leave ce.OldTuple nil and continue
populating ce.NewTuple as before so behavior is unchanged but observable
(alternatively, if the intended policy is to drop the event, return early
instead of emitting—choose one and implement consistently), and reference
Kind(), ApplyChangesetEntry, and applyInserts in the log/context for operator
clarity.

---

Nitpick comments:
In `@node/pg/repl_changeset_test.go`:
- Around line 15-82: Add an assertion in TestDecodeUpdate_NilOldTuple that
verifies the changeset entry kind is the expected Insert classification: after
retrieving ce := (<-csChan).(*ChangesetEntry) assert ce.Kind() == Insert (or the
enum/value used for insert in ChangesetEntry.Kind) to lock in the current
behavior; reference the ChangesetEntry type and its Kind() method so the test
explicitly documents the Insert classification choice.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 74f9dd67-66ac-4537-a043-f92acbbd29ec

📥 Commits

Reviewing files that changed from the base of the PR and between 12ed615 and aa0e906.

📒 Files selected for processing (2)
  • node/pg/repl_changeset.go
  • node/pg/repl_changeset_test.go

Comment thread node/pg/repl_changeset.go
Comment thread node/pg/repl_changeset.go
@MicBun MicBun merged commit 236471e into main May 4, 2026
4 checks passed
@MicBun MicBun deleted the decoderReplit branch May 4, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant