@@ -46,6 +46,7 @@ sqlite3 *do_create_database (void);
4646
4747int cloudsync_table_sanity_check (cloudsync_context *data, const char *name, CLOUDSYNC_INIT_FLAG init_flags);
4848bool database_system_exists (cloudsync_context *data, const char *name, const char *type);
49+ int cloudsync_dbversion_rebuild (cloudsync_context *data);
4950
5051static int stdout_backup = -1; // Backup file descriptor for stdout
5152static int dev_null_fd = -1; // File descriptor for /dev/null
@@ -2479,6 +2480,82 @@ bool do_test_stale_table_settings_dropped_meta(bool cleanup_databases) {
24792480 return result;
24802481}
24812482
2483+ // Authorizer that denies SELECT reads of sqlite_master. Used to force
2484+ // sqlite3_prepare_v2 of SQL_DBVERSION_BUILD_QUERY (which scans sqlite_master)
2485+ // to fail with SQLITE_AUTH, exercising the real error path in
2486+ // cloudsync_dbversion_rebuild introduced after 1.0.14.
2487+ static int deny_sqlite_master_authorizer(void *pUserData, int action, const char *zArg1,
2488+ const char *zArg2, const char *zDbName, const char *zTrigger) {
2489+ (void)pUserData; (void)zArg2; (void)zDbName; (void)zTrigger;
2490+ if (action == SQLITE_READ && zArg1 && strcmp(zArg1, "sqlite_master") == 0) {
2491+ return SQLITE_DENY;
2492+ }
2493+ return SQLITE_OK;
2494+ }
2495+
2496+ // Verify that cloudsync_dbversion_rebuild surfaces a real failure from
2497+ // database_select_text(SQL_DBVERSION_BUILD_QUERY, ...) instead of silently
2498+ // treating it as "no *_cloudsync meta-tables present" — which would leave
2499+ // db_version_stmt unset and cause writes to fall back to CLOUDSYNC_MIN_DB_VERSION.
2500+ bool do_test_dbversion_rebuild_error(void) {
2501+ sqlite3 *db = NULL;
2502+ cloudsync_context *ctx = NULL;
2503+ bool result = false;
2504+
2505+ int rc = sqlite3_open(":memory:", &db);
2506+ if (rc != SQLITE_OK) return false;
2507+ rc = sqlite3_cloudsync_init(db, NULL, NULL);
2508+ if (rc != SQLITE_OK) goto cleanup;
2509+
2510+ // Create a real cloudsync table so cloudsync_table_settings has a row
2511+ // (count_tables > 0 — the early-return-OK path is not taken).
2512+ rc = sqlite3_exec(db, "CREATE TABLE t (id TEXT PRIMARY KEY NOT NULL, v TEXT);", NULL, NULL, NULL);
2513+ if (rc != SQLITE_OK) goto cleanup;
2514+ rc = sqlite3_exec(db, "SELECT cloudsync_init('t');", NULL, NULL, NULL);
2515+ if (rc != SQLITE_OK) goto cleanup;
2516+
2517+ // Create a secondary context on the same db and initialize it. This
2518+ // context is independent from the one registered by sqlite3_cloudsync_init,
2519+ // so we can call cloudsync_dbversion_rebuild on it directly without
2520+ // disturbing the registered functions.
2521+ ctx = cloudsync_context_create(db);
2522+ if (!ctx) goto cleanup;
2523+ if (cloudsync_context_init(ctx) == NULL) goto cleanup;
2524+
2525+ // Install an authorizer that denies reads of sqlite_master. New prepares
2526+ // (including the one SQL_DBVERSION_BUILD_QUERY triggers inside
2527+ // database_select_text) will fail with SQLITE_AUTH. Already-prepared
2528+ // statements are unaffected, so the registered cloudsync_* functions
2529+ // still work for cleanup.
2530+ sqlite3_set_authorizer(db, deny_sqlite_master_authorizer, NULL);
2531+
2532+ // Expect a non-OK result now that the build query cannot be prepared.
2533+ // Before the review fix this would incorrectly return DBRES_OK and leave
2534+ // db_version_stmt == NULL, silently masking the failure.
2535+ int rebuild_rc = cloudsync_dbversion_rebuild(ctx);
2536+
2537+ // Remove authorizer before any further work so cleanup can run normally.
2538+ sqlite3_set_authorizer(db, NULL, NULL);
2539+
2540+ if (rebuild_rc == DBRES_OK) goto cleanup;
2541+
2542+ // The error must have been recorded on the context via cloudsync_set_dberror.
2543+ if (cloudsync_errcode(ctx) == DBRES_OK) goto cleanup;
2544+ const char *msg = cloudsync_errmsg(ctx);
2545+ if (!msg || msg[0] == 0) goto cleanup;
2546+
2547+ result = true;
2548+
2549+ cleanup:
2550+ sqlite3_set_authorizer(db, NULL, NULL);
2551+ if (ctx) cloudsync_context_free(ctx);
2552+ if (db) {
2553+ sqlite3_exec(db, "SELECT cloudsync_terminate();", NULL, NULL, NULL);
2554+ sqlite3_close(db);
2555+ }
2556+ return result;
2557+ }
2558+
24822559// Authorizer state for do_test_context_cb_error_cleanup.
24832560// Denies INSERT on a specific table after allowing a set number of INSERTs.
24842561static const char *g_deny_insert_table = NULL;
@@ -12347,6 +12424,7 @@ int main (int argc, const char * argv[]) {
1234712424 result += test_report("Schema Hash Mismatch:", do_test_schema_hash_mismatch(2, print_result, cleanup_databases));
1234812425 result += test_report("Stale Table Settings:", do_test_stale_table_settings(cleanup_databases));
1234912426 result += test_report("Stale Table Settings Dropped Meta:", do_test_stale_table_settings_dropped_meta(cleanup_databases));
12427+ result += test_report("DBVersion Rebuild Error:", do_test_dbversion_rebuild_error());
1235012428 result += test_report("Block LWW Existing Data:", do_test_block_lww_existing_data(cleanup_databases));
1235112429 result += test_report("Block Column Reload:", do_test_block_column_reload(cleanup_databases));
1235212430 result += test_report("CB Error Cleanup:", do_test_context_cb_error_cleanup());
0 commit comments