Skip to content

Commit c0e46e6

Browse files
andinuxclaude
andcommitted
Merge main into test/e2e-token-auth (resolve integration.c helper conflict)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 parents 94eafb7 + 6d34e85 commit c0e46e6

6 files changed

Lines changed: 108 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

7+
## [1.1.2] - 2026-07-13
8+
9+
### Fixed
10+
11+
- Querying the SQLite `cloudsync_changes` virtual table (directly or via `cloudsync_payload_blob_checked()`) on a database whose `cloudsync_table_settings` rows survived while all `*_cloudsync` meta 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 misclassified `SQLITE_NOMEM`, which servers reported as "Not enough memory to execute query".
12+
713
## [1.1.1] - 2026-07-10
814

915
### Fixed

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ let package = Package(
1414
targets: [
1515
.binaryTarget(
1616
name: "CloudSyncBinary",
17-
url: "https://github.com/sqliteai/sqlite-sync/releases/download/1.1.1/cloudsync-apple-xcframework-1.1.1.zip",
18-
checksum: "cd280eec57b2dd3b7c7c4ee80cd68d12830ebfd81597141b80b8deadeced7979"
17+
url: "https://github.com/sqliteai/sqlite-sync/releases/download/1.1.2/cloudsync-apple-xcframework-1.1.2.zip",
18+
checksum: "60f9d5a5e08f3327773991d605c032a33d8d32e72b93cf3eef4244706e8c57da"
1919
),
2020
.target(
2121
name: "CloudSync",

src/cloudsync.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
extern "C" {
1919
#endif
2020

21-
#define CLOUDSYNC_VERSION "1.1.1"
21+
#define CLOUDSYNC_VERSION "1.1.2"
2222
#define CLOUDSYNC_MAX_TABLENAME_LEN 512
2323

2424
#define CLOUDSYNC_VALUE_NOTSET -1

src/sqlite/cloudsync_changes_sqlite.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,21 @@ int cloudsync_changesvtab_filter (sqlite3_vtab_cursor *cursor, int idxn, const c
413413
"SELECT cloudsync_init('<table_name>') to enable sync on a "
414414
"table before querying cloudsync_changes.");
415415
}
416+
// settings rows exist but no *_cloudsync tables do (e.g. a dump/restore
417+
// that skipped the meta tables): still not an OOM
418+
sqlite3_stmt *vm = NULL;
419+
bool meta_missing = false;
420+
if (sqlite3_prepare_v2(db, "SELECT 1 FROM sqlite_master WHERE type='table' AND tbl_name LIKE '%_cloudsync' LIMIT 1;", -1, &vm, NULL) == SQLITE_OK) {
421+
meta_missing = (sqlite3_step(vm) != SQLITE_ROW);
422+
}
423+
if (vm) sqlite3_finalize(vm);
424+
if (meta_missing) {
425+
return vtab_set_error((sqlite3_vtab *)c->vtab,
426+
"cloudsync settings reference tables whose sync metadata is "
427+
"missing (no *_cloudsync tables found). Re-run "
428+
"SELECT cloudsync_init('<table_name>') for each configured "
429+
"table to rebuild it.");
430+
}
416431
return SQLITE_NOMEM;
417432
}
418433

test/integration.c

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,34 @@ int db_send_await_converge (sqlite3 *db, int max_attempts, int delay_ms) {
272272
return SQLITE_ERROR;
273273
}
274274

275+
// Bring a fresh receiver up to date with the tenant's accumulated history before a
276+
// capped-receive test: send a sentinel row from the sender, then drain (uncapped)
277+
// until it arrives. A fresh site's first check makes the server spool the ENTIRE
278+
// tenant history — which grows with every run — answering 202 while it prepares, and
279+
// a 202 yields rows=0/complete=1, indistinguishable from "already drained", so only
280+
// the sentinel's arrival proves the replay actually finished. This keeps the capped
281+
// attempt budgets O(this test's batch) instead of O(tenant history).
282+
int chunked_receiver_catch_up (sqlite3 *sender, sqlite3 *receiver, const char *sentinel_id) {
283+
char sql[512];
284+
snprintf(sql, sizeof(sql),
285+
"INSERT INTO chunked_payload_items (id, body) VALUES ('%s', 'sentinel');", sentinel_id);
286+
int rc = db_exec(sender, sql); if (rc != SQLITE_OK) return rc;
287+
rc = db_send_ok(sender); if (rc != SQLITE_OK) return rc;
288+
289+
for (int i = 0; i < 120; i++) {
290+
int matches = 0;
291+
rc = db_exec(receiver, "SELECT cloudsync_network_receive_changes();");
292+
if (rc != SQLITE_OK) return rc;
293+
snprintf(sql, sizeof(sql),
294+
"SELECT COUNT(*) FROM chunked_payload_items WHERE id='%s';", sentinel_id);
295+
rc = db_select_int(receiver, sql, &matches); if (rc != SQLITE_OK) return rc;
296+
if (matches == 1) return SQLITE_OK;
297+
sqlite3_sleep(500);
298+
}
299+
printf("Error: receiver did not catch up with tenant history (sentinel %s missing).\n", sentinel_id);
300+
return SQLITE_ERROR;
301+
}
302+
275303
int integration_network_init_base(sqlite3 *db, const char *database_id, char *network_init, size_t network_init_len) {
276304
if (!database_id) {
277305
fprintf(stderr, "Error: integration database ID not set.\n");
@@ -1013,6 +1041,7 @@ int test_chunked_payload_capped_receive(void) {
10131041
sqlite3 *receiver = NULL;
10141042
char network_init[1024];
10151043
char batch_id[UUID_STR_MAXLEN];
1044+
char sentinel_id[UUID_STR_MAXLEN];
10161045
char sql[1024];
10171046
bool found = false;
10181047
bool observed_capped_partial = false; // saw chunks==1 && complete=0 from a receive(1) call
@@ -1032,6 +1061,14 @@ int test_chunked_payload_capped_receive(void) {
10321061
if (rc != SQLITE_OK) goto cleanup;
10331062

10341063
cloudsync_uuid_v7_string(batch_id, true);
1064+
cloudsync_uuid_v7_string(sentinel_id, true);
1065+
1066+
// Replay accumulated tenant history up front so the 80x1-chunk budget below only
1067+
// has to fetch this test's batch (see chunked_receiver_catch_up).
1068+
rc = chunked_receiver_catch_up(sender, receiver, sentinel_id);
1069+
if (rc != SQLITE_OK) goto cleanup;
1070+
cleanup_remote_rows = true;
1071+
10351072
rc = db_exec(sender, "SELECT cloudsync_set('payload_max_chunk_size', '262144');"); if (rc != SQLITE_OK) goto cleanup;
10361073
snprintf(sql, sizeof(sql),
10371074
"WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i < %d) "
@@ -1043,7 +1080,6 @@ int test_chunked_payload_capped_receive(void) {
10431080
rc = db_expect_min(sender, "SELECT COUNT(*) FROM cloudsync_payload_chunks();", 2); if (rc != SQLITE_OK) goto cleanup;
10441081

10451082
rc = db_send_ok(sender); if (rc != SQLITE_OK) goto cleanup;
1046-
cleanup_remote_rows = true;
10471083

10481084
for (int attempt = 0; attempt < 80; ++attempt) {
10491085
int chunks = 0, complete = 0, matches = 0;
@@ -1098,7 +1134,7 @@ int test_chunked_payload_capped_receive(void) {
10981134

10991135
cleanup:
11001136
if (cleanup_remote_rows && sender) {
1101-
snprintf(sql, sizeof(sql), "DELETE FROM chunked_payload_items WHERE id LIKE '%s-%%';", batch_id);
1137+
snprintf(sql, sizeof(sql), "DELETE FROM chunked_payload_items WHERE id LIKE '%s-%%' OR id='%s';", batch_id, sentinel_id);
11021138
if (db_exec(sender, sql) == SQLITE_OK) {
11031139
db_exec(sender, "SELECT cloudsync_network_send_changes();");
11041140
}
@@ -1116,6 +1152,7 @@ int test_chunked_payload_batched_receive(void) {
11161152
sqlite3 *receiver = NULL;
11171153
char network_init[1024];
11181154
char batch_id[UUID_STR_MAXLEN];
1155+
char sentinel_id[UUID_STR_MAXLEN];
11191156
char sql[1024];
11201157
bool found = false;
11211158
bool observed_batched_partial = false; // saw chunks==2 && complete=0 from receive(2)
@@ -1128,6 +1165,14 @@ int test_chunked_payload_batched_receive(void) {
11281165
if (rc != SQLITE_OK) goto cleanup;
11291166

11301167
cloudsync_uuid_v7_string(batch_id, true);
1168+
cloudsync_uuid_v7_string(sentinel_id, true);
1169+
1170+
// Replay accumulated tenant history up front so the capped budget below only has
1171+
// to fetch this test's batch (see chunked_receiver_catch_up).
1172+
rc = chunked_receiver_catch_up(sender, receiver, sentinel_id);
1173+
if (rc != SQLITE_OK) goto cleanup;
1174+
cleanup_remote_rows = true;
1175+
11311176
rc = db_exec(sender, "SELECT cloudsync_set('payload_max_chunk_size', '262144');"); if (rc != SQLITE_OK) goto cleanup;
11321177
snprintf(sql, sizeof(sql),
11331178
"WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i < %d) "
@@ -1139,7 +1184,6 @@ int test_chunked_payload_batched_receive(void) {
11391184
rc = db_expect_min(sender, "SELECT COUNT(*) FROM cloudsync_payload_chunks();", 3); if (rc != SQLITE_OK) goto cleanup;
11401185

11411186
rc = db_send_ok(sender); if (rc != SQLITE_OK) goto cleanup;
1142-
cleanup_remote_rows = true;
11431187

11441188
for (int attempt = 0; attempt < 80; ++attempt) {
11451189
int chunks = 0, complete = 0, matches = 0;
@@ -1186,7 +1230,7 @@ int test_chunked_payload_batched_receive(void) {
11861230

11871231
cleanup:
11881232
if (cleanup_remote_rows && sender) {
1189-
snprintf(sql, sizeof(sql), "DELETE FROM chunked_payload_items WHERE id LIKE '%s-%%';", batch_id);
1233+
snprintf(sql, sizeof(sql), "DELETE FROM chunked_payload_items WHERE id LIKE '%s-%%' OR id='%s';", batch_id, sentinel_id);
11901234
if (db_exec(sender, sql) == SQLITE_OK) {
11911235
db_exec(sender, "SELECT cloudsync_network_send_changes();");
11921236
}

test/unit.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12976,6 +12976,41 @@ bool do_test_payload_chunks_uninitialized (bool print_result, bool cleanup_datab
1297612976
return result;
1297712977
}
1297812978

12979+
// A database whose cloudsync_table_settings rows survived while the *_cloudsync
12980+
// meta tables were lost (e.g. a dump/restore that skipped them) must raise an
12981+
// actionable error from cloudsync_changes, not a misclassified SQLITE_NOMEM
12982+
// that servers report as "Not enough memory to execute query".
12983+
bool do_test_changes_vtab_missing_meta_tables (bool print_result) {
12984+
sqlite3 *db = NULL;
12985+
sqlite3_stmt *stmt = NULL;
12986+
bool result = false;
12987+
12988+
db = do_create_database();
12989+
if (!db) goto finalize;
12990+
12991+
if (sqlite3_exec(db, "CREATE TABLE barang (id TEXT PRIMARY KEY, name TEXT);", NULL, NULL, NULL) != SQLITE_OK) goto finalize;
12992+
if (sqlite3_exec(db, "SELECT cloudsync_init('barang');", NULL, NULL, NULL) != SQLITE_OK) goto finalize;
12993+
if (sqlite3_exec(db, "INSERT INTO barang VALUES ('1','test');", NULL, NULL, NULL) != SQLITE_OK) goto finalize;
12994+
12995+
// settings rows survive, meta table does not
12996+
if (sqlite3_exec(db, "DROP TABLE barang_cloudsync;", NULL, NULL, NULL) != SQLITE_OK) goto finalize;
12997+
12998+
if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM cloudsync_changes;", -1, &stmt, NULL) != SQLITE_OK) goto finalize;
12999+
if (sqlite3_step(stmt) != SQLITE_ERROR) goto finalize;
13000+
if (!strstr(sqlite3_errmsg(db), "sync metadata")) goto finalize;
13001+
sqlite3_finalize(stmt); stmt = NULL;
13002+
13003+
result = true;
13004+
13005+
finalize:
13006+
if (!result && print_result) {
13007+
printf("do_test_changes_vtab_missing_meta_tables error: %s\n", db ? sqlite3_errmsg(db) : "no db");
13008+
}
13009+
if (stmt) sqlite3_finalize(stmt);
13010+
if (db) close_db(db);
13011+
return result;
13012+
}
13013+
1297913014
bool do_test_payload_idempotency (int nclients, bool print_result, bool cleanup_databases) {
1298013015
sqlite3 *db[2] = {NULL, NULL};
1298113016
bool result = false;
@@ -13429,6 +13464,7 @@ int main (int argc, const char * argv[]) {
1342913464
result += test_report("Payload Chunks Split db_version:", do_test_payload_chunks_split_dbversion(print_result, cleanup_databases));
1343013465
result += test_report("Payload Chunks Positional Resume:", do_test_payload_chunks_positional_resume(print_result, cleanup_databases));
1343113466
result += test_report("Payload Chunks Uninitialized:", do_test_payload_chunks_uninitialized(print_result, cleanup_databases));
13467+
result += test_report("Changes Vtab Missing Meta Tables:", do_test_changes_vtab_missing_meta_tables(print_result));
1343213468

1343313469
// close local database
1343413470
close_db(db);

0 commit comments

Comments
 (0)