From 97606f555210b4cbce8acbf0bbb12438eb9c0b80 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Mon, 13 Jul 2026 17:47:35 +0200 Subject: [PATCH] fix(changes): actionable error when settings exist but *_cloudsync tables are missing Querying cloudsync_changes on a database whose cloudsync_table_settings rows survived while every *_cloudsync meta table was lost (e.g. a dump/restore that skipped them) returned SQLITE_NOMEM: the NULL from vtab_build_changes_sql fell through the config-exists guard and was reported by SQLite Cloud as "Not enough memory to execute query" on the /check path, pointing operators at memory instead of the real state. - xFilter now distinguishes this state (settings rows present, zero %_cloudsync tables in sqlite_master) and raises "cloudsync settings reference tables whose sync metadata is missing" with the re-init remediation; genuine OOM still returns SQLITE_NOMEM - unit test: Changes Vtab Missing Meta Tables (red before, green after) - bump CLOUDSYNC_VERSION to 1.1.2 and add the CHANGELOG entry (SQLite vtab only, no PG changes, extension version stays 1.1) Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 6 +++++ src/cloudsync.h | 2 +- src/sqlite/cloudsync_changes_sqlite.c | 15 +++++++++++ test/unit.c | 36 +++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71306e7..d1a9aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [1.1.2] - 2026-07-13 + +### Fixed + +- 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". + ## [1.1.1] - 2026-07-10 ### Fixed diff --git a/src/cloudsync.h b/src/cloudsync.h index c721a88..3c2340d 100644 --- a/src/cloudsync.h +++ b/src/cloudsync.h @@ -18,7 +18,7 @@ extern "C" { #endif -#define CLOUDSYNC_VERSION "1.1.1" +#define CLOUDSYNC_VERSION "1.1.2" #define CLOUDSYNC_MAX_TABLENAME_LEN 512 #define CLOUDSYNC_VALUE_NOTSET -1 diff --git a/src/sqlite/cloudsync_changes_sqlite.c b/src/sqlite/cloudsync_changes_sqlite.c index 58a9b88..5cd8a14 100644 --- a/src/sqlite/cloudsync_changes_sqlite.c +++ b/src/sqlite/cloudsync_changes_sqlite.c @@ -413,6 +413,21 @@ int cloudsync_changesvtab_filter (sqlite3_vtab_cursor *cursor, int idxn, const c "SELECT cloudsync_init('') to enable sync on a " "table before querying cloudsync_changes."); } + // settings rows exist but no *_cloudsync tables do (e.g. a dump/restore + // that skipped the meta tables): still not an OOM + sqlite3_stmt *vm = NULL; + bool meta_missing = false; + 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) { + meta_missing = (sqlite3_step(vm) != SQLITE_ROW); + } + if (vm) sqlite3_finalize(vm); + if (meta_missing) { + return vtab_set_error((sqlite3_vtab *)c->vtab, + "cloudsync settings reference tables whose sync metadata is " + "missing (no *_cloudsync tables found). Re-run " + "SELECT cloudsync_init('') for each configured " + "table to rebuild it."); + } return SQLITE_NOMEM; } diff --git a/test/unit.c b/test/unit.c index 5989723..ca7d6fc 100644 --- a/test/unit.c +++ b/test/unit.c @@ -12976,6 +12976,41 @@ bool do_test_payload_chunks_uninitialized (bool print_result, bool cleanup_datab return result; } +// A database whose cloudsync_table_settings rows survived while the *_cloudsync +// meta tables were lost (e.g. a dump/restore that skipped them) must raise an +// actionable error from cloudsync_changes, not a misclassified SQLITE_NOMEM +// that servers report as "Not enough memory to execute query". +bool do_test_changes_vtab_missing_meta_tables (bool print_result) { + sqlite3 *db = NULL; + sqlite3_stmt *stmt = NULL; + bool result = false; + + db = do_create_database(); + if (!db) goto finalize; + + if (sqlite3_exec(db, "CREATE TABLE barang (id TEXT PRIMARY KEY, name TEXT);", NULL, NULL, NULL) != SQLITE_OK) goto finalize; + if (sqlite3_exec(db, "SELECT cloudsync_init('barang');", NULL, NULL, NULL) != SQLITE_OK) goto finalize; + if (sqlite3_exec(db, "INSERT INTO barang VALUES ('1','test');", NULL, NULL, NULL) != SQLITE_OK) goto finalize; + + // settings rows survive, meta table does not + if (sqlite3_exec(db, "DROP TABLE barang_cloudsync;", NULL, NULL, NULL) != SQLITE_OK) goto finalize; + + if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM cloudsync_changes;", -1, &stmt, NULL) != SQLITE_OK) goto finalize; + if (sqlite3_step(stmt) != SQLITE_ERROR) goto finalize; + if (!strstr(sqlite3_errmsg(db), "sync metadata")) goto finalize; + sqlite3_finalize(stmt); stmt = NULL; + + result = true; + +finalize: + if (!result && print_result) { + printf("do_test_changes_vtab_missing_meta_tables error: %s\n", db ? sqlite3_errmsg(db) : "no db"); + } + if (stmt) sqlite3_finalize(stmt); + if (db) close_db(db); + return result; +} + bool do_test_payload_idempotency (int nclients, bool print_result, bool cleanup_databases) { sqlite3 *db[2] = {NULL, NULL}; bool result = false; @@ -13429,6 +13464,7 @@ int main (int argc, const char * argv[]) { result += test_report("Payload Chunks Split db_version:", do_test_payload_chunks_split_dbversion(print_result, cleanup_databases)); result += test_report("Payload Chunks Positional Resume:", do_test_payload_chunks_positional_resume(print_result, cleanup_databases)); result += test_report("Payload Chunks Uninitialized:", do_test_payload_chunks_uninitialized(print_result, cleanup_databases)); + result += test_report("Changes Vtab Missing Meta Tables:", do_test_changes_vtab_missing_meta_tables(print_result)); // close local database close_db(db);