Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **SQLite: a payload whose commit fails no longer leaves its transaction open.** When `cloudsync_payload_apply` started the transaction itself and the commit then failed — a deferred foreign key violated at commit, or `SQLITE_BUSY` because a reader held the database — the transaction stayed open: the uncommitted rows remained visible on the connection and the next `BEGIN` failed. The failed transaction is now rolled back and the original error is returned. Changes from earlier source versions that were already committed are kept, the receive checkpoint does not move, and the rolled-back rows are no longer counted as applied, so delivering the payload again applies it. A transaction or savepoint opened by the caller is still left to the caller.
- **PostgreSQL: applying a payload read from a table works at any savepoint depth.** Inside 126 or more savepoints, `SELECT cloudsync_payload_apply(payload) FROM some_table` still failed with `buffer pin ... is not owned by resource owner SubTransaction` (and a caught error at that depth could abort an assertion-enabled server): cloudsync recorded the caller's resource owner and memory context for at most 128 nesting levels, counting its own internal savepoints, and silently stopped restoring them beyond that. The fixed limit is gone; only PostgreSQL's own resource limits apply.
- **A received row with a block column is applied all or nothing.** When a row's block value failed to write (a trigger that raises, a constraint, a policy), its ordinary columns and their sync metadata stayed applied, leaving the row with a missing or stale block value. The row's ordinary columns, metadata and blocks now roll back together, the receive checkpoint does not move, and delivering the payload again applies the whole row once the cause is fixed. Rows rolled back this way are no longer counted as applied.
- **A block column rewritten in place is no longer lost by the peers it syncs to.** Rewriting a row that holds a block column without changing its primary key — `INSERT OR REPLACE`, or any write that rewrites the same block positions — marked every rewritten block as deleted in the sync metadata while keeping its content locally. The local row still read correctly, so nothing looked wrong, but the peers received the blocks as deleted and dropped the row. The metadata write now keeps a block live when it is written as live, and deleted when it is written as deleted, instead of flipping the two on every write. The same flip affected a row's deletion marker.

## [1.1.4] - 2026-09-21

Expand Down
1 change: 1 addition & 0 deletions docker/postgresql/docker-compose.debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ services:
context: ../..
dockerfile: docker/postgresql/Dockerfile.debug-no-optimization
container_name: cloudsync-postgres
command: postgres -c listen_addresses=*
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
Expand Down
4 changes: 3 additions & 1 deletion src/cloudsync.c
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,9 @@ int table_add_stmts (cloudsync_table_context *table, int ncols) {
if (rc != DBRES_OK) goto cleanup;

// precompile the insert/update local row statement
sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_UPSERT_RAW_COLVERSION, table->meta_ref, table->meta_ref);
sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_UPSERT_RAW_COLVERSION,
table->meta_ref, table->meta_ref,
table->meta_ref, table->meta_ref);
if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
DEBUG_SQL("meta_row_insert_update_stmt: %s", sql);

Expand Down
5 changes: 4 additions & 1 deletion src/postgresql/sql_postgresql.c
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,10 @@ const char * const SQL_CLOUDSYNC_UPSERT_RAW_COLVERSION =
"INSERT INTO %s (pk, col_name, col_version, db_version, seq, site_id) "
"VALUES ($1, $2, $3, $4, $5, 0) "
"ON CONFLICT (pk, col_name) DO UPDATE SET "
"col_version = %s.col_version + 1, db_version = $6, seq = $7, site_id = 0;";
"col_version = CASE "
"WHEN (%s.col_version %% 2) = (excluded.col_version %% 2) THEN %s.col_version + 2 "
"ELSE %s.col_version + 1 END, "
"db_version = $6, seq = $7, site_id = 0;";

const char * const SQL_CLOUDSYNC_DELETE_PK_EXCEPT_COL =
"DELETE FROM %s WHERE pk = $1 AND col_name != '%s';"; // TODO: match SQLite delete semantics
Expand Down
5 changes: 4 additions & 1 deletion src/sqlite/sql_sqlite.c
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,10 @@ const char * const SQL_CLOUDSYNC_UPSERT_RAW_COLVERSION =
"SELECT ?, ?, ?, ?, ?, 0 "
"WHERE 1 "
"ON CONFLICT DO UPDATE SET "
"col_version = \"%w\".col_version + 1, db_version = ?, seq = ?, site_id = 0;";
"col_version = CASE "
"WHEN (\"%w\".col_version %% 2) = (excluded.col_version %% 2) THEN \"%w\".col_version + 2 "
"ELSE \"%w\".col_version + 1 END, "
"db_version = ?, seq = ?, site_id = 0;";

const char * const SQL_CLOUDSYNC_DELETE_PK_EXCEPT_COL =
"DELETE FROM \"%w\" WHERE pk=? AND col_name!='%s';";
Expand Down
Loading
Loading