Skip to content

Commit 1ad6947

Browse files
andinuxclaude
andcommitted
fix(payload): actionable error from cloudsync_payload_chunks when uninitialized
Querying the cloudsync_payload_chunks vtab on a database where cloudsync was never initialized surfaced a bare "SQL logic error": the internal statements over cloudsync_changes failed at step time and the error code propagated out of xFilter without ever setting the outer vtab's zErrMsg. The /check endpoint hit this on nodes not configured for cloudsync. Guard xFilter with the same is-initialized check (and message) used by cloudsync_payload_apply, propagate inner-statement messages onto the vtab, and stop ignoring watermark MAX() step failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent dfe6c8c commit 1ad6947

4 files changed

Lines changed: 71 additions & 3 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.1] - 2026-07-10
8+
9+
### Fixed
10+
11+
- Querying the SQLite `cloudsync_payload_chunks()` virtual table on a database where cloudsync is not initialized now raises the same actionable error as `cloudsync_payload_apply()` ("cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') ...") instead of a bare "SQL logic error". Errors raised by the internal `cloudsync_changes` scan (including the watermark computation, which previously ignored them) are now propagated with their message instead of being masked.
12+
713
## [1.1.0] - 2026-07-09
814

915
### Added

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.0"
21+
#define CLOUDSYNC_VERSION "1.1.1"
2222
#define CLOUDSYNC_MAX_TABLENAME_LEN 512
2323

2424
#define CLOUDSYNC_VALUE_NOTSET -1

src/sqlite/cloudsync_sqlite.c

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,7 +1229,12 @@ static int payload_chunks_step_source(cloudsync_payload_chunks_cursor *c) {
12291229
int rc = sqlite3_step(c->src);
12301230
if (rc == SQLITE_ROW) { c->has_row = true; return SQLITE_OK; }
12311231
c->has_row = false;
1232-
return rc == SQLITE_DONE ? SQLITE_OK : rc;
1232+
if (rc == SQLITE_DONE) return SQLITE_OK;
1233+
// copy the inner statement's message onto this vtab or SQLite surfaces the
1234+
// error as a bare "SQL logic error"
1235+
if (c->vtab->base.zErrMsg) sqlite3_free(c->vtab->base.zErrMsg);
1236+
c->vtab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(c->vtab->db));
1237+
return rc;
12331238
}
12341239

12351240
static int payload_chunks_plan_fragment(cloudsync_payload_chunks_cursor *c) {
@@ -1427,6 +1432,12 @@ static int payload_chunks_filter(sqlite3_vtab_cursor *cursor, int idxnum, const
14271432
UNUSED_PARAMETER(idxstr); UNUSED_PARAMETER(argc);
14281433
cloudsync_payload_chunks_cursor *c = (cloudsync_payload_chunks_cursor *)cursor;
14291434
cloudsync_context *data = c->vtab->data;
1435+
if (!cloudsync_context_is_initialized(data)) {
1436+
c->vtab->base.zErrMsg = sqlite3_mprintf(
1437+
"cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
1438+
"to enable sync on a table before querying cloudsync_payload_chunks.");
1439+
return SQLITE_ERROR;
1440+
}
14301441
if (c->src) { sqlite3_finalize(c->src); c->src = NULL; }
14311442
if (c->payload) { cloudsync_memory_free(c->payload); c->payload = NULL; }
14321443
// Contract: all per-scan state that can be bulk-reset here must live at or
@@ -1489,8 +1500,16 @@ static int payload_chunks_filter(sqlite3_vtab_cursor *cursor, int idxnum, const
14891500
sqlite3_free(mxsql);
14901501
if (rc != SQLITE_OK) return rc;
14911502
sqlite3_bind_blob(mx, 1, site_id, site_id_len, SQLITE_TRANSIENT);
1492-
if (sqlite3_step(mx) == SQLITE_ROW) until = sqlite3_column_int64(mx, 0);
1503+
// MAX() yields exactly one row, so anything else is an error: propagate
1504+
// it instead of silently scanning an empty window with until=0
1505+
rc = sqlite3_step(mx);
1506+
if (rc == SQLITE_ROW) until = sqlite3_column_int64(mx, 0);
14931507
sqlite3_finalize(mx);
1508+
if (rc != SQLITE_ROW) {
1509+
if (c->vtab->base.zErrMsg) sqlite3_free(c->vtab->base.zErrMsg);
1510+
c->vtab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(c->vtab->db));
1511+
return rc == SQLITE_DONE ? SQLITE_ERROR : rc;
1512+
}
14941513
}
14951514
c->watermark = until;
14961515

test/unit.c

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12934,6 +12934,48 @@ bool do_test_payload_chunks_positional_resume (bool print_result, bool cleanup_d
1293412934
return result;
1293512935
}
1293612936

12937+
// The /check endpoint queries cloudsync_payload_chunks on databases that may not
12938+
// be configured for cloudsync: the vtab must raise the same actionable message
12939+
// cloudsync_payload_apply gives, not a bare "SQL logic error".
12940+
bool do_test_payload_chunks_uninitialized (bool print_result, bool cleanup_databases) {
12941+
sqlite3 *db = NULL;
12942+
sqlite3_stmt *stmt = NULL;
12943+
bool result = false;
12944+
12945+
time_t timestamp = time(NULL);
12946+
int saved_counter = test_counter++;
12947+
12948+
db = do_create_database_file(0, timestamp, saved_counter);
12949+
if (!db) goto finalize;
12950+
12951+
// no cloudsync_init on purpose
12952+
if (sqlite3_prepare_v2(db,
12953+
"SELECT payload FROM cloudsync_payload_chunks(0, cloudsync_uuid_blob('0190a1b2-c3d4-7e5f-8a9b-001122334455'), NULL, 1) LIMIT 1;",
12954+
-1, &stmt, NULL) != SQLITE_OK) goto finalize;
12955+
if (sqlite3_step(stmt) != SQLITE_ERROR) goto finalize;
12956+
if (!strstr(sqlite3_errmsg(db), "cloudsync is not initialized")) goto finalize;
12957+
sqlite3_finalize(stmt); stmt = NULL;
12958+
12959+
result = true;
12960+
12961+
finalize:
12962+
if (!result && print_result) {
12963+
printf("do_test_payload_chunks_uninitialized error: %s\n", db ? sqlite3_errmsg(db) : "no db");
12964+
}
12965+
if (stmt) sqlite3_finalize(stmt);
12966+
if (db) close_db(db);
12967+
if (cleanup_databases) {
12968+
char path[256], walpath[300], shmpath[300];
12969+
do_build_database_path(path, 0, timestamp, saved_counter);
12970+
snprintf(walpath, sizeof(walpath), "%s-wal", path);
12971+
snprintf(shmpath, sizeof(shmpath), "%s-shm", path);
12972+
file_delete_internal(path);
12973+
file_delete_internal(walpath);
12974+
file_delete_internal(shmpath);
12975+
}
12976+
return result;
12977+
}
12978+
1293712979
bool do_test_payload_idempotency (int nclients, bool print_result, bool cleanup_databases) {
1293812980
sqlite3 *db[2] = {NULL, NULL};
1293912981
bool result = false;
@@ -13386,6 +13428,7 @@ int main (int argc, const char * argv[]) {
1338613428
result += test_report("Payload Chunks Site Exclusion:", do_test_payload_chunks_site_exclusion(print_result, cleanup_databases));
1338713429
result += test_report("Payload Chunks Split db_version:", do_test_payload_chunks_split_dbversion(print_result, cleanup_databases));
1338813430
result += test_report("Payload Chunks Positional Resume:", do_test_payload_chunks_positional_resume(print_result, cleanup_databases));
13431+
result += test_report("Payload Chunks Uninitialized:", do_test_payload_chunks_uninitialized(print_result, cleanup_databases));
1338913432

1339013433
// close local database
1339113434
close_db(db);

0 commit comments

Comments
 (0)