All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
- Querying the SQLite
cloudsync_changesvirtual table (directly or viacloudsync_payload_blob_checked()) on a database whosecloudsync_table_settingsrows survived while all*_cloudsyncmeta tables were lost (e.g. a dump/restore that skipped them) now raises an actionable error ("cloudsync settings reference tables whose sync metadata is missing ...") instead of a misclassifiedSQLITE_NOMEM, which servers reported as "Not enough memory to execute query".
- Querying the SQLite
cloudsync_payload_chunks()virtual table on a database where cloudsync is not initialized now raises the same actionable error ascloudsync_payload_apply()("cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') ...") instead of a bare "SQL logic error". Errors raised by the internalcloudsync_changesscan (including the watermark computation, which previously ignored them) are now propagated with their message instead of being masked.
- Chunked payload generation via
cloudsync_payload_chunks(), available as a SQLite virtual table and as a PostgreSQL set-returning function. The API emits transport-sized payload chunks and transparently fragments oversized BLOB/TEXT values into v3 fragment payloads. payload_max_chunk_sizeglobal setting for controlling generated chunk size. The default is 5 MB and values are clamped to the 256 KB technical minimum and the 32 MB technical maximum.exclude_filter_site_idargument forcloudsync_payload_chunks(). When set, the function streams changes from every site exceptfilter_site_id, which is what the/checkdownload path needs (a peer must not receive its own changes back). The default (omitted/false) preserves the existing single-site behavior. Passing the flag without afilter_site_idis an error.cloudsync_payload_blob_checked()scalar function on both SQLite and PostgreSQL. It performs an internal conservative size check before generating one monolithic legacy payload BLOB, allowing/checkendpoints to support old clients with one SQL round trip while rejecting unsafe responses before payload materialization.cloudsync_uuid_text()/cloudsync_uuid_blob()scalar functions on both SQLite and PostgreSQL, converting between the 16-byte binarysite_idand its canonical UUID string.cloudsync_uuid_text()takes an optionaldash_formatargument (defaulttrue);cloudsync_uuid_blob()accepts dashed or undashed, case-insensitive input. These let string-based callers (e.g. the/checkendpoint) pass asite_idtocloudsync_payload_chunks().- Payload chunking documentation in
API.mdandPERFORMANCE.md, including the explicit memory note that chunking bounds transport payloads but the database must still materialize a completed single BLOB/TEXT value when it is applied. - PostgreSQL
1.0 -> 1.1upgrade script (migrations/cloudsync--1.0--1.1.sql) for the new chunked-payload SQL surface, so existing deployments canALTER EXTENSION cloudsync UPDATE. cloudsync_network_receive_changes([max_chunks])as the canonical name for receiving changes (the oldcloudsync_network_check_changes()is retained as a deprecated alias). It drains all currently-available chunks by default; the optionalmax_chunksargument caps how many chunks are applied per call for caller-driven progress/traffic control (the in-memory page cursor persists between calls, so a capped drain resumes where it left off).chunksandbytesfields in thesendandreceiveJSON of the network functions, pluscompleteinreceive.chunksis the number of payload chunks sent/applied;bytesis the serialized (uncompressed) payload bytes;completeisfalsewhen a chunked download stopped before the final chunk, signalling the caller to call again.
cloudsync_network_check_changes([max_chunks])— usecloudsync_network_receive_changes()instead. The old name remains a thin, fully-functional alias and will be removed in a future major version.
cloudsync_network_receive_changes()(and itscloudsync_network_check_changes()alias) now drains all available chunks in one call (it does not wait for not-yet-ready server preparation). Previously each call fetched a single page.cloudsync_network_sync(wait_ms, max_retries)now drains an entire chunked download in a single call, fetching already-available chunks back-to-back with no delay.wait_ms/max_retriesare now spent only while the server payload is not yet ready (HTTP 202), not while paging through chunks that are already available. Previously a multi-chunk stream required severalcloudsync_network_sync()calls and wasted await_msdelay on each staged fragment.receive.rowsis now the cumulative number of rows applied across all chunks drained in the call (previously only the last page was reported);receive.tableslikewise reports the union of tables touched across the whole drain.cloudsync_payload_apply()now accepts legacy payloads, monolithic payloads, and v3 fragment payloads without enforcing the localpayload_max_chunk_size, preserving compatibility between peers with different settings.cloudsync_network_send_changes()now streams outgoing changes throughcloudsync_payload_chunks()instead of first building one monolithic payload. This bounds transport payload size for the built-in network path and lets large rowsets or oversized BLOB/TEXT values flow through the same/applyendpoint as regular payloads.- The built-in
/checkreceive path now advertisesX-CloudSync-Capabilities: check-status-response, check-chunksand can apply cursor-mode pages returned inline asdata.payloadbase64 bytes, in addition to larger pages returned asdata.urldownload artifacts. Requests sendcursor; responses providenextCursorwhen another page is available. - The chunked-download receive path advances the local receive checkpoint (
check_dbversion/check_seq) only after a chunk stream has been fully applied, jumping straight to the stream watermark — never into the middle of a sourcedb_version. This mirrors the send path and ensures a stop between chunks cannot skip the un-applied rows of adb_versionsplit across chunks on the next/check(the server resumes ondb_version > since, with no intra-version cursor).cloudsync_payload_apply()no longer advances the receive checkpoint per applied chunk; the built-in network/checkpath drives it from the server's watermark and final-chunk signal, and falls back to the previous monolithic behavior when the server sends no watermark. Re-delivered rows remain idempotent.
- PostgreSQL backend crash (segfault) on an error raised after
cloudsync_changes_select(). The set-returning function returned viaSRF_RETURN_NEXT/SRF_RETURN_DONEfrom inside itsPG_TRYblock, which skipsPG_END_TRY()and leavesPG_exception_stackpointing at the function's already-returned stack frame. A laterereport(ERROR)in the same query — such as thecloudsync_payload_blob_checked()size-limit check — thensiglongjmp()d into freed stack and crashed the backend. TheSRF_RETURN_*calls now run afterPG_END_TRY()so the exception stack is always restored. This is a pre-existing bug, not specific to the chunked-payload work.
- Improved network sync performance by reducing request overhead during
cloudsync_network_send_changes(), especially for small payloads that can now be applied without the extra upload-URL round trip. - Improved repeated sync request latency by allowing the network layer to reuse HTTP connections across CloudSync API calls.
- Mac Catalyst support.
cloudsync_network_check_changes()no longer errors withmissing 'url' in check responsewhen the server has not yet prepared any incoming changes for this device. The function now returns the standard "no rows yet" response in that case, so polling loops keep working without spurious errors.
receive.lastFailureJSON field oncloudsync_network_check_changes()andcloudsync_network_sync(), surfacing the most recent server-side failure of the receive pipeline (e.g. the server failed to prepare the next batch of incoming changes for this device). It complements the existingsend.lastFailure(server-side apply failures) andreceive.error(local apply failures on this device), so applications can distinguish "the server has trouble producing my changes" from "I had trouble applying them locally". Each function reports only the failures relevant to its own scope:cloudsync_network_send_changes()reportssend.lastFailure;cloudsync_network_check_changes()reportsreceive.lastFailure;cloudsync_network_sync()reports both.
- Updated the request headers sent to the cloudsync HTTP endpoints (version advertisement, per-endpoint capabilities; legacy
Acceptheader removed).
- Confusing errors when
cloudsync_initwas never called:cloudsync_changes(SQLite),cloudsync_db_version,cloudsync_db_version_next,cloudsync_set_filter,cloudsync_clear_filter, andcloudsync_payload_applynow raise a single actionable message pointing atSELECT cloudsync_init('<table_name>')instead of leaking low-level symptoms (out of memory,not an error, silent-1, multi-line "no such table" dumps). The guard runs only on the error branch, so the sync hot path is unaffected.
- WASM crash in
cloudsync_set_columnon existing rows: Callingcloudsync_set_column(table, col, 'lww', 'block')on a table with pre-existing rows crashed the WASM build with aRuntimeError: function signature mismatchas soon as the block-index migration tried to allocate memory.block_init_allocatorwas castingcloudsync_memory_alloc(auint64_t sizefunction) directly to the fractional-indexing allocator'svoid *(*)(size_t)slot. The cast is a no-op on native platforms wheresize_tis 64-bit, but WASM'scall_indirectenforces strict type checking — the function is registered as(i64) -> i32and called as(i32) -> i32, triggering an immediate runtime error. A thinfi_malloc_wrapper(mirroring the existingfi_calloc_wrapper) now bridges the signatures. Native builds are unaffected.
- Silent receive failures: When
cloudsync_payload_applyfailed during the receive phase (for example with an unknown schema hash, invalid checksum, or decompression error), the error was stored only on the internal cloudsync context and never propagated to the SQL caller. Bothcloudsync_network_check_changes()andcloudsync_network_sync()silently returned no result. Apply errors are now surfaced as areceive.errorfield in the JSON response.
- Error handling contract: endpoint/network errors (server unreachable, auth failure, bad URL) always raise a SQL error. Processing errors (
cloudsync_payload_applyfailures) are returned as structured JSON viareceive.errororsend.lastFailure, so callers can inspect and log them without try/catch logic. cloudsync_network_send_changes()output now includes asend.lastFailureobject whenever the server reports one (raw pass-through of the server'slastFailure—jobId,code,message,retryable,failedAt, …), regardless of whether the computedsend.statusissynced,syncing, orout-of-sync. The field is omitted when the server does not report a failure.cloudsync_network_check_changes()output now includes areceive.errorstring whencloudsync_payload_applyfails, instead of silently returning NULL. Endpoint/network errors still raise a SQL error.cloudsync_network_sync()output now mirrors the samesend.lastFailurefield and, if the receive phase has a processing error (cloudsync_payload_applyfailure), returns structured JSON with areceive.errorstring rather than failing silently. The send result is always preserved so callers can tell that their local changes reached the server even when applying incoming changes failed. Endpoint/network errors during the receive phase still raise a SQL error. The receive retry loop breaks immediately on processing errors (a schema-hash mismatch will not heal across retries).
- Stale
cloudsync_table_settingscrash: Reopening a database that had its base table and<table>_cloudsyncmeta-table dropped without callingcloudsync_cleanupcrashed with a double-free onsqlite3_close. Two bugs were involved: (1)cloudsync_dbversion_rebuildreturnedDBRES_NOMEMwhencloudsync_dbversion_build_queryyielded a NULL SQL string (stale row incloudsync_table_settingsbut no matching*_cloudsynctable insqlite_master), failing extension init; (2) on init failuredbsync_register_functionsmanually freed the context that SQLite already owned via thecloudsync_versiondestructor, causing a double-free when the connection was later closed.cloudsync_dbversion_rebuildnow treats a NULL build query the same ascount == 0(no prepared statement, db_version stays at the minimum and is rebuilt on the nextcloudsync_init), and the manual free in the error path has been removed.
- Unit test
do_test_stale_table_settings_dropped_meta(Stale Table Settings Dropped Meta) covering the drop-base-table + drop-meta-table + reopen scenario.
- Block-level LWW migration: When
cloudsync_set_column(..., 'algo', 'block')is called on a table that already has tracked rows, those rows are now immediately migrated into the blocks table. Previously, pre-existing column values were ignored until the next UPDATE, leaving sync state incomplete. The migration uses a two-phase collect-then-write approach to avoid SQLite cursor invalidation andINSERT OR IGNORE/ON CONFLICT DO NOTHINGsemantics for idempotency.
- Unit test
do_test_block_lww_existing_data(Block LWW Existing Data) verifying block migration onset_column, idempotency of repeatedset_columncalls, and correct materialization after update. - PostgreSQL test
50_block_lww_existing_data.sqlwith equivalent coverage for the PostgreSQL backend.
- Settings loader: Prevent infinite loop in
sqlite3_cloudsync_initwhen reopening a database that has a persisted block-column setting.dbutils_settings_table_load_callbackwas callingcloudsync_setup_block_column, whichREPLACEd the same row intocloudsync_table_settingswhilesqlite3_execwas still iterating it, re-feeding the rewritten row to the cursor. Added apersistflag tocloudsync_setup_block_columnso the loader replays the in-memory setup without writing back. - PostgreSQL tests: Updated 168
cloudsync_initcallsites across 43test/postgresql/*.sqlfiles to pass integer flags (0/1) instead oftrue/false, matching the signature change in 1.0.9. - CI: The
postgres-testjob now fails on SQL errors and[FAIL]markers.psqlis run withON_ERROR_STOP=on,pipefailis enabled around thetee, and the captured log is grepped for[FAIL]/psql ERRORas a final guard.
- Unit test
do_test_block_column_reload(Block Column Reload) that persists a block column with a custom delimiter, closes the database, and reopens it — without the fix this hangs the test process.
- cloudsync_cleanup: Now also drops the
{table}_cloudsync_blockstable when the table has block LWW columns configured viacloudsync_set_column(..., 'algo', 'block').
- Unit test
do_test_block_lww_cleanupverifying that both{table}_cloudsyncand{table}_cloudsync_blocksare removed aftercloudsync_cleanup.
- PostgreSQL: Prevent debug assertion crash on
cloudsync_initerror path (#37). - Row filter:
cloudsync_set_filterandcloudsync_clear_filternow reset the metatable and refill it from scratch, ensuring only rows matching the active filter are tracked for sync (#38).
- Row filter edge-case test coverage: clear/change filter lifecycle, complex expressions (AND, IS NULL), row enter/exit via UPDATE, composite PK with multi-column filters, multi-table roundtrip sync, and pre-existing data prefill tests for both SQLite and PostgreSQL.
- cloudsync_init: Replaced the
forceboolean parameter with aninit_flagsinteger bitmask (CLOUDSYNC_INIT_FLAG), allowing fine-grained control over which schema sanity checks are skipped. Existing callers passing0/falseor1/trueremain compatible. - API: Updated
cloudsync_initSQL signature (PostgreSQL) to acceptintegerinstead ofbooleanfor the third argument, enabling flag combinations via bitwise OR.
CLOUDSYNC_INIT_FLAG_NONE(0),CLOUDSYNC_INIT_FLAG_SKIP_INT_PK_CHECK(1),CLOUDSYNC_INIT_FLAG_SKIP_NOT_NULL_DEFAULT_CHECK(2),CLOUDSYNC_INIT_FLAG_SKIP_NOT_NULL_PRIKEYS_CHECK(4) enum values.- Documentation for
cloudsync_set_filterandcloudsync_clear_filterin API.md.
- CI/CD: Fix flutter package publish workflow not triggering on new releases.
- Harden table initialization against stale config and error cleanup.
- Swift Package: Use binary target and versioned macOS framework for Xcode 26 compatibility.
- Minor bugs in tests, docs, and examples related to the 1.0.0 major release.
- PostgreSQL support: The CloudSync extension can now be built and loaded on PostgreSQL, so both SQLiteCloud and PostgreSQL are supported as the cloud backend database of the sync service. The core CRDT functions are shared by the SQLite and PostgreSQL extensions. Includes support for PostgreSQL-native types (UUID primary keys, composite PKs with mixed types, and automatic type casting).
- Row-Level Security (RLS): Sync payloads are now fully compatible with SQLiteCloud and PostgreSQL Row-Level Security policies. Changes are buffered per primary key and flushed as complete rows, so RLS policies can evaluate all columns at once.
- Block-level LWW for text conflict resolution: Text columns can now be tracked at block level (lines by default) using Last-Writer-Wins. Concurrent edits to different parts of the same text are preserved after sync. New functions:
cloudsync_set_column()to write individual blocks andcloudsync_text_materialize()to reconstruct the full text.
-
BREAKING:
cloudsync_network_initnow accepts amanagedDatabaseIdinstead of a connection string. ThemanagedDatabaseIdis returned by the CloudSync service when a new database is registered for sync. For SQLiteCloud projects, it can be obtained from the project's OffSync page on the dashboard.Before:
SELECT cloudsync_network_init('sqlitecloud://myproject.sqlite.cloud:8860/mydb.sqlite?apikey=KEY');
After:
SELECT cloudsync_network_init('your-managed-database-id');
-
BREAKING: Sync functions now return structured JSON.
cloudsync_network_send_changes,cloudsync_network_check_changes, andcloudsync_network_syncreturn a JSON object instead of a plain integer. This provides richer status information including sync state, version numbers, row counts, and affected table names.Before:
SELECT cloudsync_network_sync(); -- 3 (number of rows received)
After:
SELECT cloudsync_network_sync(); -- '{"send":{"status":"synced","localVersion":5,"serverVersion":5},"receive":{"rows":3,"tables":["tasks"]}}'
-
Batch merge replaces column-by-column processing: During sync, changes to the same row are now applied in a single SQL statement instead of one statement per column. This eliminates the previous behavior where UPDATE triggers fired multiple times per row during synchronization.
-
Network endpoints updated for the CloudSync v2 HTTP service: Internal network layer now targets the new CloudSync service endpoints, including support for multi-organization routing.
-
NULL primary key rejection at runtime: The extension now enforces NULL primary key rejection at runtime, so the explicit
NOT NULLconstraint on primary key columns is no longer a schema requirement.
- Improved error reporting: Sync network functions now surface the actual server error message instead of generic error codes.
- Schema hash verification: Normalized schema comparison now uses only column name (lowercase), type (SQLite affinity), and primary key flag, preventing false mismatches caused by formatting differences.
- SQLite trigger safety: Internal functions used inside triggers are now marked with
SQLITE_INNOCUOUS, fixingunsafe use oferrors when initializing tables that have triggers. - NULL column binding: Column value parameters are now correctly bound even when NULL, preventing sync failures on rows with NULL values.
- Stability and reliability improvements across the SQLite and PostgreSQL codebases, including fixes to memory management, error handling, and CRDT version tracking.