Skip to content

fix(postgres): give every transaction its own db_version and restart seq - #78

Merged
andinux merged 2 commits into
mainfrom
fix/pg-dbversion-seq-issue69
Sep 24, 2026
Merged

andinux merged 2 commits into
mainfrom
fix/pg-dbversion-seq-issue69

Conversation

@andinux

@andinux andinux commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #69.

Cause

The step 1 blocker in the issue (the cached db_version reloading with a lower value) came from the PostgreSQL SQL_DBVERSION_BUILD_QUERY. It built a UNION ALL of one MAX(db_version) per meta table but had no outer MAX, unlike SQLite, and only the first row was read. With more than one synced table, consecutive transactions writing another table all took the same db_version: three separate inserts into a second table landed on db_version 1.

Changes

  • Reload query (sql_postgresql.c): wrap the union in COALESCE(MAX(version), 0) and include pre_alter_dbversion, as SQLite does.
  • Transaction callback (cloudsync_postgresql.c): RegisterXactCallback closes each transaction the way the SQLite commit/rollback hooks do, via the new cloudsync_transaction_end. seq restarts at 0, and the next transaction takes a new db_version even while another session pins xmin (defects 1 and 2). It acts only when the transaction took a db_version, so read-only transactions keep the cache.
  • Apply (cloudsync_payload_apply): inside a caller's transaction, which is always the case on PostgreSQL, the local db_version is closed at every source db_version boundary, so remote versions no longer collapse into duplicate (db_version, seq) pairs (defect 3). The version is also closed after the last group and around each fragment value, so local writes in the same transaction take their own. Closing only marks the pending version (the next change takes pending + 1) and never promotes it into the committed cache, so a rolled back apply leaves cloudsync_db_version() unchanged. It is a no-op after the per-db_version savepoint release on SQLite autocommit.

C-only: no cloudsync.sql.in change and no migration.

Tests

  • New: test_db_version_per_transaction (SQLite) and test/postgresql/66_db_version_per_transaction.sql. Both run the same scenario with the same expected numbering, plus the pinned-xmin case on PostgreSQL.
  • New: test_db_version_apply_in_transaction (SQLite) and matching cases in test 66: local writes around an apply in one transaction, and a rolled back apply.
  • SQLite unit tests: pass.
  • PostgreSQL 17 debug: 562 PASS, 0 failures (tests 28 and 59 included).
  • Supabase (17.6.1.143): 560 PASS, 0 failures.
  • Combined with feat: bound what one cloudsync_payload_chunks() call prepares #77 (feat/payload-chunks-window-cap): PostgreSQL 565 PASS; SQLite passes. The only conflict is one line in test/postgresql/full_test.sql (both add a test; keep both).

Not covered: two backends picking the same next db_version concurrently, which predates this change.

🤖 Generated with Claude Code

andinux and others added 2 commits September 24, 2026 16:14
With more than one synced table, the query that reloads the cached
db_version returned one row per meta table and only the first was read,
so consecutive transactions writing another table shared one db_version.
Wrap it in MAX() and include pre_alter_dbversion, as on SQLite.

Close each transaction from a PostgreSQL transaction callback, as the
SQLite commit/rollback hooks do: seq restarts at 0 and the next
transaction takes a new db_version even while another session pins xmin.

Inside one transaction, cloudsync_payload_apply now closes the local
db_version at every source db_version boundary, so remote versions no
longer collapse into duplicate (db_version, seq) pairs.

Fixes #69

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Closing a payload group inside a transaction reused the commit hook,
which copied an uncommitted version into the committed cache: a local
write after the apply reused the last merged db_version with seq back
at 0, and a rolled back apply left the cache ahead of the tables.

A group now only marks the pending version closed, so the next change
takes pending + 1 while the committed version stays untouched. The
apply also closes it after the last group and around each fragment
value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@andinux
andinux merged commit 0c20064 into main Sep 24, 2026
38 checks passed
@andinux
andinux deleted the fix/pg-dbversion-seq-issue69 branch September 24, 2026 22:50
andinux added a commit that referenced this pull request Sep 24, 2026
The oversized values were kept in the already-synced table to avoid the issue
#69 step 1 defect, where the db_version reload read only the first synced
table's maximum and a second table's writes collapsed onto one db_version,
leaving the window no boundary to end on.

#78 fixed that, so the case goes back to a table of its own, which is what it
was meant to be. It now also exercises the fix: eight single-statement
transactions into a second table have to take eight db_versions for the
fragmented history to be capped into eight windows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
andinux added a commit that referenced this pull request Sep 25, 2026
The oversized values were kept in the already-synced table to avoid the issue
#69 step 1 defect, where the db_version reload read only the first synced
table's maximum and a second table's writes collapsed onto one db_version,
leaving the window no boundary to end on.

#78 fixed that, so the case goes back to a table of its own, which is what it
was meant to be. It now also exercises the fix: eight single-statement
transactions into a second table have to take eight db_versions for the
fragmented history to be capped into eight windows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

PostgreSQL: seq never restarts, db_version can be reused, and one apply collapses remote db_versions

1 participant