Fix stale block content after SQLite INSERT OR REPLACE - #68
Merged
Merged
Conversation
andinux
force-pushed
the
fix/block-lww-upsert-tombstone
branch
from
September 22, 2026 18:24
6af69fd to
333bb2e
Compare
andinux
changed the base branch from
fix/block-lww-upsert-tombstone
to
main
September 22, 2026 19:17
Deleting a row keeps its block values, and SQLite's INSERT OR REPLACE skips the delete trigger unless recursive_triggers is on. A row written again rewrites its block column from the first position, so blocks past the new value's end stayed stored: on SQLite they kept live metadata and reached the peers as content (ZZZ became ZZZ/BBB/CCC), on PostgreSQL they only accumulated in the blocks table. local_block_update now reads the stored blocks on that path too, keeps them out of the diff, and tombstones and removes whatever the new value does not rewrite. Ported from the SQLite-only implementation in #68 so both backends share one block write. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
andinux
force-pushed
the
codex/block-replace-stale-blocks
branch
from
September 22, 2026 21:46
40bb4ab to
e816300
Compare
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
With SQLite's default
recursive_triggers=OFF,INSERT OR REPLACEskips the old row's delete trigger. Deleting a row also keeps its block values, so a row written again as a whole rewrites its block column from the first position and anything past the new value's end stays behind. Replacing block textAAA\nBBB\nCCCwithZZZproducedZZZ\nBBB\nCCCon replicas and on a later local materialization.local_block_updatenow reads the column's stored blocks on that path too. They stay out of the diff — a whole-row write is not an edit of the previous value — and whatever the new value does not rewrite is tombstoned and its value removed. Write failures propagate, so the enclosing statement rolls back rather than committing half a column.PostgreSQL shares that code and has a milder form of the same problem: the leftovers carry no metadata, so replicas were never affected, but they stayed in the blocks table for the life of the row. They are now retired there too.
Rebased onto main
This PR was rebased after #46, #64, #65, #66 and #67 merged, and now carries a single commit.
mainhad since moved the SQLite insert path onto the sharedlocal_block_insert/local_block_update, so the original SQLite-onlydbsync_replace_blocksno longer applied. The fix is ported into the shared block write instead, which keeps one implementation for both backends and preserves the error reportingmainadded.Validation
test/postgresql/64_block_rewrite_leftovers.sql— a row recreated with fewer blocks, and an upsert of the whole row, must leave one stored block and deliver the shorter value to a peer. Full suite withON_ERROR_STOP=on: 557 checks pass on standalone PostgreSQL 17, 555 on Supabase.main's code: the SQLite regression fails (replace: recursive=0 round=0 replica=0 mismatch) and the PostgreSQL test fails (shorter rewrite left 3 stored block(s)).CHANGELOG.mdhas an[Unreleased]entry;docs/block-replace-fix.mddescribes the behaviour and the tests.