LCOV - code coverage report
Current view: top level - src/sqlite - cloudsync_sqlite.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 86.2 % 1388 1197
Test Date: 2026-07-17 12:57:54 Functions: 96.0 % 75 72

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

Generated by: LCOV version 2.5-0