fix(postgres): give every transaction its own db_version and restart seq - #78
Merged
Merged
Conversation
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
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>
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.
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 aUNION ALLof oneMAX(db_version)per meta table but had no outerMAX, 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
sql_postgresql.c): wrap the union inCOALESCE(MAX(version), 0)and includepre_alter_dbversion, as SQLite does.cloudsync_postgresql.c):RegisterXactCallbackcloses each transaction the way the SQLite commit/rollback hooks do, via the newcloudsync_transaction_end.seqrestarts 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.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 takespending + 1) and never promotes it into the committed cache, so a rolled back apply leavescloudsync_db_version()unchanged. It is a no-op after the per-db_version savepoint release on SQLite autocommit.C-only: no
cloudsync.sql.inchange and no migration.Tests
test_db_version_per_transaction(SQLite) andtest/postgresql/66_db_version_per_transaction.sql. Both run the same scenario with the same expected numbering, plus the pinned-xmin case on PostgreSQL.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.feat/payload-chunks-window-cap): PostgreSQL 565 PASS; SQLite passes. The only conflict is one line intest/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