LCOV - code coverage report
Current view: top level - src/sqlite - cloudsync_sqlite.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 85.9 % 1327 1140
Test Date: 2026-10-21 16:35:02 Functions: 96.1 % 77 74

            Line data    Source code
       1              : //
       2              : //  cloudsync_sqlite.c
       3              : //  cloudsync
       4              : //
       5              : //  Created by Marco Bambini on 05/12/25.
       6              : //
       7              : 
       8              : #include "cloudsync_sqlite.h"
       9              : #include "cloudsync_changes_sqlite.h"
      10              : #include "../pk.h"
      11              : #include "../cloudsync.h"
      12              : #include "../block.h"
      13              : #include "../database.h"
      14              : #include "../dbutils.h"
      15              : #include "../sql.h"
      16              : #include <inttypes.h>
      17              : #include <stddef.h>
      18              : 
      19              : #ifndef CLOUDSYNC_OMIT_NETWORK
      20              : #include "../network/network.h"
      21              : #endif
      22              : 
      23              : #ifndef SQLITE_CORE
      24              : SQLITE_EXTENSION_INIT1
      25              : #endif
      26              : 
      27              : #ifndef UNUSED_PARAMETER
      28              : #define UNUSED_PARAMETER(X) (void)(X)
      29              : #endif
      30              : 
      31              : #ifdef _WIN32
      32              : #define APIEXPORT   __declspec(dllexport)
      33              : #else
      34              : #define APIEXPORT
      35              : #endif
      36              : 
      37              : typedef struct {
      38              :     sqlite3_context *context;
      39              :     int             index;
      40              : } cloudsync_pk_decode_context;
      41              : 
      42              : typedef struct {
      43              :     sqlite3_value   *table_name;
      44              :     sqlite3_value   **new_values;
      45              :     sqlite3_value   **old_values;
      46              :     int             count;
      47              :     int             capacity;
      48              : } cloudsync_update_payload;
      49              : 
      50              : // Reports a failed tracking write from the insert/update/delete triggers: cloudsync's
      51              : // own message when it set one (it names the table and column and carries the database
      52              : // error), else SQLite's, and the real result code, so a caller can still tell SQLITE_BUSY
      53              : // from a constraint violation. The context error is reset on entry to each trigger, so
      54              : // the message is never a stale one.
      55            4 : static void dbsync_result_trigger_error (sqlite3_context *context, cloudsync_context *data, int rc) {
      56            4 :     const char *message = cloudsync_errmsg(data);
      57            4 :     sqlite3_result_error(context, (message && message[0]) ? message : database_errmsg(data), -1);
      58            4 :     sqlite3_result_error_code(context, (rc > 0) ? rc : SQLITE_ERROR);
      59            4 : }
      60              : 
      61            6 : void dbsync_set_error (sqlite3_context *context, const char *format, ...) {
      62              :     char buffer[2048];
      63              :     
      64              :     va_list arg;
      65            6 :     va_start (arg, format);
      66            6 :     vsnprintf(buffer, sizeof(buffer), format, arg);
      67            6 :     va_end (arg);
      68              :     
      69            6 :     if (context) sqlite3_result_error(context, buffer, -1);
      70            6 : }
      71              : 
      72              : // MARK: - Public -
      73              : 
      74            8 : void dbsync_version (sqlite3_context *context, int argc, sqlite3_value **argv) {
      75              :     DEBUG_FUNCTION("cloudsync_version");
      76            8 :     UNUSED_PARAMETER(argc);
      77            8 :     UNUSED_PARAMETER(argv);
      78            8 :     sqlite3_result_text(context, CLOUDSYNC_VERSION, -1, SQLITE_STATIC);
      79            8 : }
      80              : 
      81          549 : void dbsync_siteid (sqlite3_context *context, int argc, sqlite3_value **argv) {
      82              :     DEBUG_FUNCTION("cloudsync_siteid");
      83          549 :     UNUSED_PARAMETER(argc);
      84          549 :     UNUSED_PARAMETER(argv);
      85              :     
      86          549 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
      87          549 :     sqlite3_result_blob(context, cloudsync_siteid(data), UUID_LEN, SQLITE_STATIC);
      88          549 : }
      89              : 
      90            6 : void dbsync_db_version (sqlite3_context *context, int argc, sqlite3_value **argv) {
      91              :     DEBUG_FUNCTION("cloudsync_db_version");
      92            6 :     UNUSED_PARAMETER(argc);
      93            6 :     UNUSED_PARAMETER(argv);
      94              : 
      95              :     // retrieve context
      96            6 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
      97              : 
      98            6 :     int rc = cloudsync_dbversion_check_uptodate(data);
      99            6 :     if (rc != SQLITE_OK) {
     100              :         // When cloudsync_init was never called, data_version_stmt is NULL and
     101              :         // database_errmsg() falls back to "not an error", producing the
     102              :         // confusing "Unable to retrieve db_version (not an error)". Detect the
     103              :         // uninitialized state and return an actionable message instead. The
     104              :         // extra check only runs on the error branch, so it costs nothing on
     105              :         // the sync hot path (merge operations keep going through the normal
     106              :         // path where rc == SQLITE_OK).
     107            1 :         if (!cloudsync_context_is_initialized(data)) {
     108            1 :             dbsync_set_error(context,
     109              :                 "cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
     110              :                 "to enable sync on a table before calling cloudsync_db_version().");
     111            1 :         } else {
     112            0 :             dbsync_set_error(context, "Unable to retrieve db_version (%s).", database_errmsg(data));
     113              :         }
     114            1 :         return;
     115              :     }
     116              : 
     117            5 :     sqlite3_result_int64(context, cloudsync_dbversion(data));
     118            6 : }
     119              : 
     120        32209 : void dbsync_db_version_next (sqlite3_context *context, int argc, sqlite3_value **argv) {
     121              :     DEBUG_FUNCTION("cloudsync_db_version_next");
     122              : 
     123              :     // retrieve context
     124        32209 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     125              : 
     126        32209 :     sqlite3_int64 merging_version = (argc == 1) ? database_value_int(argv[0]) : CLOUDSYNC_VALUE_NOTSET;
     127        32209 :     sqlite3_int64 value = cloudsync_dbversion_next(data, merging_version);
     128        32209 :     if (value == -1) {
     129            1 :         if (!cloudsync_context_is_initialized(data)) {
     130            1 :             dbsync_set_error(context,
     131              :                 "cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
     132              :                 "to enable sync on a table before calling cloudsync_db_version_next().");
     133            1 :         } else {
     134            0 :             dbsync_set_error(context, "Unable to retrieve next_db_version (%s).", database_errmsg(data));
     135              :         }
     136            1 :         return;
     137              :     }
     138              : 
     139        32208 :     sqlite3_result_int64(context, value);
     140        32209 : }
     141              : 
     142           55 : void dbsync_seq (sqlite3_context *context, int argc, sqlite3_value **argv) {
     143              :     DEBUG_FUNCTION("cloudsync_seq");
     144              :     
     145              :     // retrieve context
     146           55 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     147           55 :     sqlite3_result_int(context, cloudsync_bumpseq(data));
     148           55 : }
     149              : 
     150            5 : void dbsync_uuid (sqlite3_context *context, int argc, sqlite3_value **argv) {
     151              :     DEBUG_FUNCTION("cloudsync_uuid");
     152              : 
     153              :     char value[UUID_STR_MAXLEN];
     154            5 :     char *uuid = cloudsync_uuid_v7_string(value, true);
     155            5 :     sqlite3_result_text(context, uuid, -1, SQLITE_TRANSIENT);
     156            5 : }
     157              : 
     158              : // cloudsync_uuid_text(blob, [dash_format]) -> canonical UUID string
     159            4 : void dbsync_uuid_text (sqlite3_context *context, int argc, sqlite3_value **argv) {
     160              :     DEBUG_FUNCTION("cloudsync_uuid_text");
     161              : 
     162            4 :     if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { sqlite3_result_null(context); return; }
     163            4 :     if (sqlite3_value_type(argv[0]) != SQLITE_BLOB || sqlite3_value_bytes(argv[0]) != UUID_LEN) {
     164            0 :         sqlite3_result_error(context, "cloudsync_uuid_text: expected a 16-byte BLOB.", -1);
     165            0 :         return;
     166              :     }
     167            4 :     bool dash_format = (argc > 1) ? (sqlite3_value_int(argv[1]) != 0) : true;
     168              :     char value[UUID_STR_MAXLEN];
     169            4 :     cloudsync_uuid_v7_stringify((uint8_t *)sqlite3_value_blob(argv[0]), value, dash_format);
     170            4 :     sqlite3_result_text(context, value, -1, SQLITE_TRANSIENT);
     171            4 : }
     172              : 
     173              : // cloudsync_uuid_blob(text) -> 16-byte UUID blob (accepts dashed/undashed)
     174            3 : void dbsync_uuid_blob (sqlite3_context *context, int argc, sqlite3_value **argv) {
     175              :     DEBUG_FUNCTION("cloudsync_uuid_blob");
     176              : 
     177            3 :     if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { sqlite3_result_null(context); return; }
     178            3 :     const char *str = (const char *)sqlite3_value_text(argv[0]);
     179            3 :     int len = sqlite3_value_bytes(argv[0]);
     180              :     uint8_t uuid[UUID_LEN];
     181            3 :     if (!str || cloudsync_uuid_v7_parse(str, len, uuid) != 0) {
     182            0 :         sqlite3_result_error(context, "cloudsync_uuid_blob: malformed UUID string.", -1);
     183            0 :         return;
     184              :     }
     185            3 :     sqlite3_result_blob(context, uuid, UUID_LEN, SQLITE_TRANSIENT);
     186            3 : }
     187              : 
     188              : // MARK: -
     189              : 
     190           25 : void dbsync_set (sqlite3_context *context, int argc, sqlite3_value **argv) {
     191              :     DEBUG_FUNCTION("cloudsync_set");
     192              :     
     193              :     // sanity check parameters
     194           25 :     const char *key = (const char *)database_value_text(argv[0]);
     195           25 :     const char *value = (const char *)database_value_text(argv[1]);
     196              :     
     197              :     // silently fails
     198           25 :     if (key == NULL) return;
     199              :     
     200           25 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     201           25 :     dbutils_settings_set_key_value(data, key, value);
     202           25 : }
     203              : 
     204          336 : void dbsync_set_column (sqlite3_context *context, int argc, sqlite3_value **argv) {
     205              :     DEBUG_FUNCTION("cloudsync_set_column");
     206              : 
     207          336 :     const char *tbl = (const char *)database_value_text(argv[0]);
     208          336 :     const char *col = (const char *)database_value_text(argv[1]);
     209          336 :     const char *key = (const char *)database_value_text(argv[2]);
     210          336 :     const char *value = (const char *)database_value_text(argv[3]);
     211              : 
     212          336 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     213              : 
     214              :     // Handle block column setup: cloudsync_set_column('tbl', 'col', 'algo', 'block')
     215          336 :     if (key && value && strcmp(key, "algo") == 0 && strcmp(value, "block") == 0) {
     216          331 :         int rc = cloudsync_setup_block_column(data, tbl, col, NULL, true);
     217          331 :         if (rc != DBRES_OK) {
     218            0 :             sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     219            0 :         }
     220          331 :         return;
     221              :     }
     222              : 
     223              :     // Handle delimiter setting: cloudsync_set_column('tbl', 'col', 'delimiter', '\n\n')
     224            5 :     if (key && strcmp(key, "delimiter") == 0) {
     225            3 :         cloudsync_table_context *table = table_lookup(data, tbl);
     226            3 :         if (table) {
     227            3 :             int col_idx = table_col_index(table, col);
     228            3 :             if (col_idx >= 0 && table_col_algo(table, col_idx) == col_algo_block) {
     229            3 :                 table_set_col_delimiter(table, col_idx, value);
     230            3 :             }
     231            3 :         }
     232            3 :     }
     233              : 
     234            5 :     dbutils_table_settings_set_key_value(data, tbl, col, key, value);
     235          336 : }
     236              : 
     237            2 : void dbsync_set_table (sqlite3_context *context, int argc, sqlite3_value **argv) {
     238              :     DEBUG_FUNCTION("cloudsync_set_table");
     239              :     
     240            2 :     const char *tbl = (const char *)database_value_text(argv[0]);
     241            2 :     const char *key = (const char *)database_value_text(argv[1]);
     242            2 :     const char *value = (const char *)database_value_text(argv[2]);
     243              :     
     244            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     245            2 :     dbutils_table_settings_set_key_value(data, tbl, "*", key, value);
     246            2 : }
     247              : 
     248            2 : void dbsync_set_schema (sqlite3_context *context, int argc, sqlite3_value **argv) {
     249              :     DEBUG_FUNCTION("dbsync_set_schema");
     250              :     
     251            2 :     const char *schema = (const char *)database_value_text(argv[0]);
     252            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     253            2 :     cloudsync_set_schema(data, schema);
     254            2 : }
     255              : 
     256            2 : void dbsync_schema (sqlite3_context *context, int argc, sqlite3_value **argv) {
     257              :     DEBUG_FUNCTION("dbsync_schema");
     258              :     
     259            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     260            2 :     const char *schema = cloudsync_schema(data);
     261            2 :     (schema) ? sqlite3_result_text(context, schema, -1, NULL) : sqlite3_result_null(context);
     262            2 : }
     263              : 
     264            2 : void dbsync_table_schema (sqlite3_context *context, int argc, sqlite3_value **argv) {
     265              :     DEBUG_FUNCTION("dbsync_table_schema");
     266              :     
     267            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     268            2 :     const char *table_name = (const char *)database_value_text(argv[0]);
     269            2 :     const char *schema = cloudsync_table_schema(data, table_name);
     270            2 :     (schema) ? sqlite3_result_text(context, schema, -1, NULL) : sqlite3_result_null(context);
     271            2 : }
     272              : 
     273        21167 : void dbsync_is_sync (sqlite3_context *context, int argc, sqlite3_value **argv) {
     274              :     DEBUG_FUNCTION("cloudsync_is_sync");
     275              :     
     276        21167 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     277        21167 :     if (cloudsync_insync(data)) {
     278        14384 :         sqlite3_result_int(context, 1);
     279        14384 :         return;
     280              :     }
     281              :     
     282         6783 :     const char *table_name = (const char *)database_value_text(argv[0]);
     283         6783 :     cloudsync_table_context *table = table_lookup(data, table_name);
     284         6783 :     sqlite3_result_int(context, (table) ? (table_enabled(table) == 0) : 0);
     285        21167 : }
     286              : 
     287       155541 : void dbsync_col_value (sqlite3_context *context, int argc, sqlite3_value **argv) {
     288              :     // DEBUG_FUNCTION("cloudsync_col_value");
     289              :     
     290              :     // argv[0] -> table name
     291              :     // argv[1] -> column name
     292              :     // argv[2] -> encoded pk
     293              :     
     294              :     // retrieve column name
     295       155541 :     const char *col_name = (const char *)database_value_text(argv[1]);
     296       155541 :     if (!col_name) {
     297            0 :         dbsync_set_error(context, "Column name cannot be NULL");
     298            0 :         return;
     299              :     }
     300              :     
     301              :     // check for special tombstone value
     302       155541 :     if (strcmp(col_name, CLOUDSYNC_TOMBSTONE_VALUE) == 0) {
     303         4789 :         sqlite3_result_null(context);
     304         4789 :         return;
     305              :     }
     306              : 
     307              :     // lookup table
     308       150752 :     const char *table_name = (const char *)database_value_text(argv[0]);
     309       150752 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     310       150752 :     cloudsync_table_context *table = table_lookup(data, table_name);
     311       150752 :     if (!table) {
     312            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in clousdsync_colvalue.", table_name);
     313            0 :         return;
     314              :     }
     315              : 
     316              :     // Block column: if col_name contains \x1F, read from blocks table
     317       150752 :     if (block_is_block_colname(col_name) && table_has_block_cols(table)) {
     318        12235 :         dbvm_t *bvm = table_block_value_read_stmt(table);
     319        12235 :         if (!bvm) {
     320            0 :             sqlite3_result_null(context);
     321            0 :             return;
     322              :         }
     323        12235 :         int rc = databasevm_bind_blob(bvm, 1, database_value_blob(argv[2]), database_value_bytes(argv[2]));
     324        12235 :         if (rc != DBRES_OK) { databasevm_reset(bvm); sqlite3_result_error(context, database_errmsg(data), -1); return; }
     325        12235 :         rc = databasevm_bind_text(bvm, 2, col_name, -1);
     326        12235 :         if (rc != DBRES_OK) { databasevm_reset(bvm); sqlite3_result_error(context, database_errmsg(data), -1); return; }
     327              : 
     328        12235 :         rc = databasevm_step(bvm);
     329        12235 :         if (rc == SQLITE_ROW) {
     330        10474 :             sqlite3_result_value(context, database_column_value(bvm, 0));
     331        12235 :         } else if (rc == SQLITE_DONE) {
     332         1761 :             sqlite3_result_null(context);
     333         1761 :         } else {
     334            0 :             sqlite3_result_error(context, database_errmsg(data), -1);
     335              :         }
     336        12235 :         databasevm_reset(bvm);
     337        12235 :         return;
     338              :     }
     339              : 
     340              :     // extract the right col_value vm associated to the column name
     341       138517 :     sqlite3_stmt *vm = table_column_lookup(table, col_name, false, NULL);
     342       138517 :     if (!vm) {
     343            0 :         sqlite3_result_error(context, "Unable to retrieve column value precompiled statement in clousdsync_colvalue.", -1);
     344            0 :         return;
     345              :     }
     346              : 
     347              :     // bind primary key values
     348       138517 :     int rc = pk_decode_prikey((char *)database_value_blob(argv[2]), (size_t)database_value_bytes(argv[2]), pk_decode_bind_callback, (void *)vm);
     349       138517 :     if (rc < 0) goto cleanup;
     350              : 
     351              :     // execute vm
     352       138517 :     rc = databasevm_step(vm);
     353       277034 :     if (rc == SQLITE_DONE) {
     354            0 :         rc = SQLITE_OK;
     355            0 :         sqlite3_result_text(context, CLOUDSYNC_RLS_RESTRICTED_VALUE, -1, SQLITE_STATIC);
     356       138517 :     } else if (rc == SQLITE_ROW) {
     357              :         // store value result
     358       138517 :         rc = SQLITE_OK;
     359       138517 :         sqlite3_result_value(context, database_column_value(vm, 0));
     360       138517 :     }
     361              : 
     362              : cleanup:
     363       138517 :     if (rc != SQLITE_OK) {
     364            0 :         sqlite3_result_error(context, database_errmsg(data), -1);
     365            0 :     }
     366       138517 :     databasevm_reset(vm);
     367       155541 : }
     368              : 
     369        10700 : void dbsync_pk_encode (sqlite3_context *context, int argc, sqlite3_value **argv) {
     370        10700 :     size_t bsize = 0;
     371        10700 :     char *buffer = pk_encode_prikey((dbvalue_t **)argv, argc, NULL, &bsize);
     372        10700 :     if (!buffer || buffer == PRIKEY_NULL_CONSTRAINT_ERROR) {
     373            1 :         sqlite3_result_null(context);
     374            1 :         return;
     375              :     }
     376        10699 :     sqlite3_result_blob(context, (const void *)buffer, (int)bsize, SQLITE_TRANSIENT);
     377        10699 :     cloudsync_memory_free(buffer);
     378        10700 : }
     379              : 
     380           38 : int dbsync_pk_decode_set_result_callback (void *xdata, int index, int type, int64_t ival, double dval, char *pval) {
     381           38 :     cloudsync_pk_decode_context *decode_context = (cloudsync_pk_decode_context *)xdata;
     382              :     // decode_context->index is 1 based
     383              :     // index is 0 based
     384           38 :     if (decode_context->index != index+1) return SQLITE_OK;
     385              :     
     386           26 :     int rc = 0;
     387           26 :     sqlite3_context *context = decode_context->context;
     388           26 :     switch (type) {
     389              :         case SQLITE_INTEGER:
     390            3 :             sqlite3_result_int64(context, ival);
     391            3 :             break;
     392              : 
     393              :         case SQLITE_FLOAT:
     394            4 :             sqlite3_result_double(context, dval);
     395            4 :             break;
     396              : 
     397              :         case SQLITE_NULL:
     398            0 :             sqlite3_result_null(context);
     399            0 :             break;
     400              : 
     401              :         case SQLITE_TEXT:
     402           18 :             sqlite3_result_text(context, pval, (int)ival, SQLITE_TRANSIENT);
     403           18 :             break;
     404              : 
     405              :         case SQLITE_BLOB:
     406            1 :             sqlite3_result_blob(context, pval, (int)ival, SQLITE_TRANSIENT);
     407            1 :             break;
     408              :     }
     409              :     
     410           26 :     return rc;
     411           38 : }
     412              : 
     413              : 
     414           26 : void dbsync_pk_decode (sqlite3_context *context, int argc, sqlite3_value **argv) {
     415           26 :     const char *pk = (const char *)database_value_blob(argv[0]);
     416           26 :     int pk_len = database_value_bytes(argv[0]);
     417           26 :     int i = (int)database_value_int(argv[1]);
     418              :     
     419           26 :     cloudsync_pk_decode_context xdata = {.context = context, .index = i};
     420           26 :     pk_decode_prikey((char *)pk, (size_t)pk_len, dbsync_pk_decode_set_result_callback, &xdata);
     421           26 : }
     422              : 
     423              : // MARK: -
     424              : 
     425         6013 : void dbsync_insert (sqlite3_context *context, int argc, sqlite3_value **argv) {
     426              :     DEBUG_FUNCTION("cloudsync_insert %s", database_value_text(argv[0]));
     427              :     // debug_values(argc-1, &argv[1]);
     428              :     
     429              :     // argv[0] is table name
     430              :     // argv[1]..[N] is primary key(s)
     431              :     
     432              :     // table_cloudsync
     433              :     // pk               -> encode(argc-1, &argv[1])
     434              :     // col_name         -> name
     435              :     // col_version      -> 0/1 +1
     436              :     // db_version       -> check
     437              :     // site_id          0
     438              :     // seq              -> sqlite_master
     439              :     
     440              :     // retrieve context
     441         6013 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     442         6013 :     cloudsync_reset_error(data);
     443              :     
     444              :     // lookup table
     445         6013 :     const char *table_name = (const char *)database_value_text(argv[0]);
     446         6013 :     cloudsync_table_context *table = table_lookup(data, table_name);
     447         6013 :     if (!table) {
     448            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_insert.", table_name);
     449            0 :         return;
     450              :     }
     451              :     
     452              :     // encode the primary key values into a buffer
     453              :     char buffer[1024];
     454         6013 :     size_t pklen = sizeof(buffer);
     455         6013 :     char *pk = pk_encode_prikey((dbvalue_t **)&argv[1], table_count_pks(table), buffer, &pklen);
     456         6013 :     if (!pk) {
     457            0 :         sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
     458            0 :         return;
     459              :     }
     460         6013 :     if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
     461            1 :         dbsync_set_error(context, "Insert aborted because primary key in table %s contains NULL values.", table_name);
     462            1 :         return;
     463              :     }
     464              :     
     465              :     // compute the next database version for tracking changes
     466         6012 :     int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
     467              :     
     468              :     // check if a row with the same primary key already exists
     469              :     // if so, this means the row might have been previously deleted (sentinel)
     470         6012 :     bool pk_exists = table_pk_exists(table, pk, pklen);
     471         6012 :     int rc = SQLITE_OK;
     472              :     
     473         6012 :     if (table_count_cols(table) == 0) {
     474              :         // if there are no columns other than primary keys, insert a sentinel record
     475           97 :         rc = local_mark_insert_sentinel_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
     476           97 :         if (rc != SQLITE_OK) goto cleanup;
     477         6012 :     } else if (pk_exists){
     478              :         // if a row with the same primary key already exists, update the sentinel record
     479          290 :         rc = local_update_sentinel(table, pk, pklen, db_version, cloudsync_bumpseq(data));
     480          290 :         if (rc != SQLITE_OK) goto cleanup;
     481          290 :     }
     482              :     
     483              :     // process each non-primary key column for insert or update
     484        19923 :     for (int i=0; i<table_count_cols(table); ++i) {
     485        13914 :         if (table_col_algo(table, i) == col_algo_block) {
     486          702 :             rc = local_block_insert(data, table, pk, pklen, i, db_version);
     487          702 :             if (rc != SQLITE_OK) goto cleanup;
     488          699 :         } else {
     489              :             // Regular column: mark as inserted or updated in the metadata
     490        13212 :             rc = local_mark_insert_or_update_meta(table, pk, pklen, table_colname(table, i), db_version, cloudsync_bumpseq(data));
     491        13212 :             if (rc != SQLITE_OK) goto cleanup;
     492              :         }
     493        19920 :     }
     494              : 
     495              : cleanup:
     496         6012 :     if (rc != SQLITE_OK) dbsync_result_trigger_error(context, data, rc);
     497              :     // free memory if the primary key was dynamically allocated
     498         6012 :     if (pk != buffer) cloudsync_memory_free(pk);
     499         6013 : }
     500              : 
     501          202 : void dbsync_delete (sqlite3_context *context, int argc, sqlite3_value **argv) {
     502              :     DEBUG_FUNCTION("cloudsync_delete %s", database_value_text(argv[0]));
     503              :     // debug_values(argc-1, &argv[1]);
     504              :     
     505              :     // retrieve context
     506          202 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     507          202 :     cloudsync_reset_error(data);
     508              :     
     509              :     // lookup table
     510          202 :     const char *table_name = (const char *)database_value_text(argv[0]);
     511          202 :     cloudsync_table_context *table = table_lookup(data, table_name);
     512          202 :     if (!table) {
     513            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_delete.", table_name);
     514            0 :         return;
     515              :     }
     516              :     
     517              :     // compute the next database version for tracking changes
     518          202 :     int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
     519          202 :     int rc = SQLITE_OK;
     520              :     
     521              :     // encode the primary key values into a buffer
     522              :     char buffer[1024];
     523          202 :     size_t pklen = sizeof(buffer);
     524          202 :     char *pk = pk_encode_prikey((dbvalue_t **)&argv[1], table_count_pks(table), buffer, &pklen);
     525          202 :     if (!pk) {
     526            0 :         sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
     527            0 :         return;
     528              :     }
     529              :     
     530          202 :     if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
     531            0 :         dbsync_set_error(context, "Delete aborted because primary key in table %s contains NULL values.", table_name);
     532            0 :         return;
     533              :     }
     534              :     
     535              :     // mark the row as deleted by inserting a delete sentinel into the metadata
     536          202 :     rc = local_mark_delete_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
     537          202 :     if (rc != SQLITE_OK) goto cleanup;
     538              :     
     539              :     // remove any metadata related to the old rows associated with this primary key
     540          202 :     rc = local_drop_meta(table, pk, pklen);
     541          202 :     if (rc != SQLITE_OK) goto cleanup;
     542              :     
     543              : cleanup:
     544          202 :     if (rc != SQLITE_OK) dbsync_result_trigger_error(context, data, rc);
     545              :     // free memory if the primary key was dynamically allocated
     546          202 :     if (pk != buffer) cloudsync_memory_free(pk);
     547          202 : }
     548              : 
     549              : // MARK: -
     550              : 
     551          614 : void dbsync_update_payload_free (cloudsync_update_payload *payload) {
     552         3317 :     for (int i=0; i<payload->count; i++) {
     553         2703 :         database_value_free(payload->new_values[i]);
     554         2703 :         database_value_free(payload->old_values[i]);
     555         2703 :     }
     556          614 :     cloudsync_memory_free(payload->new_values);
     557          614 :     cloudsync_memory_free(payload->old_values);
     558          614 :     database_value_free(payload->table_name);
     559          614 :     payload->new_values = NULL;
     560          614 :     payload->old_values = NULL;
     561          614 :     payload->table_name = NULL;
     562          614 :     payload->count = 0;
     563          614 :     payload->capacity = 0;
     564          614 : }
     565              : 
     566         2703 : int dbsync_update_payload_append (cloudsync_update_payload *payload, sqlite3_value *v1, sqlite3_value *v2, sqlite3_value *v3) {
     567         2703 :     if (payload->count >= payload->capacity) {
     568          617 :         int newcap = payload->capacity ? payload->capacity * 2 : 128;
     569              : 
     570          617 :         sqlite3_value **new_values_2 = (sqlite3_value **)cloudsync_memory_realloc(payload->new_values, newcap * sizeof(*new_values_2));
     571          617 :         if (!new_values_2) return SQLITE_NOMEM;
     572              : 
     573          617 :         sqlite3_value **old_values_2 = (sqlite3_value **)cloudsync_memory_realloc(payload->old_values, newcap * sizeof(*old_values_2));
     574          617 :         if (!old_values_2) {
     575              :             // new_values_2 succeeded but old_values failed; keep new_values_2 pointer
     576              :             // (it's still valid, just larger) but don't update capacity
     577            0 :             payload->new_values = new_values_2;
     578            0 :             return SQLITE_NOMEM;
     579              :         }
     580              : 
     581          617 :         payload->new_values = new_values_2;
     582          617 :         payload->old_values = old_values_2;
     583          617 :         payload->capacity = newcap;
     584          617 :     }
     585              : 
     586         2703 :     int index = payload->count;
     587         2703 :     if (payload->table_name == NULL) payload->table_name = database_value_dup(v1);
     588         2089 :     else if (dbutils_value_compare(payload->table_name, v1) != 0) return SQLITE_NOMEM;
     589              : 
     590         2703 :     payload->new_values[index] = database_value_dup(v2);
     591         2703 :     payload->old_values[index] = database_value_dup(v3);
     592              : 
     593              :     // sanity check memory allocations before committing count
     594         2703 :     bool v1_can_be_null = (database_value_type(v1) == SQLITE_NULL);
     595         2703 :     bool v2_can_be_null = (database_value_type(v2) == SQLITE_NULL);
     596         2703 :     bool v3_can_be_null = (database_value_type(v3) == SQLITE_NULL);
     597              : 
     598         2703 :     bool oom = false;
     599         2703 :     if ((payload->table_name == NULL) && (!v1_can_be_null)) oom = true;
     600         2703 :     if ((payload->new_values[index] == NULL) && (!v2_can_be_null)) oom = true;
     601         2703 :     if ((payload->old_values[index] == NULL) && (!v3_can_be_null)) oom = true;
     602              : 
     603         2703 :     if (oom) {
     604              :         // clean up partial allocations at this index to prevent leaks
     605            0 :         if (payload->new_values[index]) { database_value_free(payload->new_values[index]); payload->new_values[index] = NULL; }
     606            0 :         if (payload->old_values[index]) { database_value_free(payload->old_values[index]); payload->old_values[index] = NULL; }
     607            0 :         return SQLITE_NOMEM;
     608              :     }
     609              : 
     610         2703 :     payload->count++;
     611         2703 :     return SQLITE_OK;
     612         2703 : }
     613              : 
     614         2703 : void dbsync_update_step (sqlite3_context *context, int argc, sqlite3_value **argv) {
     615              :     // argv[0] => table_name
     616              :     // argv[1] => new_column_value
     617              :     // argv[2] => old_column_value
     618              :     
     619              :     // allocate/get the update payload
     620         2703 :     cloudsync_update_payload *payload = (cloudsync_update_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_update_payload));
     621         2703 :     if (!payload) {sqlite3_result_error_nomem(context); return;}
     622              :     
     623         2703 :     if (dbsync_update_payload_append(payload, argv[0], argv[1], argv[2]) != SQLITE_OK) {
     624            0 :         sqlite3_result_error_nomem(context);
     625            0 :     }
     626         2703 : }
     627              : 
     628          614 : void dbsync_update_final (sqlite3_context *context) {
     629          614 :     cloudsync_update_payload *payload = (cloudsync_update_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_update_payload));
     630          614 :     if (!payload || payload->count == 0) return;
     631              :     
     632              :     // retrieve context
     633          614 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     634          614 :     cloudsync_reset_error(data);
     635              :     
     636              :     // lookup table
     637          614 :     const char *table_name = (const char *)database_value_text(payload->table_name);
     638          614 :     cloudsync_table_context *table = table_lookup(data, table_name);
     639          614 :     if (!table) {
     640            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_update.", table_name);
     641            0 :         dbsync_update_payload_free(payload);
     642            0 :         return;
     643              :     }
     644              : 
     645              :     // compute the next database version for tracking changes
     646          614 :     int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
     647          614 :     int rc = SQLITE_OK;
     648              :     
     649              :     // Check if the primary key(s) have changed
     650          614 :     bool prikey_changed = false;
     651         1510 :     for (int i=0; i<table_count_pks(table); ++i) {
     652          924 :         if (dbutils_value_compare(payload->old_values[i], payload->new_values[i]) != 0) {
     653           28 :             prikey_changed = true;
     654           28 :             break;
     655              :         }
     656          896 :     }
     657              : 
     658              :     // encode the NEW primary key values into a buffer (used later for indexing)
     659              :     char buffer[1024];
     660              :     char buffer2[1024];
     661          614 :     size_t pklen = sizeof(buffer);
     662          614 :     size_t oldpklen = sizeof(buffer2);
     663          614 :     char *oldpk = NULL;
     664              :     
     665          614 :     char *pk = pk_encode_prikey((dbvalue_t **)payload->new_values, table_count_pks(table), buffer, &pklen);
     666          614 :     if (!pk) {
     667            0 :         sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
     668            0 :         dbsync_update_payload_free(payload);
     669            0 :         return;
     670              :     }
     671          614 :     if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
     672            0 :         dbsync_set_error(context, "Update aborted because primary key in table %s contains NULL values.", table_name);
     673            0 :         dbsync_update_payload_free(payload);
     674            0 :         return;
     675              :     }
     676              :     
     677          614 :     if (prikey_changed) {
     678              :         // if the primary key has changed, we need to handle the row differently:
     679              :         // 1. mark the old row (OLD primary key) as deleted
     680              :         // 2. create a new row (NEW primary key)
     681              :         
     682              :         // encode the OLD primary key into a buffer
     683           28 :         oldpk = pk_encode_prikey((dbvalue_t **)payload->old_values, table_count_pks(table), buffer2, &oldpklen);
     684           28 :         if (!oldpk) {
     685              :             // no check here about PRIKEY_NULL_CONSTRAINT_ERROR because by design oldpk cannot contain NULL values
     686            0 :             if (pk != buffer) cloudsync_memory_free(pk);
     687            0 :             sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
     688            0 :             dbsync_update_payload_free(payload);
     689            0 :             return;
     690              :         }
     691              :         
     692              :         // mark the rows with the old primary key as deleted in the metadata (old row handling)
     693           28 :         rc = local_mark_delete_meta(table, oldpk, oldpklen, db_version, cloudsync_bumpseq(data));
     694           28 :         if (rc != SQLITE_OK) goto cleanup;
     695              :         
     696              :         // move non-sentinel metadata entries from OLD primary key to NEW primary key
     697              :         // handles the case where some metadata is retained across primary key change
     698              :         // see https://github.com/sqliteai/sqlite-sync/blob/main/docs/PriKey.md for more details
     699           28 :         rc = local_update_move_meta(table, pk, pklen, oldpk, oldpklen, db_version);
     700           28 :         if (rc != SQLITE_OK) goto cleanup;
     701              :         
     702              :         // mark a new sentinel row with the new primary key in the metadata
     703           28 :         rc = local_mark_insert_sentinel_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
     704           28 :         if (rc != SQLITE_OK) goto cleanup;
     705              :         
     706              :         // free memory if the OLD primary key was dynamically allocated
     707           28 :         if (oldpk != buffer2) cloudsync_memory_free(oldpk);
     708           28 :         oldpk = NULL;
     709           28 :     }
     710              :     
     711              :     // compare NEW and OLD values (excluding primary keys) to handle column updates
     712         2365 :     for (int i=0; i<table_count_cols(table); i++) {
     713         1752 :         int col_index = table_count_pks(table) + i;  // Regular columns start after primary keys
     714              : 
     715         1752 :         if (dbutils_value_compare(payload->old_values[col_index], payload->new_values[col_index]) != 0) {
     716          938 :             if (table_col_algo(table, i) == col_algo_block) {
     717          510 :                 rc = local_block_update(data, table, pk, pklen, i,
     718          255 :                     (const char *)database_value_text(payload->new_values[col_index]), db_version, false);
     719          255 :                 if (rc != SQLITE_OK) goto cleanup;
     720          254 :             } else {
     721              :                 // Regular column: mark as updated in the metadata (columns are in cid order)
     722          683 :                 rc = local_mark_insert_or_update_meta(table, pk, pklen, table_colname(table, i), db_version, cloudsync_bumpseq(data));
     723          683 :                 if (rc != SQLITE_OK) goto cleanup;
     724              :             }
     725          937 :         }
     726         2364 :     }
     727              :     
     728              : cleanup:
     729          614 :     if (rc != SQLITE_OK) dbsync_result_trigger_error(context, data, rc);
     730          614 :     if (pk != buffer) cloudsync_memory_free(pk);
     731          614 :     if (oldpk && (oldpk != buffer2)) cloudsync_memory_free(oldpk);
     732              :     
     733          614 :     dbsync_update_payload_free(payload);
     734          614 : }
     735              : 
     736              : // MARK: -
     737              : 
     738            4 : void dbsync_cleanup (sqlite3_context *context, int argc, sqlite3_value **argv) {
     739              :     DEBUG_FUNCTION("cloudsync_cleanup");
     740              :     
     741            4 :     const char *table = (const char *)database_value_text(argv[0]);
     742            4 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     743              :     
     744            4 :     int rc = cloudsync_cleanup(data, table);
     745            4 :     if (rc != DBRES_OK) {
     746            0 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     747            0 :         sqlite3_result_error_code(context, rc);
     748            0 :     }
     749            4 : }
     750              : 
     751           10 : void dbsync_enable_disable (sqlite3_context *context, const char *table_name, bool value) {
     752              :     DEBUG_FUNCTION("cloudsync_enable_disable");
     753              :     
     754           10 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     755           10 :     cloudsync_table_context *table = table_lookup(data, table_name);
     756           10 :     if (!table) return;
     757              :     
     758           10 :     table_set_enabled(table, value);
     759           10 : }
     760              : 
     761            5 : void dbsync_enable (sqlite3_context *context, int argc, sqlite3_value **argv) {
     762              :     DEBUG_FUNCTION("cloudsync_enable");
     763              :     
     764            5 :     const char *table = (const char *)database_value_text(argv[0]);
     765            5 :     dbsync_enable_disable(context, table, true);
     766            5 : }
     767              : 
     768            5 : void dbsync_disable (sqlite3_context *context, int argc, sqlite3_value **argv) {
     769              :     DEBUG_FUNCTION("cloudsync_disable");
     770              :     
     771            5 :     const char *table = (const char *)database_value_text(argv[0]);
     772            5 :     dbsync_enable_disable(context, table, false);
     773            5 : }
     774              : 
     775         2858 : void dbsync_is_enabled (sqlite3_context *context, int argc, sqlite3_value **argv) {
     776              :     DEBUG_FUNCTION("cloudsync_is_enabled");
     777              :     
     778         2858 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     779         2858 :     const char *table_name = (const char *)database_value_text(argv[0]);
     780         2858 :     cloudsync_table_context *table = table_lookup(data, table_name);
     781              :     
     782         2858 :     int result = (table && table_enabled(table)) ? 1 : 0;
     783         2858 :     sqlite3_result_int(context, result);
     784         2858 : }
     785              : 
     786          772 : void dbsync_terminate (sqlite3_context *context, int argc, sqlite3_value **argv) {
     787              :     DEBUG_FUNCTION("cloudsync_terminate");
     788              :     
     789          772 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     790          772 :     int rc = cloudsync_terminate(data);
     791          772 :     sqlite3_result_int(context, rc);
     792          772 : }
     793              : 
     794              : // MARK: -
     795              : 
     796          801 : void dbsync_init (sqlite3_context *context, const char *table, const char *algo, CLOUDSYNC_INIT_FLAG init_flags) {
     797          801 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     798              :     
     799          801 :     int rc = database_begin_savepoint(data, "cloudsync_init");
     800          801 :     if (rc != SQLITE_OK) {
     801            0 :         dbsync_set_error(context, "Unable to create cloudsync_init savepoint. %s", database_errmsg(data));
     802            0 :         sqlite3_result_error_code(context, rc);
     803            0 :         return;
     804              :     }
     805              :     
     806          801 :     rc = cloudsync_init_table(data, table, algo, init_flags);
     807          801 :     if (rc == SQLITE_OK) {
     808          796 :         rc = database_commit_savepoint(data, "cloudsync_init");
     809          796 :         if (rc != SQLITE_OK) {
     810            0 :             dbsync_set_error(context, "Unable to release cloudsync_init savepoint. %s", database_errmsg(data));
     811            0 :             sqlite3_result_error_code(context, rc);
     812            0 :         }
     813          796 :     } else {
     814              :         // in case of error, rollback transaction
     815            5 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     816            5 :         sqlite3_result_error_code(context, rc);
     817            5 :         database_rollback_savepoint(data, "cloudsync_init");
     818            5 :         return;
     819              :     }
     820              :     
     821          796 :     cloudsync_update_schema_hash(data);
     822              :     
     823              :     // returns site_id as TEXT
     824              :     char buffer[UUID_STR_MAXLEN];
     825          796 :     cloudsync_uuid_v7_stringify(cloudsync_siteid(data), buffer, false);
     826          796 :     sqlite3_result_text(context, buffer, -1, SQLITE_TRANSIENT);
     827          801 : }
     828              : 
     829           46 : void dbsync_init3 (sqlite3_context *context, int argc, sqlite3_value **argv) {
     830              :     DEBUG_FUNCTION("cloudsync_init2");
     831              :     
     832           46 :     const char *table = (const char *)database_value_text(argv[0]);
     833           46 :     const char *algo = (const char *)database_value_text(argv[1]);
     834           46 :     int init_flags = database_value_int(argv[2]);
     835           46 :     dbsync_init(context, table, algo, init_flags);
     836           46 : }
     837              : 
     838           86 : void dbsync_init2 (sqlite3_context *context, int argc, sqlite3_value **argv) {
     839              :     DEBUG_FUNCTION("cloudsync_init2");
     840              :     
     841           86 :     const char *table = (const char *)database_value_text(argv[0]);
     842           86 :     const char *algo = (const char *)database_value_text(argv[1]);
     843           86 :     dbsync_init(context, table, algo, CLOUDSYNC_INIT_FLAG_NONE);
     844           86 : }
     845              : 
     846          669 : void dbsync_init1 (sqlite3_context *context, int argc, sqlite3_value **argv) {
     847              :     DEBUG_FUNCTION("cloudsync_init1");
     848              :     
     849          669 :     const char *table = (const char *)database_value_text(argv[0]);
     850          669 :     dbsync_init(context, table, NULL, CLOUDSYNC_INIT_FLAG_NONE);
     851          669 : }
     852              : 
     853              : // MARK: -
     854              : 
     855           24 : void dbsync_begin_alter (sqlite3_context *context, int argc, sqlite3_value **argv) {
     856              :     DEBUG_FUNCTION("dbsync_begin_alter");
     857              : 
     858           24 :     const char *table_name = (const char *)database_value_text(argv[0]);
     859           24 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     860              : 
     861           24 :     int rc = database_begin_savepoint(data, "cloudsync_alter");
     862           24 :     if (rc != DBRES_OK) {
     863            0 :         sqlite3_result_error(context, "Unable to create cloudsync_alter savepoint", -1);
     864            0 :         sqlite3_result_error_code(context, rc);
     865            0 :         return;
     866              :     }
     867              : 
     868           24 :     rc = cloudsync_begin_alter(data, table_name);
     869           24 :     if (rc != DBRES_OK) {
     870            1 :         database_rollback_savepoint(data, "cloudsync_alter");
     871            1 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     872            1 :         sqlite3_result_error_code(context, rc);
     873            1 :     }
     874           24 : }
     875              : 
     876           24 : void dbsync_commit_alter (sqlite3_context *context, int argc, sqlite3_value **argv) {
     877              :     DEBUG_FUNCTION("cloudsync_commit_alter");
     878              :     
     879              :     //retrieve table argument
     880           24 :     const char *table_name = (const char *)database_value_text(argv[0]);
     881              :     
     882              :     // retrieve context
     883           24 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     884              :     
     885           24 :     int rc = cloudsync_commit_alter(data, table_name);
     886           24 :     if (rc != DBRES_OK) {
     887            1 :         database_rollback_savepoint(data, "cloudsync_alter");
     888            1 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     889            1 :         sqlite3_result_error_code(context, rc);
     890            1 :         return;
     891              :     }
     892              : 
     893           23 :     rc = database_commit_savepoint(data, "cloudsync_alter");
     894           23 :     if (rc != DBRES_OK) {
     895            0 :         sqlite3_result_error(context, database_errmsg(data), -1);
     896            0 :         sqlite3_result_error_code(context, rc);
     897            0 :         return;
     898              :     }
     899              : 
     900           23 :     cloudsync_update_schema_hash(data);
     901           24 : }
     902              : 
     903              : // MARK: - Payload -
     904              : 
     905        56708 : void dbsync_payload_encode_step (sqlite3_context *context, int argc, sqlite3_value **argv) {
     906              :     // allocate/get the session context
     907        56708 :     cloudsync_payload_context *payload = (cloudsync_payload_context *)sqlite3_aggregate_context(context, (int)cloudsync_payload_context_size(NULL));
     908        56708 :     if (!payload) {
     909            0 :         sqlite3_result_error(context, "Not enough memory to allocate payload session context", -1);
     910            0 :         sqlite3_result_error_code(context, SQLITE_NOMEM);
     911            0 :         return;
     912              :     }
     913              :     
     914              :     // retrieve context
     915        56708 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     916              :     
     917        56708 :     int rc = cloudsync_payload_encode_step(payload, data, argc, (dbvalue_t **)argv);
     918        56708 :     if (rc != SQLITE_OK) {
     919            0 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     920            0 :         sqlite3_result_error_code(context, rc);
     921            0 :     }
     922        56708 : }
     923              : 
     924         1810 : void dbsync_payload_encode_final (sqlite3_context *context) {
     925              :     // get the session context
     926         1810 :     cloudsync_payload_context *payload = (cloudsync_payload_context *)sqlite3_aggregate_context(context, (int)cloudsync_payload_context_size(NULL));
     927         1810 :     if (!payload) {
     928            0 :         sqlite3_result_error(context, "Unable to extract payload session context", -1);
     929            0 :         sqlite3_result_error_code(context, SQLITE_NOMEM);
     930            0 :         return;
     931              :     }
     932              :     
     933              :     // retrieve context
     934         1810 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     935              :     
     936         1810 :     int rc = cloudsync_payload_encode_final(payload, data);
     937         1810 :     if (rc != SQLITE_OK) {
     938            0 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     939            0 :         sqlite3_result_error_code(context, rc);
     940            0 :         return;
     941              :     }
     942              :     
     943              :     // result is OK so get BLOB and returns it
     944         1810 :     int64_t blob_size = 0;
     945         1810 :     char *blob = cloudsync_payload_blob (payload, &blob_size, NULL);
     946         1810 :     if (!blob) {
     947            8 :         sqlite3_result_null(context);
     948            8 :     } else {
     949         1802 :         sqlite3_result_blob64(context, blob, blob_size, SQLITE_TRANSIENT);
     950         1802 :         cloudsync_memory_free(blob);
     951              :     }
     952              :     
     953              :     // from: https://sqlite.org/c3ref/aggregate_context.html
     954              :     // SQLite automatically frees the memory allocated by sqlite3_aggregate_context() when the aggregate query concludes.
     955         1810 : }
     956              : 
     957         1884 : void dbsync_payload_decode (sqlite3_context *context, int argc, sqlite3_value **argv) {
     958              :     DEBUG_FUNCTION("dbsync_payload_decode");
     959              :     //debug_values(argc, argv);
     960              :     
     961              :     // sanity check payload type
     962         1884 :     if (database_value_type(argv[0]) != SQLITE_BLOB) {
     963            0 :         sqlite3_result_error(context, "Error on cloudsync_payload_decode: value must be a BLOB.", -1);
     964            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
     965            0 :         return;
     966              :     }
     967              :     
     968              :     // sanity check payload size
     969         1884 :     int blen = database_value_bytes(argv[0]);
     970         1884 :     size_t header_size = 0;
     971         1884 :     cloudsync_payload_context_size(&header_size);
     972         1884 :     if (blen < (int)header_size) {
     973            3 :         sqlite3_result_error(context, "Error on cloudsync_payload_decode: invalid input size.", -1);
     974            3 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
     975            3 :         return;
     976              :     }
     977              :     
     978              :     // obtain payload
     979         1881 :     const char *payload = (const char *)database_value_blob(argv[0]);
     980              : 
     981              :     // apply changes
     982              :     // The public SQL function applies a single complete payload: advance the
     983              :     // receive cursor to its last applied (db_version, seq) (legacy behavior, safe
     984              :     // for a payload that ends on a db_version boundary). The chunked-download
     985              :     // receive path gates cursor advancement on stream completion via the C-level
     986              :     // checkpoint argument instead (see cloudsync_payload_apply in cloudsync.h).
     987         1881 :     int nrows = 0;
     988         1881 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     989         1881 :     int rc = cloudsync_payload_apply(data, payload, blen, &nrows, CLOUDSYNC_CHECKPOINT_LAST_APPLIED, 0);
     990         1881 :     if (rc != SQLITE_OK) {
     991          272 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     992          272 :         sqlite3_result_error_code(context, rc);
     993          272 :         return;
     994              :     }
     995              :     
     996              :     // returns number of applied rows
     997         1609 :     sqlite3_result_int(context, nrows);
     998         1884 : }
     999              : 
    1000              : typedef struct {
    1001              :     sqlite3_vtab       base;
    1002              :     sqlite3           *db;
    1003              :     cloudsync_context *data;
    1004              : } cloudsync_payload_chunks_vtab;
    1005              : 
    1006              : typedef struct {
    1007              :     sqlite3_vtab_cursor base;
    1008              :     cloudsync_payload_chunks_vtab *vtab;
    1009              :     sqlite3_stmt *src;
    1010              :     bool eof;
    1011              :     bool has_row;
    1012              :     int chunk_index;
    1013              :     char *payload;
    1014              :     int64_t payload_size;
    1015              :     int64_t rows;
    1016              :     int64_t dbv_min;
    1017              :     int64_t dbv_max;
    1018              :     int64_t watermark;
    1019              :     bool frag_active;
    1020              :     int frag_part;
    1021              :     int frag_count;
    1022              :     int frag_target;
    1023              :     int64_t frag_offset;
    1024              :     int64_t frag_total;
    1025              :     uint64_t frag_checksum;
    1026              :     char value_header[16];
    1027              :     int value_header_len;
    1028              :     const char *value_data;
    1029              :     int64_t value_data_len;
    1030              :     // Positional-cursor outputs: the resume point AFTER the chunk currently held.
    1031              :     // These live in the per-scan reset region (after eof) so xFilter's bulk memset
    1032              :     // clears them. next_* is the (db_version, seq, frag_offset) a follow-up call
    1033              :     // passes back as resume_* to continue exactly where this chunk stopped.
    1034              :     int64_t next_dbv;
    1035              :     int64_t next_seq;
    1036              :     int64_t next_frag_offset;
    1037              :     bool is_final;
    1038              :     // Window cap (max_window_bytes): payload bytes emitted so far by this scan, and
    1039              :     // whether the scan ended because the budget ran out rather than because the
    1040              :     // window was drained. window_bytes counts across chunks, not within one.
    1041              :     int64_t max_window_bytes;
    1042              :     int64_t window_bytes;
    1043              :     bool window_capped;
    1044              : } cloudsync_payload_chunks_cursor;
    1045              : 
    1046           19 : static int payload_chunks_connect(sqlite3 *db, void *aux, int argc, const char *const *argv, sqlite3_vtab **vtab, char **err) {
    1047           19 :     UNUSED_PARAMETER(argc); UNUSED_PARAMETER(argv); UNUSED_PARAMETER(err);
    1048           19 :     int rc = sqlite3_declare_vtab(db,
    1049              :         "CREATE TABLE x(payload BLOB, chunk_index INTEGER, payload_size INTEGER, rows INTEGER, "
    1050              :         "db_version_min INTEGER, db_version_max INTEGER, watermark_db_version INTEGER, "
    1051              :         "since_db_version HIDDEN, site_id HIDDEN, until_db_version HIDDEN, exclude_filter_site_id HIDDEN, "
    1052              :         // Positional-cursor outputs (cols 11..14): the resume point after the
    1053              :         // emitted chunk, plus a final-chunk flag. A stateless /check passes these
    1054              :         // back as the resume_* inputs (cols 15..17) to continue the drain without
    1055              :         // a spool table — O(1) seek per chunk instead of replaying from since.
    1056              :         "next_db_version INTEGER, next_seq INTEGER, next_frag_offset INTEGER, is_final INTEGER, "
    1057              :         // window_capped and window_bytes (cols 15..16) are outputs, so they do not shift
    1058              :         // the hidden columns a table-valued call binds positionally. The two budget
    1059              :         // inputs are declared last for the same reason: they become arguments 8 and 9,
    1060              :         // leaving arguments 1..7 as they were.
    1061              :         //
    1062              :         // window_bytes/resume_window_bytes carry the budget spent across calls exactly
    1063              :         // as next_*/resume_* carry the stream position. Without it a caller that fetches
    1064              :         // one chunk per call -- which is how a stateless /check pages -- restarts the
    1065              :         // count every time and never reaches the budget at all.
    1066              :         "window_capped INTEGER, window_bytes INTEGER, "
    1067              :         "resume_db_version HIDDEN, resume_seq HIDDEN, resume_frag_offset HIDDEN, "
    1068              :         "max_window_bytes HIDDEN, resume_window_bytes HIDDEN)");
    1069           19 :     if (rc != SQLITE_OK) return rc;
    1070           19 :     cloudsync_payload_chunks_vtab *p = sqlite3_malloc64(sizeof(*p));
    1071           19 :     if (!p) return SQLITE_NOMEM;
    1072           19 :     memset(p, 0, sizeof(*p));
    1073           19 :     p->db = db;
    1074           19 :     p->data = (cloudsync_context *)aux;
    1075           19 :     *vtab = (sqlite3_vtab *)p;
    1076           19 :     return SQLITE_OK;
    1077           19 : }
    1078              : 
    1079           19 : static int payload_chunks_disconnect(sqlite3_vtab *vtab) {
    1080           19 :     sqlite3_free(vtab);
    1081           19 :     return SQLITE_OK;
    1082              : }
    1083              : 
    1084          187 : static int payload_chunks_open(sqlite3_vtab *vtab, sqlite3_vtab_cursor **cursor) {
    1085          187 :     cloudsync_payload_chunks_cursor *c = cloudsync_memory_zeroalloc(sizeof(*c));
    1086          187 :     if (!c) return SQLITE_NOMEM;
    1087          187 :     c->vtab = (cloudsync_payload_chunks_vtab *)vtab;
    1088          187 :     *cursor = (sqlite3_vtab_cursor *)c;
    1089          187 :     return SQLITE_OK;
    1090          187 : }
    1091              : 
    1092          187 : static int payload_chunks_close(sqlite3_vtab_cursor *cursor) {
    1093          187 :     cloudsync_payload_chunks_cursor *c = (cloudsync_payload_chunks_cursor *)cursor;
    1094          187 :     if (c->src) sqlite3_finalize(c->src);
    1095          187 :     if (c->payload) cloudsync_memory_free(c->payload);
    1096          187 :     cloudsync_memory_free(c);
    1097          187 :     return SQLITE_OK;
    1098              : }
    1099              : 
    1100          178 : static int payload_chunks_best_index(sqlite3_vtab *vtab, sqlite3_index_info *idxinfo) {
    1101          178 :     UNUSED_PARAMETER(vtab);
    1102              :     // Assign argvIndex in a canonical hidden-column order so xFilter can read argv
    1103              :     // in a fixed order regardless of how SQLite presents constraints. idxNum bit k
    1104              :     // is set when handled_cols[k] is bound; xFilter reads argv in this same order.
    1105              :     //   bit0=since_db_version(7) bit1=site_id(8) bit2=until_db_version(9)
    1106              :     //   bit3=exclude_filter_site_id(10) bit4=resume_db_version(17)
    1107              :     //   bit5=resume_seq(18) bit6=resume_frag_offset(19) bit7=max_window_bytes(20)
    1108              :     //   bit8=resume_window_bytes(21)
    1109              :     static const int handled_cols[] = {7, 8, 9, 10, 17, 18, 19, 20, 21};
    1110          178 :     int argv_index = 1;
    1111          178 :     int idxnum = 0;
    1112         1780 :     for (size_t k = 0; k < sizeof(handled_cols) / sizeof(handled_cols[0]); ++k) {
    1113         1602 :         int col = handled_cols[k];
    1114         6352 :         for (int i = 0; i < idxinfo->nConstraint; ++i) {
    1115         5753 :             struct sqlite3_index_constraint *cn = &idxinfo->aConstraint[i];
    1116         5753 :             if (!cn->usable || cn->op != SQLITE_INDEX_CONSTRAINT_EQ || cn->iColumn != col) continue;
    1117         1003 :             idxinfo->aConstraintUsage[i].argvIndex = argv_index++;
    1118         1003 :             idxinfo->aConstraintUsage[i].omit = 1;
    1119         1003 :             idxnum |= (1 << k);
    1120         1003 :             break; // at most one constraint consumed per hidden column
    1121              :         }
    1122         1602 :     }
    1123          178 :     idxinfo->idxNum = idxnum;
    1124          178 :     idxinfo->estimatedCost = 10.0;
    1125          178 :     idxinfo->estimatedRows = 10;
    1126          178 :     return SQLITE_OK;
    1127              : }
    1128              : 
    1129         3425 : static int payload_chunks_step_source(cloudsync_payload_chunks_cursor *c) {
    1130         3425 :     int rc = sqlite3_step(c->src);
    1131         3425 :     if (rc == SQLITE_ROW) { c->has_row = true; return SQLITE_OK; }
    1132           66 :     c->has_row = false;
    1133           66 :     if (rc == SQLITE_DONE) return SQLITE_OK;
    1134              :     // copy the inner statement's message onto this vtab or SQLite surfaces the
    1135              :     // error as a bare "SQL logic error"
    1136            0 :     if (c->vtab->base.zErrMsg) sqlite3_free(c->vtab->base.zErrMsg);
    1137            0 :     c->vtab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(c->vtab->db));
    1138            0 :     return rc;
    1139         3425 : }
    1140              : 
    1141           61 : static int payload_chunks_plan_fragment(cloudsync_payload_chunks_cursor *c) {
    1142           61 :     cloudsync_context *data = c->vtab->data;
    1143          122 :     int target = cloudsync_payload_fragment_data_size(data,
    1144           61 :         (const char *)sqlite3_column_text(c->src, 0), sqlite3_column_bytes(c->src, 0),
    1145           61 :         sqlite3_column_blob(c->src, 1), sqlite3_column_bytes(c->src, 1),
    1146           61 :         (const char *)sqlite3_column_text(c->src, 2), sqlite3_column_bytes(c->src, 2),
    1147           61 :         sqlite3_column_int64(c->src, 4), sqlite3_column_int64(c->src, 5),
    1148           61 :         sqlite3_column_blob(c->src, 6), sqlite3_column_bytes(c->src, 6),
    1149           61 :         sqlite3_column_int64(c->src, 7), sqlite3_column_int64(c->src, 8),
    1150           61 :         c->frag_total, 0, 1);
    1151           61 :     if (target <= 0) return SQLITE_TOOBIG;
    1152              : 
    1153           61 :     int count = 0;
    1154           61 :     for (int i = 0; i < CLOUDSYNC_PAYLOAD_FRAGMENT_SIZE_FIXPOINT_ITERATIONS; ++i) {
    1155           61 :         count = cloudsync_payload_fragment_count(c->frag_total, target);
    1156           61 :         if (count <= 0) return SQLITE_TOOBIG;
    1157          122 :         int planned = cloudsync_payload_fragment_data_size(data,
    1158           61 :             (const char *)sqlite3_column_text(c->src, 0), sqlite3_column_bytes(c->src, 0),
    1159           61 :             sqlite3_column_blob(c->src, 1), sqlite3_column_bytes(c->src, 1),
    1160           61 :             (const char *)sqlite3_column_text(c->src, 2), sqlite3_column_bytes(c->src, 2),
    1161           61 :             sqlite3_column_int64(c->src, 4), sqlite3_column_int64(c->src, 5),
    1162           61 :             sqlite3_column_blob(c->src, 6), sqlite3_column_bytes(c->src, 6),
    1163           61 :             sqlite3_column_int64(c->src, 7), sqlite3_column_int64(c->src, 8),
    1164           61 :             c->frag_total, count - 1, count);
    1165           61 :         if (planned <= 0) return SQLITE_TOOBIG;
    1166           61 :         if (planned == target) break;
    1167            0 :         target = planned;
    1168            0 :     }
    1169              : 
    1170           61 :     c->frag_target = target;
    1171           61 :     c->frag_count = cloudsync_payload_fragment_count(c->frag_total, target);
    1172           61 :     if (c->frag_count <= 0) return SQLITE_TOOBIG;
    1173           61 :     return SQLITE_OK;
    1174           61 : }
    1175              : 
    1176              : // Set up fragment state for the current source row (a single value larger than
    1177              : // max_chunk_size) so emit_fragment can stream it. start_offset is the byte offset
    1178              : // within the encoded value to resume from (0 when first reaching the value;
    1179              : // >0 when a positional cursor resumes mid-value). frag_part is derived from the
    1180              : // offset so the fragment's part index is consistent whether reached by streaming
    1181              : // or by a seek. The plan (frag_target/frag_count) is a deterministic function of
    1182              : // the row, so a resumed fragment tiles identically to a streamed one.
    1183           61 : static int payload_chunks_begin_fragment(cloudsync_payload_chunks_cursor *c, int64_t start_offset) {
    1184           61 :     dbvalue_t *col_value = (dbvalue_t *)sqlite3_column_value(c->src, 3);
    1185           61 :     int type = database_value_type(col_value);
    1186           61 :     if (type != DBTYPE_TEXT && type != DBTYPE_BLOB) return SQLITE_TOOBIG;
    1187           61 :     int64_t raw_len = 0;
    1188           61 :     int header_len = cloudsync_payload_encoded_value_header(col_value, c->value_header, sizeof(c->value_header), &raw_len);
    1189           61 :     if (header_len <= 0) return SQLITE_ERROR;
    1190           61 :     c->value_header_len = header_len;
    1191           61 :     c->value_data = (const char *)database_value_blob(col_value);
    1192           61 :     c->value_data_len = raw_len;
    1193           61 :     c->frag_total = header_len + raw_len;
    1194           61 :     c->frag_offset = start_offset;
    1195           61 :     int rc = payload_chunks_plan_fragment(c);
    1196           61 :     if (rc != SQLITE_OK) return rc;
    1197           61 :     c->frag_part = (c->frag_target > 0) ? (int)(start_offset / c->frag_target) : 0;
    1198           61 :     c->frag_checksum = cloudsync_payload_encoded_value_checksum(col_value);
    1199           61 :     c->frag_active = true;
    1200           61 :     return SQLITE_OK;
    1201           61 : }
    1202              : 
    1203          133 : static int payload_chunks_emit_fragment(cloudsync_payload_chunks_cursor *c) {
    1204          133 :     cloudsync_context *data = c->vtab->data;
    1205          133 :     if (c->payload) { cloudsync_memory_free(c->payload); c->payload = NULL; }
    1206          133 :     int64_t remaining = c->frag_total - c->frag_offset;
    1207          133 :     int frag_len = remaining > c->frag_target ? c->frag_target : (int)remaining;
    1208          133 :     if (frag_len <= 0) return SQLITE_CORRUPT;
    1209          133 :     char *frag = cloudsync_memory_alloc((uint64_t)frag_len);
    1210          133 :     if (!frag) return SQLITE_NOMEM;
    1211          133 :     int copied = 0;
    1212          133 :     int64_t off = c->frag_offset;
    1213          133 :     if (off < c->value_header_len) {
    1214           55 :         int n = c->value_header_len - (int)off;
    1215           55 :         if (n > frag_len) n = frag_len;
    1216           55 :         memcpy(frag, c->value_header + off, (size_t)n);
    1217           55 :         copied += n;
    1218           55 :         off += n;
    1219           55 :     }
    1220          133 :     if (copied < frag_len) {
    1221          133 :         int64_t data_off = off - c->value_header_len;
    1222          133 :         memcpy(frag + copied, c->value_data + data_off, (size_t)(frag_len - copied));
    1223          133 :     }
    1224              : 
    1225          133 :     cloudsync_payload_context *payload = cloudsync_memory_zeroalloc((uint64_t)cloudsync_payload_context_size(NULL));
    1226          133 :     if (!payload) { cloudsync_memory_free(frag); return SQLITE_NOMEM; }
    1227          266 :     int rc = cloudsync_payload_encode_fragment_step(payload, data,
    1228          133 :         (const char *)sqlite3_column_text(c->src, 0), sqlite3_column_bytes(c->src, 0),
    1229          133 :         sqlite3_column_blob(c->src, 1), sqlite3_column_bytes(c->src, 1),
    1230          133 :         (const char *)sqlite3_column_text(c->src, 2), sqlite3_column_bytes(c->src, 2),
    1231          133 :         frag, frag_len,
    1232          133 :         sqlite3_column_int64(c->src, 4), sqlite3_column_int64(c->src, 5),
    1233          133 :         sqlite3_column_blob(c->src, 6), sqlite3_column_bytes(c->src, 6),
    1234          133 :         sqlite3_column_int64(c->src, 7), sqlite3_column_int64(c->src, 8),
    1235          133 :         c->frag_checksum, c->frag_total, c->frag_part, c->frag_count);
    1236          133 :     cloudsync_memory_free(frag);
    1237          133 :     if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
    1238          133 :     rc = cloudsync_payload_encode_final(payload, data);
    1239          133 :     if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
    1240              :     // A fragment chunk spends the window budget like any other. The fragment paths
    1241              :     // return before the ordinary builder's accounting, so without this a history made
    1242              :     // of oversized values never spends the budget and the cap never fires.
    1243          133 :     c->window_bytes += (int64_t)cloudsync_payload_context_bused(payload);
    1244          133 :     c->payload = cloudsync_payload_blob(payload, &c->payload_size, &c->rows);
    1245          133 :     cloudsync_memory_free(payload);
    1246          133 :     c->dbv_min = sqlite3_column_int64(c->src, 5);
    1247          133 :     c->dbv_max = c->dbv_min;
    1248          133 :     c->chunk_index++;
    1249          133 :     c->frag_offset += frag_len;
    1250          133 :     c->frag_part++;
    1251          133 :     if (c->frag_part >= c->frag_count) {
    1252           55 :         c->frag_active = false;
    1253           55 :         rc = payload_chunks_step_source(c);
    1254           55 :     }
    1255          133 :     return rc;
    1256          133 : }
    1257              : 
    1258          376 : static int payload_chunks_build_next(cloudsync_payload_chunks_cursor *c) {
    1259          376 :     cloudsync_context *data = c->vtab->data;
    1260          376 :     int rc = SQLITE_OK;
    1261          376 :     if (c->payload) { cloudsync_memory_free(c->payload); c->payload = NULL; }
    1262          376 :     c->payload_size = c->rows = c->dbv_min = c->dbv_max = 0;
    1263          376 :     if (c->frag_active) return payload_chunks_emit_fragment(c);
    1264          298 :     if (!c->has_row) { c->eof = true; return SQLITE_OK; }
    1265              : 
    1266          246 :     int max_size = cloudsync_payload_max_chunk_size(data);
    1267          246 :     size_t payload_header_size = 0;
    1268          246 :     cloudsync_payload_context_size(&payload_header_size);
    1269          246 :     cloudsync_payload_context *payload = cloudsync_memory_zeroalloc((uint64_t)cloudsync_payload_context_size(NULL));
    1270          246 :     if (!payload) return SQLITE_NOMEM;
    1271         3431 :     while (c->has_row) {
    1272              :         sqlite3_value *rowv[9];
    1273        33820 :         for (int i = 0; i < 9; ++i) rowv[i] = sqlite3_column_value(c->src, i);
    1274         3382 :         size_t row_size = pk_encode_size((dbvalue_t **)rowv, 9, 0, -1);
    1275         3382 :         if (row_size == SIZE_MAX) { cloudsync_memory_free(payload); return SQLITE_NOMEM; }
    1276              : 
    1277         3382 :         if ((int64_t)row_size + (int64_t)payload_header_size + CLOUDSYNC_PAYLOAD_CHUNK_SAFETY_MARGIN > max_size) {
    1278           61 :             if (cloudsync_payload_context_nrows(payload) > 0) break;
    1279           55 :             cloudsync_memory_free(payload);
    1280           55 :             rc = payload_chunks_begin_fragment(c, 0);
    1281           55 :             if (rc != SQLITE_OK) return rc;
    1282           55 :             return payload_chunks_emit_fragment(c);
    1283              :         }
    1284              : 
    1285         3321 :         if (cloudsync_payload_context_nrows(payload) > 0 && cloudsync_payload_context_bused(payload) + row_size > (size_t)max_size) break;
    1286              :         // Once the budget is spent, end the chunk at the first db_version boundary so
    1287              :         // the window can be capped there. Waiting for a chunk to happen to end on a
    1288              :         // boundary is not enough: when a transaction's rows and a chunk's capacity stay
    1289              :         // out of step, every chunk ends mid-version and the cap never fires at all.
    1290         3377 :         if (c->max_window_bytes > 0 && cloudsync_payload_context_nrows(payload) > 0 &&
    1291          671 :             sqlite3_column_int64(c->src, 5) != c->dbv_max &&
    1292          141 :             c->window_bytes + (int64_t)cloudsync_payload_context_bused(payload) >= c->max_window_bytes) break;
    1293         3185 :         rc = cloudsync_payload_encode_step(payload, data, 9, (dbvalue_t **)rowv);
    1294         3185 :         if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
    1295         3185 :         int64_t dbv = sqlite3_column_int64(c->src, 5);
    1296         3185 :         if (cloudsync_payload_context_nrows(payload) == 1) c->dbv_min = dbv;
    1297         3185 :         c->dbv_max = dbv;
    1298         3185 :         rc = payload_chunks_step_source(c);
    1299         3185 :         if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
    1300              :     }
    1301              : 
    1302          191 :     if (cloudsync_payload_context_nrows(payload) == 0) { cloudsync_memory_free(payload); c->eof = true; return SQLITE_OK; }
    1303          191 :     rc = cloudsync_payload_encode_final(payload, data);
    1304          191 :     if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
    1305              :     // Measure the window in the unit max_size is expressed in -- encoded bytes before
    1306              :     // compression -- so a budget and a chunk size mean the same thing to a caller.
    1307          191 :     c->window_bytes += (int64_t)cloudsync_payload_context_bused(payload);
    1308          191 :     c->payload = cloudsync_payload_blob(payload, &c->payload_size, &c->rows);
    1309          191 :     cloudsync_memory_free(payload);
    1310          191 :     c->chunk_index++;
    1311          191 :     return SQLITE_OK;
    1312          376 : }
    1313              : 
    1314              : // Record the resume point a stateless caller passes back to continue after the
    1315              : // chunk just built. Reads the source statement, which is positioned at the next
    1316              : // unconsumed row (or the same row when a value is still mid-fragment). Must be
    1317              : // called only after build_next produced a chunk (i.e. !eof).
    1318          324 : static void payload_chunks_set_next_cursor(cloudsync_payload_chunks_cursor *c) {
    1319          324 :     if (c->frag_active) {
    1320              :         // Mid-value: resume the same row at the next byte offset.
    1321           78 :         c->next_dbv = sqlite3_column_int64(c->src, 5);
    1322           78 :         c->next_seq = sqlite3_column_int64(c->src, 8);
    1323           78 :         c->next_frag_offset = c->frag_offset;
    1324           78 :         c->is_final = false;
    1325          324 :     } else if (c->has_row) {
    1326              :         // Row boundary: the next chunk starts at the current (unconsumed) row.
    1327          188 :         c->next_dbv = sqlite3_column_int64(c->src, 5);
    1328          188 :         c->next_seq = sqlite3_column_int64(c->src, 8);
    1329          188 :         c->next_frag_offset = 0;
    1330          188 :         c->is_final = false;
    1331          188 :     } else {
    1332              :         // Stream exhausted: this was the last chunk of the window.
    1333           58 :         c->next_dbv = c->watermark;
    1334           58 :         c->next_seq = 0;
    1335           58 :         c->next_frag_offset = 0;
    1336           58 :         c->is_final = true;
    1337              :     }
    1338          324 : }
    1339              : 
    1340              : // Stop the scan once max_window_bytes is spent, at the first db_version boundary, and
    1341              : // report the window as an ordinary complete stream ending at that reduced watermark.
    1342              : // The caller then checkpoints there and asks again, so preparation is bounded without
    1343              : // any new resumable state.
    1344              : //
    1345              : // Two conditions are load-bearing. Never cap mid-value (frag_active) or mid-db_version
    1346              : // (next_dbv == dbv_max): the receive cursor must land on a complete db_version or the
    1347              : // next request skips the unapplied remainder, since it resumes with db_version > since
    1348              : // and no seq. And because the boundary test is what stops us, a db_version larger than
    1349              : // the whole budget is still emitted in full -- otherwise a window could end up empty
    1350              : // and the drain would never advance at all.
    1351          324 : static void payload_chunks_apply_window_cap(cloudsync_payload_chunks_cursor *c) {
    1352          324 :     if (c->max_window_bytes <= 0 || c->eof) return;
    1353          138 :     if (c->is_final || c->frag_active) return;
    1354          114 :     if (c->window_bytes < c->max_window_bytes) return;
    1355          110 :     if (c->next_dbv == c->dbv_max) return;      // still inside a db_version
    1356              : 
    1357          106 :     c->window_capped = true;
    1358          106 :     c->is_final = true;
    1359          106 :     c->watermark = c->dbv_max;                  // the window the caller checkpoints at
    1360          106 :     c->next_dbv = c->watermark;
    1361          106 :     c->next_seq = 0;
    1362          106 :     c->next_frag_offset = 0;
    1363          324 : }
    1364              : 
    1365          376 : static int payload_chunks_advance(cloudsync_payload_chunks_cursor *c) {
    1366          376 :     int rc = payload_chunks_build_next(c);
    1367          376 :     if (rc == SQLITE_OK && !c->eof) {
    1368          324 :         payload_chunks_set_next_cursor(c);
    1369          324 :         payload_chunks_apply_window_cap(c);
    1370          324 :     }
    1371          376 :     return rc;
    1372              : }
    1373              : 
    1374          187 : static int payload_chunks_filter(sqlite3_vtab_cursor *cursor, int idxnum, const char *idxstr, int argc, sqlite3_value **argv) {
    1375          187 :     UNUSED_PARAMETER(idxstr); UNUSED_PARAMETER(argc);
    1376          187 :     cloudsync_payload_chunks_cursor *c = (cloudsync_payload_chunks_cursor *)cursor;
    1377          187 :     cloudsync_context *data = c->vtab->data;
    1378          187 :     if (!cloudsync_context_is_initialized(data)) {
    1379            1 :         c->vtab->base.zErrMsg = sqlite3_mprintf(
    1380              :             "cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
    1381              :             "to enable sync on a table before querying cloudsync_payload_chunks.");
    1382            1 :         return SQLITE_ERROR;
    1383              :     }
    1384          186 :     if (c->src) { sqlite3_finalize(c->src); c->src = NULL; }
    1385          186 :     if (c->payload) { cloudsync_memory_free(c->payload); c->payload = NULL; }
    1386              :     // Contract: all per-scan state that can be bulk-reset here must live at or
    1387              :     // after eof. Fields before eof are cursor lifetime state preserved across
    1388              :     // xFilter calls.
    1389          186 :     memset(&c->eof, 0, sizeof(*c) - offsetof(cloudsync_payload_chunks_cursor, eof));
    1390              : 
    1391          186 :     int argi = 0;
    1392          186 :     int64_t since = dbutils_settings_get_int64_value(data, CLOUDSYNC_KEY_SEND_DBVERSION);
    1393          186 :     const void *site_id = NULL;
    1394          186 :     int site_id_len = 0;
    1395          186 :     bool site_id_given = false;
    1396          186 :     int64_t until = 0;
    1397          186 :     bool exclude = false;
    1398              :     // Positional resume cursor (cols 15..17): when resume_db_version is bound the
    1399              :     // scan starts at (resume_db_version, resume_seq) inclusive and the first chunk
    1400              :     // resumes a mid-value fragment at resume_frag_offset, instead of replaying the
    1401              :     // whole window from `since`. Lets a stateless /check page the stream with an
    1402              :     // O(1) seek per call and no spool table.
    1403          186 :     bool positional = false;
    1404          186 :     int64_t resume_dbv = 0, resume_seq = 0, resume_frag = 0;
    1405          186 :     if (idxnum & 1) since = sqlite3_value_int64(argv[argi++]);
    1406          186 :     if (idxnum & 2) {
    1407          119 :         if (sqlite3_value_type(argv[argi]) != SQLITE_NULL) {
    1408            2 :             site_id = sqlite3_value_blob(argv[argi]);
    1409            2 :             site_id_len = sqlite3_value_bytes(argv[argi]);
    1410            2 :             site_id_given = true;
    1411            2 :         }
    1412          119 :         argi++;
    1413          119 :     }
    1414          186 :     if (idxnum & 4) until = sqlite3_value_int64(argv[argi++]);
    1415          186 :     if (idxnum & 8) exclude = (sqlite3_value_int(argv[argi++]) != 0);
    1416              :     // An explicit NULL means "not given", as it already does for site_id. Callers must
    1417              :     // pass NULLs to reach a later argument, and treating one as a resume point of 0
    1418              :     // would silently restart the scan at the beginning of the window.
    1419          186 :     if (idxnum & 16) {
    1420          124 :         if (sqlite3_value_type(argv[argi]) != SQLITE_NULL) {
    1421           14 :             resume_dbv = sqlite3_value_int64(argv[argi]);
    1422           14 :             positional = true;
    1423           14 :         }
    1424          124 :         argi++;
    1425          124 :     }
    1426          186 :     if (idxnum & 32) resume_seq = sqlite3_value_int64(argv[argi++]);
    1427          186 :     if (idxnum & 64) resume_frag = sqlite3_value_int64(argv[argi++]);
    1428          186 :     if (idxnum & 128) {
    1429          114 :         int64_t cap = sqlite3_value_int64(argv[argi++]);
    1430          114 :         c->max_window_bytes = (cap > 0) ? cap : 0;   // <= 0 means no cap
    1431          114 :     }
    1432              :     // Budget already spent by earlier calls of this window. The per-scan reset above
    1433              :     // cleared window_bytes, so a caller paging one chunk at a time seeds it here; a
    1434              :     // caller draining the window in one scan leaves it at 0 and it accumulates.
    1435          186 :     if (idxnum & 256) {
    1436            6 :         int64_t spent = sqlite3_value_int64(argv[argi++]);
    1437            6 :         c->window_bytes = (spent > 0) ? spent : 0;
    1438            6 :     }
    1439              : 
    1440              :     // Resolve the site filter:
    1441              :     //   exclude=true  -> all sites except filter_site_id (CHECK path); site required
    1442              :     //   filter given  -> only that site
    1443              :     //   default       -> local site (send path, unchanged)
    1444              :     const char *site_op;
    1445          186 :     if (exclude) {
    1446            2 :         if (!site_id_given) {
    1447            1 :             c->vtab->base.zErrMsg = sqlite3_mprintf(
    1448              :                 "cloudsync_payload_chunks: exclude_filter_site_id requires a non-NULL site_id");
    1449            1 :             return SQLITE_ERROR;
    1450              :         }
    1451            1 :         site_op = "<>";
    1452            1 :     } else {
    1453          184 :         site_op = "=";
    1454          184 :         if (!site_id_given) { site_id = cloudsync_siteid(data); site_id_len = UUID_LEN; }
    1455              :     }
    1456              : 
    1457          185 :     if (until == 0) {
    1458          161 :         char *mxsql = sqlite3_mprintf(
    1459          161 :             "SELECT COALESCE(MAX(db_version),0) FROM cloudsync_changes WHERE site_id%s?", site_op);
    1460          161 :         if (!mxsql) return SQLITE_NOMEM;
    1461          161 :         sqlite3_stmt *mx = NULL;
    1462          161 :         int rc = sqlite3_prepare_v2(c->vtab->db, mxsql, -1, &mx, NULL);
    1463          161 :         sqlite3_free(mxsql);
    1464          161 :         if (rc != SQLITE_OK) return rc;
    1465          161 :         sqlite3_bind_blob(mx, 1, site_id, site_id_len, SQLITE_TRANSIENT);
    1466              :         // MAX() yields exactly one row, so anything else is an error: propagate
    1467              :         // it instead of silently scanning an empty window with until=0
    1468          161 :         rc = sqlite3_step(mx);
    1469          161 :         if (rc == SQLITE_ROW) until = sqlite3_column_int64(mx, 0);
    1470          161 :         sqlite3_finalize(mx);
    1471          161 :         if (rc != SQLITE_ROW) {
    1472            0 :             if (c->vtab->base.zErrMsg) sqlite3_free(c->vtab->base.zErrMsg);
    1473            0 :             c->vtab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(c->vtab->db));
    1474            0 :             return rc == SQLITE_DONE ? SQLITE_ERROR : rc;
    1475              :         }
    1476          161 :     }
    1477          185 :     c->watermark = until;
    1478              : 
    1479              :     // Window upper bound is always `until`. The lower bound is either the legacy
    1480              :     // exclusive `since` (db_version > since) or the inclusive positional cursor
    1481              :     // (db_version, seq) >= (resume_dbv, resume_seq).
    1482              :     char *sql;
    1483          185 :     if (positional) {
    1484              :         // The redundant db_version>=? is what makes the resume a seek, and it is
    1485              :         // load-bearing: SQLite derives a range from a disjunction only when both arms
    1486              :         // compare against the same value, and these two arms carry distinct
    1487              :         // parameters. Without it cloudsync_changes' xBestIndex is offered no lower
    1488              :         // bound at all and every call replays the window from the start.
    1489           14 :         sql = sqlite3_mprintf(
    1490              :             "SELECT tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq "
    1491              :             "FROM cloudsync_changes WHERE db_version<=? AND site_id%s? AND db_version>=? AND "
    1492              :             "(db_version>? OR (db_version=? AND seq>=?)) ORDER BY db_version, seq ASC",
    1493           14 :             site_op);
    1494           14 :     } else {
    1495          171 :         sql = sqlite3_mprintf(
    1496              :             "SELECT tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq "
    1497              :             "FROM cloudsync_changes WHERE db_version>? AND site_id%s? AND db_version<=? ORDER BY db_version, seq ASC",
    1498          171 :             site_op);
    1499              :     }
    1500          185 :     if (!sql) return SQLITE_NOMEM;
    1501          185 :     int rc = sqlite3_prepare_v2(c->vtab->db, sql, -1, &c->src, NULL);
    1502          185 :     sqlite3_free(sql);
    1503          185 :     if (rc != SQLITE_OK) return rc;
    1504          185 :     if (positional) {
    1505           14 :         sqlite3_bind_int64(c->src, 1, until);
    1506           14 :         sqlite3_bind_blob(c->src, 2, site_id, site_id_len, SQLITE_TRANSIENT);
    1507           14 :         sqlite3_bind_int64(c->src, 3, resume_dbv);
    1508           14 :         sqlite3_bind_int64(c->src, 4, resume_dbv);
    1509           14 :         sqlite3_bind_int64(c->src, 5, resume_dbv);
    1510           14 :         sqlite3_bind_int64(c->src, 6, resume_seq);
    1511           14 :     } else {
    1512          171 :         sqlite3_bind_int64(c->src, 1, since);
    1513          171 :         sqlite3_bind_blob(c->src, 2, site_id, site_id_len, SQLITE_TRANSIENT);
    1514          171 :         sqlite3_bind_int64(c->src, 3, until);
    1515              :     }
    1516          185 :     rc = payload_chunks_step_source(c);
    1517          185 :     if (rc != SQLITE_OK) return rc;
    1518              :     // Resuming inside a value that was fragmented across chunks: the first row is
    1519              :     // that value; re-establish the fragment plan and skip to resume_frag.
    1520          185 :     if (positional && resume_frag > 0 && c->has_row) {
    1521            6 :         rc = payload_chunks_begin_fragment(c, resume_frag);
    1522            6 :         if (rc != SQLITE_OK) return rc;
    1523            6 :     }
    1524          185 :     return payload_chunks_advance(c);
    1525          187 : }
    1526              : 
    1527          296 : static int payload_chunks_next(sqlite3_vtab_cursor *cursor) {
    1528          296 :     cloudsync_payload_chunks_cursor *c = (cloudsync_payload_chunks_cursor *)cursor;
    1529              :     // A capped window ends the scan: the rows past it belong to the next window, and
    1530              :     // emitting them here would contradict the reduced watermark already reported.
    1531          296 :     if (c->window_capped) { c->eof = true; return SQLITE_OK; }
    1532          191 :     return payload_chunks_advance(c);
    1533          296 : }
    1534              : 
    1535          481 : static int payload_chunks_eof(sqlite3_vtab_cursor *cursor) {
    1536          481 :     return ((cloudsync_payload_chunks_cursor *)cursor)->eof;
    1537              : }
    1538              : 
    1539         1378 : static int payload_chunks_column(sqlite3_vtab_cursor *cursor, sqlite3_context *ctx, int col) {
    1540         1378 :     cloudsync_payload_chunks_cursor *c = (cloudsync_payload_chunks_cursor *)cursor;
    1541         1378 :     switch (col) {
    1542          129 :         case 0: sqlite3_result_blob64(ctx, c->payload, (sqlite3_uint64)c->payload_size, SQLITE_TRANSIENT); break;
    1543           63 :         case 1: sqlite3_result_int(ctx, c->chunk_index - 1); break;
    1544          173 :         case 2: sqlite3_result_int64(ctx, c->payload_size); break;
    1545          226 :         case 3: sqlite3_result_int64(ctx, c->rows); break;
    1546          141 :         case 4: sqlite3_result_int64(ctx, c->dbv_min); break;
    1547            9 :         case 5: sqlite3_result_int64(ctx, c->dbv_max); break;
    1548          162 :         case 6: sqlite3_result_int64(ctx, c->watermark); break;
    1549           16 :         case 11: sqlite3_result_int64(ctx, c->next_dbv); break;
    1550           16 :         case 12: sqlite3_result_int64(ctx, c->next_seq); break;
    1551           16 :         case 13: sqlite3_result_int64(ctx, c->next_frag_offset); break;
    1552          257 :         case 14: sqlite3_result_int(ctx, c->is_final ? 1 : 0); break;
    1553          164 :         case 15: sqlite3_result_int(ctx, c->window_capped ? 1 : 0); break;
    1554            6 :         case 16: sqlite3_result_int64(ctx, c->window_bytes); break;
    1555            0 :         default: sqlite3_result_null(ctx); break;
    1556              :     }
    1557         1378 :     return SQLITE_OK;
    1558              : }
    1559              : 
    1560            0 : static int payload_chunks_rowid(sqlite3_vtab_cursor *cursor, sqlite3_int64 *rowid) {
    1561            0 :     *rowid = ((cloudsync_payload_chunks_cursor *)cursor)->chunk_index;
    1562            0 :     return SQLITE_OK;
    1563              : }
    1564              : 
    1565              : static sqlite3_module cloudsync_payload_chunks_module = {
    1566              :     /* iVersion    */ 0,
    1567              :     /* xCreate     */ NULL,
    1568              :     /* xConnect    */ payload_chunks_connect,
    1569              :     /* xBestIndex  */ payload_chunks_best_index,
    1570              :     /* xDisconnect */ payload_chunks_disconnect,
    1571              :     /* xDestroy    */ NULL,
    1572              :     /* xOpen       */ payload_chunks_open,
    1573              :     /* xClose      */ payload_chunks_close,
    1574              :     /* xFilter     */ payload_chunks_filter,
    1575              :     /* xNext       */ payload_chunks_next,
    1576              :     /* xEof        */ payload_chunks_eof,
    1577              :     /* xColumn     */ payload_chunks_column,
    1578              :     /* xRowid      */ payload_chunks_rowid,
    1579              :     /* xUpdate     */ NULL,
    1580              :     /* xBegin      */ NULL,
    1581              :     /* xSync       */ NULL,
    1582              :     /* xCommit     */ NULL,
    1583              :     /* xRollback   */ NULL,
    1584              :     /* xFindMethod */ NULL,
    1585              :     /* xRename     */ NULL,
    1586              :     /* xSavepoint  */ NULL,
    1587              :     /* xRelease    */ NULL,
    1588              :     /* xRollbackTo */ NULL,
    1589              :     /* xShadowName */ NULL,
    1590              :     /* xIntegrity  */ NULL
    1591              : };
    1592              : 
    1593         1060 : static int payload_estimated_size_add(sqlite3_int64 *acc, sqlite3_int64 value) {
    1594         1060 :     if (value < 0 || *acc > INT64_MAX - value) return SQLITE_TOOBIG;
    1595         1060 :     *acc += value;
    1596         1060 :     return SQLITE_OK;
    1597         1060 : }
    1598              : 
    1599            6 : static int payload_blob_checked_estimate(sqlite3 *db, const void *site_id, int site_id_len,
    1600              :                                          sqlite3_int64 since, sqlite3_int64 since_seq,
    1601              :                                          bool exclude, sqlite3_int64 *estimated,
    1602              :                                          sqlite3_int64 *watermark) {
    1603            6 :     sqlite3_stmt *stmt = NULL;
    1604            6 :     sqlite3_stmt *mx = NULL;
    1605            6 :     int rc = SQLITE_OK;
    1606            6 :     sqlite3_int64 until = 0;
    1607            6 :     size_t header_size = 0;
    1608            6 :     bool has_rows = false;
    1609            6 :     const char *site_op = exclude ? "<>" : "=";
    1610            6 :     *estimated = 0;
    1611              : 
    1612            6 :     char *mxsql = sqlite3_mprintf(
    1613            6 :         "SELECT COALESCE(MAX(db_version),0) FROM cloudsync_changes WHERE site_id%s?", site_op);
    1614            6 :     if (!mxsql) return SQLITE_NOMEM;
    1615            6 :     rc = sqlite3_prepare_v2(db, mxsql, -1, &mx, NULL);
    1616            6 :     sqlite3_free(mxsql);
    1617            6 :     if (rc != SQLITE_OK) goto error;
    1618            6 :     sqlite3_bind_blob(mx, 1, site_id, site_id_len, SQLITE_TRANSIENT);
    1619            6 :     rc = sqlite3_step(mx);
    1620            6 :     if (rc == SQLITE_ROW) until = sqlite3_column_int64(mx, 0);
    1621            0 :     else if (rc != SQLITE_DONE) goto error;
    1622            6 :     sqlite3_finalize(mx);
    1623            6 :     mx = NULL;
    1624            6 :     if (watermark) *watermark = until;
    1625              : 
    1626            6 :     if (until < since) return SQLITE_OK;
    1627              : 
    1628            4 :     char *sql = sqlite3_mprintf(
    1629              :         "SELECT tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq "
    1630              :         "FROM cloudsync_changes WHERE (db_version>? OR (db_version=? AND seq>?)) "
    1631              :         "AND site_id%s? AND db_version<=? ORDER BY db_version, seq ASC",
    1632            4 :         site_op);
    1633            4 :     if (!sql) return SQLITE_NOMEM;
    1634            4 :     rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    1635            4 :     sqlite3_free(sql);
    1636            4 :     if (rc != SQLITE_OK) goto error;
    1637            4 :     sqlite3_bind_int64(stmt, 1, since);
    1638            4 :     sqlite3_bind_int64(stmt, 2, since);
    1639            4 :     sqlite3_bind_int64(stmt, 3, since_seq);
    1640            4 :     sqlite3_bind_blob(stmt, 4, site_id, site_id_len, SQLITE_TRANSIENT);
    1641            4 :     sqlite3_bind_int64(stmt, 5, until);
    1642              : 
    1643            4 :     cloudsync_payload_context_size(&header_size);
    1644         1060 :     while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
    1645              :         sqlite3_value *rowv[9];
    1646        10560 :         for (int i = 0; i < 9; ++i) rowv[i] = sqlite3_column_value(stmt, i);
    1647         1056 :         size_t row_size = pk_encode_size((dbvalue_t **)rowv, 9, 0, -1);
    1648         1056 :         if (row_size == SIZE_MAX || row_size > (size_t)INT64_MAX) {
    1649            0 :             rc = SQLITE_TOOBIG;
    1650            0 :             goto error;
    1651              :         }
    1652         1056 :         if (!has_rows) {
    1653            4 :             if (header_size > (size_t)INT64_MAX) {
    1654            0 :                 rc = SQLITE_TOOBIG;
    1655            0 :                 goto error;
    1656              :             }
    1657            4 :             rc = payload_estimated_size_add(estimated, (sqlite3_int64)header_size);
    1658            4 :             if (rc != SQLITE_OK) goto error;
    1659            4 :             has_rows = true;
    1660            4 :         }
    1661         1056 :         rc = payload_estimated_size_add(estimated, (sqlite3_int64)row_size);
    1662         1056 :         if (rc != SQLITE_OK) goto error;
    1663              :     }
    1664            4 :     if (rc != SQLITE_DONE) goto error;
    1665            4 :     sqlite3_finalize(stmt);
    1666            4 :     return SQLITE_OK;
    1667              : 
    1668              : error:
    1669            0 :     if (stmt) sqlite3_finalize(stmt);
    1670            0 :     if (mx) sqlite3_finalize(mx);
    1671            0 :     return rc;
    1672            6 : }
    1673              : 
    1674            7 : void dbsync_payload_blob_checked(sqlite3_context *context, int argc, sqlite3_value **argv) {
    1675              :     DEBUG_FUNCTION("cloudsync_payload_blob_checked");
    1676            7 :     UNUSED_PARAMETER(argc);
    1677              : 
    1678            7 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1679            7 :     sqlite3 *db = sqlite3_context_db_handle(context);
    1680            7 :     sqlite3_stmt *stmt = NULL;
    1681            7 :     cloudsync_payload_context *payload = NULL;
    1682            7 :     int rc = SQLITE_OK;
    1683            7 :     sqlite3_int64 since = 0;
    1684            7 :     sqlite3_int64 since_seq = 0;
    1685            7 :     sqlite3_int64 max_estimated_size = 0;
    1686            7 :     sqlite3_int64 estimated = 0;
    1687            7 :     sqlite3_int64 watermark = 0;
    1688            7 :     const void *site_id = NULL;
    1689            7 :     int site_id_len = 0;
    1690            7 :     bool exclude = false;
    1691              : 
    1692            7 :     if (sqlite3_value_type(argv[0]) != SQLITE_NULL) since = sqlite3_value_int64(argv[0]);
    1693            7 :     if (sqlite3_value_type(argv[1]) != SQLITE_NULL) since_seq = sqlite3_value_int64(argv[1]);
    1694            7 :     if (sqlite3_value_type(argv[2]) != SQLITE_NULL) {
    1695            3 :         site_id = sqlite3_value_blob(argv[2]);
    1696            3 :         site_id_len = sqlite3_value_bytes(argv[2]);
    1697            3 :     }
    1698            7 :     exclude = sqlite3_value_type(argv[3]) != SQLITE_NULL && sqlite3_value_int(argv[3]) != 0;
    1699            7 :     if (sqlite3_value_type(argv[4]) == SQLITE_NULL || sqlite3_value_int64(argv[4]) <= 0) {
    1700            0 :         sqlite3_result_error(context, "cloudsync_payload_blob_checked: max_estimated_payload_size must be positive", -1);
    1701            0 :         return;
    1702              :     }
    1703            7 :     max_estimated_size = sqlite3_value_int64(argv[4]);
    1704              : 
    1705            7 :     if (exclude && !site_id) {
    1706            1 :         sqlite3_result_error(context,
    1707              :             "cloudsync_payload_blob_checked: exclude_filter_site_id requires a non-NULL site_id", -1);
    1708            1 :         return;
    1709              :     }
    1710            6 :     if (!exclude && !site_id) {
    1711            3 :         site_id = cloudsync_siteid(data);
    1712            3 :         site_id_len = UUID_LEN;
    1713            3 :     }
    1714              : 
    1715            6 :     rc = payload_blob_checked_estimate(db, site_id, site_id_len, since, since_seq, exclude, &estimated, &watermark);
    1716            6 :     if (rc != SQLITE_OK) goto error;
    1717            6 :     if (estimated == 0) {
    1718            2 :         sqlite3_result_null(context);
    1719            2 :         return;
    1720              :     }
    1721            4 :     if (estimated > max_estimated_size) {
    1722            2 :         dbsync_set_error(context,
    1723              :             CLOUDSYNC_ERRCODE_PAYLOAD_TOO_LARGE "cloudsync_payload_blob_checked: estimated payload size %" PRId64 " exceeds max_estimated_payload_size %" PRId64,
    1724            1 :             (int64_t)estimated, (int64_t)max_estimated_size);
    1725            1 :         return;
    1726              :     }
    1727              : 
    1728            3 :     const char *site_op = exclude ? "<>" : "=";
    1729            3 :     char *sql = sqlite3_mprintf(
    1730              :         "SELECT tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq "
    1731              :         "FROM cloudsync_changes WHERE (db_version>? OR (db_version=? AND seq>?)) "
    1732              :         "AND site_id%s? AND db_version<=? ORDER BY db_version, seq ASC",
    1733            3 :         site_op);
    1734            3 :     if (!sql) { rc = SQLITE_NOMEM; goto error; }
    1735            3 :     rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    1736            3 :     sqlite3_free(sql);
    1737            3 :     if (rc != SQLITE_OK) goto error;
    1738            3 :     sqlite3_bind_int64(stmt, 1, since);
    1739            3 :     sqlite3_bind_int64(stmt, 2, since);
    1740            3 :     sqlite3_bind_int64(stmt, 3, since_seq);
    1741            3 :     sqlite3_bind_blob(stmt, 4, site_id, site_id_len, SQLITE_TRANSIENT);
    1742            3 :     sqlite3_bind_int64(stmt, 5, watermark);
    1743              : 
    1744            3 :     payload = cloudsync_memory_zeroalloc((uint64_t)cloudsync_payload_context_size(NULL));
    1745            3 :     if (!payload) { rc = SQLITE_NOMEM; goto error; }
    1746              : 
    1747          533 :     while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
    1748              :         sqlite3_value *rowv[9];
    1749         5300 :         for (int i = 0; i < 9; ++i) rowv[i] = sqlite3_column_value(stmt, i);
    1750          530 :         rc = cloudsync_payload_encode_step(payload, data, 9, (dbvalue_t **)rowv);
    1751          530 :         if (rc != SQLITE_OK) goto error;
    1752              :     }
    1753            3 :     if (rc != SQLITE_DONE) goto error;
    1754            3 :     sqlite3_finalize(stmt);
    1755            3 :     stmt = NULL;
    1756              : 
    1757            3 :     rc = cloudsync_payload_encode_final(payload, data);
    1758            3 :     if (rc != SQLITE_OK) goto error;
    1759            3 :     int64_t blob_size = 0;
    1760            3 :     char *blob = cloudsync_payload_blob(payload, &blob_size, NULL);
    1761            3 :     if (!blob) {
    1762            0 :         sqlite3_result_null(context);
    1763            0 :         cloudsync_payload_context_free(payload);
    1764            0 :     } else {
    1765            3 :         sqlite3_result_blob64(context, blob, (sqlite3_uint64)blob_size, cloudsync_memory_free);
    1766            3 :         cloudsync_memory_free(payload);
    1767              :     }
    1768            3 :     return;
    1769              : 
    1770              : error:
    1771            0 :     if (stmt) sqlite3_finalize(stmt);
    1772            0 :     if (payload) cloudsync_payload_context_free(payload);
    1773            0 :     if (rc == SQLITE_NOMEM) sqlite3_result_error_nomem(context);
    1774            0 :     else if (rc == SQLITE_TOOBIG) sqlite3_result_error(context, CLOUDSYNC_ERRCODE_PAYLOAD_TOO_LARGE "cloudsync_payload_blob_checked: payload estimate is too large", -1);
    1775            0 :     else sqlite3_result_error(context, sqlite3_errmsg(db), -1);
    1776            7 : }
    1777              : 
    1778              : #ifdef CLOUDSYNC_DESKTOP_OS
    1779            0 : void dbsync_payload_save (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1780              :     DEBUG_FUNCTION("dbsync_payload_save");
    1781              :     
    1782              :     // sanity check argument
    1783            0 :     if (database_value_type(argv[0]) != SQLITE_TEXT) {
    1784            0 :         sqlite3_result_error(context, "Unable to retrieve file path.", -1);
    1785            0 :         return;
    1786              :     }
    1787              :     
    1788              :     // retrieve full path to file
    1789            0 :     const char *payload_path = (const char *)database_value_text(argv[0]);
    1790              :     
    1791              :     // retrieve global context
    1792            0 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1793              :     
    1794            0 :     int blob_size = 0;
    1795            0 :     int rc = cloudsync_payload_save(data, payload_path, &blob_size);
    1796            0 :     if (rc == SQLITE_OK) {
    1797              :         // if OK then returns blob size
    1798            0 :         sqlite3_result_int64(context, (sqlite3_int64)blob_size);
    1799            0 :         return;
    1800              :     }
    1801              :     
    1802            0 :     sqlite3_result_error(context, cloudsync_errmsg(data), -1);
    1803            0 :     sqlite3_result_error_code(context, rc);
    1804            0 : }
    1805              : 
    1806            0 : void dbsync_payload_load (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1807              :     DEBUG_FUNCTION("dbsync_payload_load");
    1808              :     
    1809              :     // sanity check argument
    1810            0 :     if (database_value_type(argv[0]) != SQLITE_TEXT) {
    1811            0 :         sqlite3_result_error(context, "Unable to retrieve file path.", -1);
    1812            0 :         return;
    1813              :     }
    1814              :     
    1815              :     // retrieve full path to file
    1816            0 :     const char *path = (const char *)database_value_text(argv[0]);
    1817              :     
    1818            0 :     int64_t payload_size = 0;
    1819            0 :     char *payload = cloudsync_file_read(path, &payload_size);
    1820            0 :     if (!payload) {
    1821            0 :         if (payload_size < 0) {
    1822            0 :             sqlite3_result_error(context, "Unable to read payload from file path.", -1);
    1823            0 :             sqlite3_result_error_code(context, SQLITE_IOERR);
    1824            0 :             return;
    1825              :         }
    1826              :         // no rows affected but no error either
    1827            0 :         sqlite3_result_int(context, 0);
    1828            0 :         return;
    1829              :     }
    1830              :     
    1831            0 :     int nrows = 0;
    1832            0 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1833              :     // File-based load applies a complete monolithic payload: legacy last-applied
    1834              :     // checkpoint (ends on a db_version boundary, so it is safe).
    1835            0 :     int rc = cloudsync_payload_apply (data, payload, (int)payload_size, &nrows, CLOUDSYNC_CHECKPOINT_LAST_APPLIED, 0);
    1836            0 :     if (payload) cloudsync_memory_free(payload);
    1837              :     
    1838            0 :     if (rc != SQLITE_OK) {
    1839            0 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
    1840            0 :         sqlite3_result_error_code(context, rc);
    1841            0 :         return;
    1842              :     }
    1843              :     
    1844              :     // returns number of applied rows
    1845            0 :     sqlite3_result_int(context, nrows);
    1846            0 : }
    1847              : #endif
    1848              : 
    1849              : // MARK: - Register -
    1850              : 
    1851        32340 : int dbsync_register_with_flags (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), void (*xstep)(sqlite3_context*,int,sqlite3_value**), void (*xfinal)(sqlite3_context*), int nargs, int flags, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
    1852              : 
    1853        32340 :     int rc = sqlite3_create_function_v2(db, name, nargs, flags, ctx, xfunc, xstep, xfinal, ctx_free);
    1854              :     
    1855        32340 :     if (rc != SQLITE_OK) {
    1856            0 :         if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Error creating function %s: %s", name, sqlite3_errmsg(db));
    1857            0 :         return rc;
    1858              :     }
    1859        32340 :     return SQLITE_OK;
    1860        32340 : }
    1861              : 
    1862        26950 : int dbsync_register (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), void (*xstep)(sqlite3_context*,int,sqlite3_value**), void (*xfinal)(sqlite3_context*), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
    1863        26950 :     const int FLAGS_VOLATILE = SQLITE_UTF8;
    1864              :     DEBUG_DBFUNCTION("dbsync_register %s", name);
    1865        26950 :     return dbsync_register_with_flags(db, name, xfunc, xstep, xfinal, nargs, FLAGS_VOLATILE, pzErrMsg, ctx, ctx_free);
    1866              : }
    1867              : 
    1868        26180 : int dbsync_register_function (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
    1869              :     DEBUG_DBFUNCTION("dbsync_register_function %s", name);
    1870        26180 :     return dbsync_register(db, name, xfunc, NULL, NULL, nargs, pzErrMsg, ctx, ctx_free);
    1871              : }
    1872              : 
    1873         2310 : int dbsync_register_pure_function (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
    1874         2310 :     const int FLAGS_PURE = SQLITE_UTF8 | SQLITE_INNOCUOUS | SQLITE_DETERMINISTIC;
    1875              :     DEBUG_DBFUNCTION("dbsync_register_pure_function %s", name);
    1876         2310 :     return dbsync_register_with_flags(db, name, xfunc, NULL, NULL, nargs, FLAGS_PURE, pzErrMsg, ctx, ctx_free);
    1877              : }
    1878              : 
    1879         2310 : int dbsync_register_trigger_function (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
    1880         2310 :     const int FLAGS_TRIGGER = SQLITE_UTF8 | SQLITE_INNOCUOUS;
    1881              :     DEBUG_DBFUNCTION("dbsync_register_trigger_function %s", name);
    1882         2310 :     return dbsync_register_with_flags(db, name, xfunc, NULL, NULL, nargs, FLAGS_TRIGGER, pzErrMsg, ctx, ctx_free);
    1883              : }
    1884              : 
    1885          770 : int dbsync_register_aggregate (sqlite3 *db, const char *name, void (*xstep)(sqlite3_context*,int,sqlite3_value**), void (*xfinal)(sqlite3_context*), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
    1886              :     DEBUG_DBFUNCTION("dbsync_register_aggregate %s", name);
    1887          770 :     return dbsync_register(db, name, NULL, xstep, xfinal, nargs, pzErrMsg, ctx, ctx_free);
    1888              : }
    1889              : 
    1890          770 : int dbsync_register_trigger_aggregate (sqlite3 *db, const char *name, void (*xstep)(sqlite3_context*,int,sqlite3_value**), void (*xfinal)(sqlite3_context*), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
    1891          770 :     const int FLAGS_TRIGGER = SQLITE_UTF8 | SQLITE_INNOCUOUS;
    1892              :     DEBUG_DBFUNCTION("dbsync_register_trigger_aggregate %s", name);
    1893          770 :     return dbsync_register_with_flags(db, name, NULL, xstep, xfinal, nargs, FLAGS_TRIGGER, pzErrMsg, ctx, ctx_free);
    1894              : }
    1895              : 
    1896              : // MARK: - Block-level LWW -
    1897              : 
    1898          603 : void dbsync_text_materialize (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1899              :     DEBUG_FUNCTION("cloudsync_text_materialize");
    1900              : 
    1901              :     // argv[0] -> table name
    1902              :     // argv[1] -> column name
    1903              :     // argv[2..N] -> primary key values
    1904              : 
    1905          603 :     if (argc < 3) {
    1906            0 :         sqlite3_result_error(context, "cloudsync_text_materialize requires at least 3 arguments: table, column, pk...", -1);
    1907            0 :         return;
    1908              :     }
    1909              : 
    1910          603 :     const char *table_name = (const char *)database_value_text(argv[0]);
    1911          603 :     const char *col_name = (const char *)database_value_text(argv[1]);
    1912          603 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1913              : 
    1914          603 :     cloudsync_table_context *table = table_lookup(data, table_name);
    1915          603 :     if (!table) {
    1916            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_text_materialize.", table_name);
    1917            0 :         return;
    1918              :     }
    1919              : 
    1920          603 :     int col_idx = table_col_index(table, col_name);
    1921          603 :     if (col_idx < 0 || table_col_algo(table, col_idx) != col_algo_block) {
    1922            0 :         dbsync_set_error(context, "Column %s in table %s is not configured as block-level.", col_name, table_name);
    1923            0 :         return;
    1924              :     }
    1925              : 
    1926              :     // Encode primary keys
    1927          603 :     int npks = table_count_pks(table);
    1928          603 :     if (argc - 2 != npks) {
    1929            0 :         sqlite3_result_error(context, "Wrong number of primary key values for cloudsync_text_materialize.", -1);
    1930            0 :         return;
    1931              :     }
    1932              : 
    1933              :     char buffer[1024];
    1934          603 :     size_t pklen = sizeof(buffer);
    1935          603 :     char *pk = pk_encode_prikey((dbvalue_t **)&argv[2], npks, buffer, &pklen);
    1936          603 :     if (!pk || pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
    1937            0 :         sqlite3_result_error(context, "Failed to encode primary key(s).", -1);
    1938            0 :         return;
    1939              :     }
    1940              : 
    1941              :     // Materialize the column
    1942          603 :     int rc = block_materialize_column(data, table, pk, (int)pklen, col_name);
    1943          603 :     if (rc != DBRES_OK) {
    1944          309 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
    1945          309 :         sqlite3_result_error_code(context, rc);
    1946          309 :     } else {
    1947          294 :         sqlite3_result_int(context, 1);
    1948              :     }
    1949              : 
    1950          603 :     if (pk != buffer) cloudsync_memory_free(pk);
    1951          603 : }
    1952              : 
    1953              : // MARK: - Row Filter -
    1954              : 
    1955           21 : void dbsync_set_filter (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1956              :     DEBUG_FUNCTION("cloudsync_set_filter");
    1957              : 
    1958           21 :     const char *tbl = (const char *)database_value_text(argv[0]);
    1959           21 :     const char *filter_expr = (const char *)database_value_text(argv[1]);
    1960           21 :     if (!tbl || !filter_expr) {
    1961            0 :         dbsync_set_error(context, "cloudsync_set_filter: table and filter expression required");
    1962            0 :         return;
    1963              :     }
    1964              : 
    1965           21 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1966              : 
    1967              :     // Guard against calling set_filter before the target table has been set
    1968              :     // up for sync: without this, we'd hit "no such table:
    1969              :     // cloudsync_table_settings" or "no such table: main.<tbl>" deep inside
    1970              :     // the trigger recreation path, which is not actionable.
    1971           21 :     if (!cloudsync_context_is_initialized(data) || !table_lookup(data, tbl)) {
    1972            2 :         dbsync_set_error(context,
    1973              :             "cloudsync_set_filter: table '%s' is not configured for sync. "
    1974            1 :             "Call SELECT cloudsync_init('%s') first.", tbl, tbl);
    1975            1 :         return;
    1976              :     }
    1977              : 
    1978              :     // Store filter in table settings
    1979           20 :     dbutils_table_settings_set_key_value(data, tbl, "*", "filter", filter_expr);
    1980              : 
    1981              :     // Read current algo
    1982           20 :     table_algo algo = dbutils_table_settings_get_algo(data, tbl);
    1983           20 :     if (algo == table_algo_none) algo = table_algo_crdt_cls;
    1984              : 
    1985              :     // Drop and recreate triggers with the filter
    1986           20 :     database_delete_triggers(data, tbl);
    1987           20 :     int rc = database_create_triggers(data, tbl, algo, filter_expr);
    1988           20 :     if (rc != DBRES_OK) {
    1989            0 :         dbsync_set_error(context, "cloudsync_set_filter: error recreating triggers");
    1990            0 :         sqlite3_result_error_code(context, rc);
    1991            0 :         return;
    1992              :     }
    1993              : 
    1994              :     // Clean and refill metatable with the new filter
    1995           20 :     rc = cloudsync_reset_metatable(data, tbl);
    1996           20 :     if (rc != DBRES_OK) {
    1997            0 :         dbsync_set_error(context, "cloudsync_set_filter: error resetting metatable");
    1998            0 :         sqlite3_result_error_code(context, rc);
    1999            0 :         return;
    2000              :     }
    2001              : 
    2002           20 :     sqlite3_result_int(context, 1);
    2003           21 : }
    2004              : 
    2005            5 : void dbsync_clear_filter (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2006              :     DEBUG_FUNCTION("cloudsync_clear_filter");
    2007              : 
    2008            5 :     const char *tbl = (const char *)database_value_text(argv[0]);
    2009            5 :     if (!tbl) {
    2010            0 :         dbsync_set_error(context, "cloudsync_clear_filter: table name required");
    2011            0 :         return;
    2012              :     }
    2013              : 
    2014            5 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2015              : 
    2016              :     // Guard against calling clear_filter before the target table has been set
    2017              :     // up for sync — see dbsync_set_filter for the same rationale.
    2018            5 :     if (!cloudsync_context_is_initialized(data) || !table_lookup(data, tbl)) {
    2019            2 :         dbsync_set_error(context,
    2020              :             "cloudsync_clear_filter: table '%s' is not configured for sync. "
    2021            1 :             "Call SELECT cloudsync_init('%s') first.", tbl, tbl);
    2022            1 :         return;
    2023              :     }
    2024              : 
    2025              :     // Remove filter from table settings (set to NULL/empty)
    2026            4 :     dbutils_table_settings_set_key_value(data, tbl, "*", "filter", NULL);
    2027              : 
    2028              :     // Read current algo
    2029            4 :     table_algo algo = dbutils_table_settings_get_algo(data, tbl);
    2030            4 :     if (algo == table_algo_none) algo = table_algo_crdt_cls;
    2031              : 
    2032              :     // Drop and recreate triggers without filter
    2033            4 :     database_delete_triggers(data, tbl);
    2034            4 :     int rc = database_create_triggers(data, tbl, algo, NULL);
    2035            4 :     if (rc != DBRES_OK) {
    2036            0 :         dbsync_set_error(context, "cloudsync_clear_filter: error recreating triggers");
    2037            0 :         sqlite3_result_error_code(context, rc);
    2038            0 :         return;
    2039              :     }
    2040              : 
    2041              :     // Clean and refill metatable without filter (all rows)
    2042            4 :     rc = cloudsync_reset_metatable(data, tbl);
    2043            4 :     if (rc != DBRES_OK) {
    2044            0 :         dbsync_set_error(context, "cloudsync_clear_filter: error resetting metatable");
    2045            0 :         sqlite3_result_error_code(context, rc);
    2046            0 :         return;
    2047              :     }
    2048              : 
    2049            4 :     sqlite3_result_int(context, 1);
    2050            5 : }
    2051              : 
    2052          772 : int dbsync_register_functions (sqlite3 *db, char **pzErrMsg) {
    2053          772 :     int rc = SQLITE_OK;
    2054              :     
    2055              :     // there's no built-in way to verify if sqlite3_cloudsync_init has already been called
    2056              :     // for this specific database connection, we use a workaround: we attempt to retrieve the
    2057              :     // cloudsync_version and check for an error, an error indicates that initialization has not been performed
    2058          772 :     if (sqlite3_exec(db, "SELECT cloudsync_version();", NULL, NULL, NULL) == SQLITE_OK) return SQLITE_OK;
    2059              :     
    2060              :     // init memory debugger (NOOP in production)
    2061              :     cloudsync_memory_init(1);
    2062              : 
    2063              :     // set fractional-indexing allocator to use cloudsync memory
    2064          770 :     block_init_allocator();
    2065              : 
    2066              :     // init context
    2067          770 :     void *ctx = cloudsync_context_create(db);
    2068          770 :     if (!ctx) {
    2069            0 :         if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Not enought memory to create a database context");
    2070            0 :         return SQLITE_NOMEM;
    2071              :     }
    2072              :     
    2073              :     // register functions
    2074              :     
    2075              :     // PUBLIC functions
    2076          770 :     rc = dbsync_register_pure_function(db, "cloudsync_version", dbsync_version, 0, pzErrMsg, ctx, cloudsync_context_free);
    2077          770 :     if (rc != SQLITE_OK) return rc;
    2078              :     
    2079          770 :     rc = dbsync_register_function(db, "cloudsync_init", dbsync_init1, 1, pzErrMsg, ctx, NULL);
    2080          770 :     if (rc != SQLITE_OK) return rc;
    2081              :     
    2082          770 :     rc = dbsync_register_function(db, "cloudsync_init", dbsync_init2, 2, pzErrMsg, ctx, NULL);
    2083          770 :     if (rc != SQLITE_OK) return rc;
    2084              :     
    2085          770 :     rc = dbsync_register_function(db, "cloudsync_init", dbsync_init3, 3, pzErrMsg, ctx, NULL);
    2086          770 :     if (rc != SQLITE_OK) return rc;
    2087              : 
    2088          770 :     rc = dbsync_register_function(db, "cloudsync_enable", dbsync_enable, 1, pzErrMsg, ctx, NULL);
    2089          770 :     if (rc != SQLITE_OK) return rc;
    2090              :     
    2091          770 :     rc = dbsync_register_function(db, "cloudsync_disable", dbsync_disable, 1, pzErrMsg, ctx, NULL);
    2092          770 :     if (rc != SQLITE_OK) return rc;
    2093              :     
    2094          770 :     rc = dbsync_register_function(db, "cloudsync_is_enabled", dbsync_is_enabled, 1, pzErrMsg, ctx, NULL);
    2095          770 :     if (rc != SQLITE_OK) return rc;
    2096              :     
    2097          770 :     rc = dbsync_register_function(db, "cloudsync_cleanup", dbsync_cleanup, 1, pzErrMsg, ctx, NULL);
    2098          770 :     if (rc != SQLITE_OK) return rc;
    2099              :     
    2100          770 :     rc = dbsync_register_function(db, "cloudsync_terminate", dbsync_terminate, 0, pzErrMsg, ctx, NULL);
    2101          770 :     if (rc != SQLITE_OK) return rc;
    2102              :     
    2103          770 :     rc = dbsync_register_function(db, "cloudsync_set", dbsync_set, 2, pzErrMsg, ctx, NULL);
    2104          770 :     if (rc != SQLITE_OK) return rc;
    2105              :     
    2106          770 :     rc = dbsync_register_function(db, "cloudsync_set_table", dbsync_set_table, 3, pzErrMsg, ctx, NULL);
    2107          770 :     if (rc != SQLITE_OK) return rc;
    2108              : 
    2109          770 :     rc = dbsync_register_function(db, "cloudsync_set_filter", dbsync_set_filter, 2, pzErrMsg, ctx, NULL);
    2110          770 :     if (rc != SQLITE_OK) return rc;
    2111              : 
    2112          770 :     rc = dbsync_register_function(db, "cloudsync_clear_filter", dbsync_clear_filter, 1, pzErrMsg, ctx, NULL);
    2113          770 :     if (rc != SQLITE_OK) return rc;
    2114              : 
    2115          770 :     rc = dbsync_register_function(db, "cloudsync_set_schema", dbsync_set_schema, 1, pzErrMsg, ctx, NULL);
    2116          770 :     if (rc != SQLITE_OK) return rc;
    2117              :     
    2118          770 :     rc = dbsync_register_function(db, "cloudsync_schema", dbsync_schema, 0, pzErrMsg, ctx, NULL);
    2119          770 :     if (rc != SQLITE_OK) return rc;
    2120              :     
    2121          770 :     rc = dbsync_register_function(db, "cloudsync_table_schema", dbsync_table_schema, 1, pzErrMsg, ctx, NULL);
    2122          770 :     if (rc != SQLITE_OK) return rc;
    2123              : 
    2124          770 :     rc = dbsync_register_function(db, "cloudsync_set_column", dbsync_set_column, 4, pzErrMsg, ctx, NULL);
    2125          770 :     if (rc != SQLITE_OK) return rc;
    2126              :     
    2127          770 :     rc = dbsync_register_function(db, "cloudsync_siteid", dbsync_siteid, 0, pzErrMsg, ctx, NULL);
    2128          770 :     if (rc != SQLITE_OK) return rc;
    2129              :     
    2130          770 :     rc = dbsync_register_function(db, "cloudsync_db_version", dbsync_db_version, 0, pzErrMsg, ctx, NULL);
    2131          770 :     if (rc != SQLITE_OK) return rc;
    2132              :     
    2133          770 :     rc = dbsync_register_function(db, "cloudsync_db_version_next", dbsync_db_version_next, 0, pzErrMsg, ctx, NULL);
    2134          770 :     if (rc != SQLITE_OK) return rc;
    2135              :     
    2136          770 :     rc = dbsync_register_function(db, "cloudsync_db_version_next", dbsync_db_version_next, 1, pzErrMsg, ctx, NULL);
    2137          770 :     if (rc != SQLITE_OK) return rc;
    2138              :     
    2139          770 :     rc = dbsync_register_function(db, "cloudsync_begin_alter", dbsync_begin_alter, 1, pzErrMsg, ctx, NULL);
    2140          770 :     if (rc != SQLITE_OK) return rc;
    2141              :     
    2142          770 :     rc = dbsync_register_function(db, "cloudsync_commit_alter", dbsync_commit_alter, 1, pzErrMsg, ctx, NULL);
    2143          770 :     if (rc != SQLITE_OK) return rc;
    2144              :     
    2145          770 :     rc = dbsync_register_function(db, "cloudsync_uuid", dbsync_uuid, 0, pzErrMsg, ctx, NULL);
    2146          770 :     if (rc != SQLITE_OK) return rc;
    2147              : 
    2148          770 :     rc = dbsync_register_function(db, "cloudsync_uuid_text", dbsync_uuid_text, 1, pzErrMsg, ctx, NULL);
    2149          770 :     if (rc != SQLITE_OK) return rc;
    2150          770 :     rc = dbsync_register_function(db, "cloudsync_uuid_text", dbsync_uuid_text, 2, pzErrMsg, ctx, NULL);
    2151          770 :     if (rc != SQLITE_OK) return rc;
    2152          770 :     rc = dbsync_register_function(db, "cloudsync_uuid_blob", dbsync_uuid_blob, 1, pzErrMsg, ctx, NULL);
    2153          770 :     if (rc != SQLITE_OK) return rc;
    2154              : 
    2155              :     // PAYLOAD
    2156          770 :     rc = dbsync_register_aggregate(db, "cloudsync_payload_encode", dbsync_payload_encode_step, dbsync_payload_encode_final, -1, pzErrMsg, ctx, NULL);
    2157          770 :     if (rc != SQLITE_OK) return rc;
    2158              :     
    2159              :     // alias
    2160          770 :     rc = dbsync_register_function(db, "cloudsync_payload_decode", dbsync_payload_decode, -1, pzErrMsg, ctx, NULL);
    2161          770 :     if (rc != SQLITE_OK) return rc;
    2162          770 :     rc = dbsync_register_function(db, "cloudsync_payload_apply", dbsync_payload_decode, -1, pzErrMsg, ctx, NULL);
    2163          770 :     if (rc != SQLITE_OK) return rc;
    2164              : 
    2165          770 :     rc = sqlite3_create_module(db, "cloudsync_payload_chunks", &cloudsync_payload_chunks_module, (void *)ctx);
    2166          770 :     if (rc != SQLITE_OK) return rc;
    2167              : 
    2168          770 :     rc = dbsync_register_function(db, "cloudsync_payload_blob_checked", dbsync_payload_blob_checked, 5, pzErrMsg, ctx, NULL);
    2169          770 :     if (rc != SQLITE_OK) return rc;
    2170              : 
    2171              :     #ifdef CLOUDSYNC_DESKTOP_OS
    2172          770 :     rc = dbsync_register_function(db, "cloudsync_payload_save", dbsync_payload_save, 1, pzErrMsg, ctx, NULL);
    2173          770 :     if (rc != SQLITE_OK) return rc;
    2174              :     
    2175          770 :     rc = dbsync_register_function(db, "cloudsync_payload_load", dbsync_payload_load, 1, pzErrMsg, ctx, NULL);
    2176          770 :     if (rc != SQLITE_OK) return rc;
    2177              :     #endif
    2178              :     
    2179              :     // PRIVATE functions (used inside triggers — require SQLITE_INNOCUOUS)
    2180          770 :     rc = dbsync_register_trigger_function(db, "cloudsync_is_sync", dbsync_is_sync, 1, pzErrMsg, ctx, NULL);
    2181          770 :     if (rc != SQLITE_OK) return rc;
    2182              : 
    2183          770 :     rc = dbsync_register_trigger_function(db, "cloudsync_insert", dbsync_insert, -1, pzErrMsg, ctx, NULL);
    2184          770 :     if (rc != SQLITE_OK) return rc;
    2185              : 
    2186          770 :     rc = dbsync_register_trigger_aggregate(db, "cloudsync_update", dbsync_update_step, dbsync_update_final, 3, pzErrMsg, ctx, NULL);
    2187          770 :     if (rc != SQLITE_OK) return rc;
    2188              : 
    2189          770 :     rc = dbsync_register_trigger_function(db, "cloudsync_delete", dbsync_delete, -1, pzErrMsg, ctx, NULL);
    2190          770 :     if (rc != SQLITE_OK) return rc;
    2191              :     
    2192          770 :     rc = dbsync_register_function(db, "cloudsync_col_value", dbsync_col_value, 3, pzErrMsg, ctx, NULL);
    2193          770 :     if (rc != SQLITE_OK) return rc;
    2194              :     
    2195          770 :     rc = dbsync_register_pure_function(db, "cloudsync_pk_encode", dbsync_pk_encode, -1, pzErrMsg, ctx, NULL);
    2196          770 :     if (rc != SQLITE_OK) return rc;
    2197              : 
    2198          770 :     rc = dbsync_register_pure_function(db, "cloudsync_pk_decode", dbsync_pk_decode, 2, pzErrMsg, ctx, NULL);
    2199          770 :     if (rc != SQLITE_OK) return rc;
    2200              :     
    2201          770 :     rc = dbsync_register_function(db, "cloudsync_seq", dbsync_seq, 0, pzErrMsg, ctx, NULL);
    2202          770 :     if (rc != SQLITE_OK) return rc;
    2203              : 
    2204          770 :     rc = dbsync_register_function(db, "cloudsync_text_materialize", dbsync_text_materialize, -1, pzErrMsg, ctx, NULL);
    2205          770 :     if (rc != SQLITE_OK) return rc;
    2206              : 
    2207              :     // NETWORK LAYER
    2208              :     #ifndef CLOUDSYNC_OMIT_NETWORK
    2209           20 :     rc = cloudsync_network_register(db, pzErrMsg, ctx);
    2210           20 :     if (rc != SQLITE_OK) return rc;
    2211              :     #endif
    2212              :     
    2213          770 :     cloudsync_context *data = (cloudsync_context *)ctx;
    2214          770 :     sqlite3_commit_hook(db, cloudsync_commit_hook, ctx);
    2215          770 :     sqlite3_rollback_hook(db, cloudsync_rollback_hook, ctx);
    2216              :     
    2217              :     // register eponymous only changes virtual table
    2218          770 :     rc = cloudsync_vtab_register_changes (db, data);
    2219          770 :     if (rc != SQLITE_OK) {
    2220            0 :         if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Error creating changes virtual table: %s", sqlite3_errmsg(db));
    2221            0 :         return rc;
    2222              :     }
    2223              :     
    2224              :     // load config, if exists
    2225          770 :     if (cloudsync_config_exists(data)) {
    2226            6 :         if (cloudsync_context_init(data) == NULL) {
    2227              :             // Do not free ctx here: it is already owned by the cloudsync_version
    2228              :             // function (registered above with cloudsync_context_free as its
    2229              :             // destructor). SQLite will release it when the connection is closed.
    2230              :             // Freeing it manually would cause a double-free on sqlite3_close.
    2231            0 :             if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("An error occurred while trying to initialize context");
    2232            0 :             return SQLITE_ERROR;
    2233              :         }
    2234              :         
    2235              :         // update schema hash if upgrading from an older version
    2236            6 :         if (dbutils_settings_check_version(data, NULL) != 0) {
    2237            0 :             cloudsync_update_schema_hash(data);
    2238            0 :         }
    2239              : 
    2240              :         // make sure to update internal version to current version
    2241            6 :         dbutils_settings_set_key_value(data, CLOUDSYNC_KEY_LIBVERSION, CLOUDSYNC_VERSION);
    2242            6 :     }
    2243              :     
    2244          770 :     return SQLITE_OK;
    2245          772 : }
    2246              : 
    2247              : // MARK: - Main Entrypoint -
    2248              : 
    2249          772 : APIEXPORT int sqlite3_cloudsync_init (sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
    2250              :     DEBUG_FUNCTION("sqlite3_cloudsync_init");
    2251              :     
    2252              :     #ifndef SQLITE_CORE
    2253              :     SQLITE_EXTENSION_INIT2(pApi);
    2254              :     #endif
    2255              :     
    2256          772 :     return dbsync_register_functions(db, pzErrMsg);
    2257              : }
        

Generated by: LCOV version 2.5-0