Skip to content

Fix stale block content after SQLite INSERT OR REPLACE - #68

Merged
andinux merged 3 commits into
mainfrom
codex/block-replace-stale-blocks
Sep 22, 2026
Merged

andinux merged 3 commits into
mainfrom
codex/block-replace-stale-blocks

Conversation

@marcobambini

@marcobambini marcobambini commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

With SQLite's default recursive_triggers=OFF, INSERT OR REPLACE skips 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 text AAA\nBBB\nCCC with ZZZ produced ZZZ\nBBB\nCCC on replicas and on a later local materialization.

local_block_update now 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. main had since moved the SQLite insert path onto the shared local_block_insert/local_block_update, so the original SQLite-only dbsync_replace_blocks no longer applied. The fix is ported into the shared block write instead, which keeps one implementation for both backends and preserves the error reporting main added.

Validation

  • SQLite: the original 120-cycle replacement regression (recursive triggers off and on; shorter, longer, empty, NULL, duplicate-line and trailing-delimiter values; fractional positions; duplicate delivery; injected write failure) passes unchanged, as does the whole unit suite, including the delete/re-insert test that a naive fix breaks.
  • PostgreSQL: new 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 with ON_ERROR_STOP=on: 557 checks pass on standalone PostgreSQL 17, 555 on Supabase.
  • Negative controls against 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.md has an [Unreleased] entry; docs/block-replace-fix.md describes the behaviour and the tests.

@andinux
andinux force-pushed the fix/block-lww-upsert-tombstone branch from 6af69fd to 333bb2e Compare September 22, 2026 18:24
@andinux
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
andinux force-pushed the codex/block-replace-stale-blocks branch from 40bb4ab to e816300 Compare September 22, 2026 21:46
andinux and others added 2 commits September 22, 2026 16:14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@andinux
andinux merged commit cf44ad0 into main Sep 22, 2026
38 checks passed
@andinux
andinux deleted the codex/block-replace-stale-blocks branch September 22, 2026 22:47
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