LCOV - code coverage report
Current view: top level - src/sqlite - cloudsync_sqlite.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 82.6 % 891 736
Test Date: 2026-05-26 15:22:06 Functions: 96.2 % 53 51

            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              : 
      16              : #ifndef CLOUDSYNC_OMIT_NETWORK
      17              : #include "../network/network.h"
      18              : #endif
      19              : 
      20              : #ifndef SQLITE_CORE
      21              : SQLITE_EXTENSION_INIT1
      22              : #endif
      23              : 
      24              : #ifndef UNUSED_PARAMETER
      25              : #define UNUSED_PARAMETER(X) (void)(X)
      26              : #endif
      27              : 
      28              : #ifdef _WIN32
      29              : #define APIEXPORT   __declspec(dllexport)
      30              : #else
      31              : #define APIEXPORT
      32              : #endif
      33              : 
      34              : typedef struct {
      35              :     sqlite3_context *context;
      36              :     int             index;
      37              : } cloudsync_pk_decode_context;
      38              : 
      39              : typedef struct {
      40              :     sqlite3_value   *table_name;
      41              :     sqlite3_value   **new_values;
      42              :     sqlite3_value   **old_values;
      43              :     int             count;
      44              :     int             capacity;
      45              : } cloudsync_update_payload;
      46              : 
      47            5 : void dbsync_set_error (sqlite3_context *context, const char *format, ...) {
      48              :     char buffer[2048];
      49              :     
      50              :     va_list arg;
      51            5 :     va_start (arg, format);
      52            5 :     vsnprintf(buffer, sizeof(buffer), format, arg);
      53            5 :     va_end (arg);
      54              :     
      55            5 :     if (context) sqlite3_result_error(context, buffer, -1);
      56            5 : }
      57              : 
      58              : // MARK: - Public -
      59              : 
      60            8 : void dbsync_version (sqlite3_context *context, int argc, sqlite3_value **argv) {
      61              :     DEBUG_FUNCTION("cloudsync_version");
      62            8 :     UNUSED_PARAMETER(argc);
      63            8 :     UNUSED_PARAMETER(argv);
      64            8 :     sqlite3_result_text(context, CLOUDSYNC_VERSION, -1, SQLITE_STATIC);
      65            8 : }
      66              : 
      67          536 : void dbsync_siteid (sqlite3_context *context, int argc, sqlite3_value **argv) {
      68              :     DEBUG_FUNCTION("cloudsync_siteid");
      69          536 :     UNUSED_PARAMETER(argc);
      70          536 :     UNUSED_PARAMETER(argv);
      71              :     
      72          536 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
      73          536 :     sqlite3_result_blob(context, cloudsync_siteid(data), UUID_LEN, SQLITE_STATIC);
      74          536 : }
      75              : 
      76            3 : void dbsync_db_version (sqlite3_context *context, int argc, sqlite3_value **argv) {
      77              :     DEBUG_FUNCTION("cloudsync_db_version");
      78            3 :     UNUSED_PARAMETER(argc);
      79            3 :     UNUSED_PARAMETER(argv);
      80              : 
      81              :     // retrieve context
      82            3 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
      83              : 
      84            3 :     int rc = cloudsync_dbversion_check_uptodate(data);
      85            3 :     if (rc != SQLITE_OK) {
      86              :         // When cloudsync_init was never called, data_version_stmt is NULL and
      87              :         // database_errmsg() falls back to "not an error", producing the
      88              :         // confusing "Unable to retrieve db_version (not an error)". Detect the
      89              :         // uninitialized state and return an actionable message instead. The
      90              :         // extra check only runs on the error branch, so it costs nothing on
      91              :         // the sync hot path (merge operations keep going through the normal
      92              :         // path where rc == SQLITE_OK).
      93            1 :         if (!cloudsync_context_is_initialized(data)) {
      94            1 :             dbsync_set_error(context,
      95              :                 "cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
      96              :                 "to enable sync on a table before calling cloudsync_db_version().");
      97            1 :         } else {
      98            0 :             dbsync_set_error(context, "Unable to retrieve db_version (%s).", database_errmsg(data));
      99              :         }
     100            1 :         return;
     101              :     }
     102              : 
     103            2 :     sqlite3_result_int64(context, cloudsync_dbversion(data));
     104            3 : }
     105              : 
     106        25143 : void dbsync_db_version_next (sqlite3_context *context, int argc, sqlite3_value **argv) {
     107              :     DEBUG_FUNCTION("cloudsync_db_version_next");
     108              : 
     109              :     // retrieve context
     110        25143 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     111              : 
     112        25143 :     sqlite3_int64 merging_version = (argc == 1) ? database_value_int(argv[0]) : CLOUDSYNC_VALUE_NOTSET;
     113        25143 :     sqlite3_int64 value = cloudsync_dbversion_next(data, merging_version);
     114        25143 :     if (value == -1) {
     115            1 :         if (!cloudsync_context_is_initialized(data)) {
     116            1 :             dbsync_set_error(context,
     117              :                 "cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
     118              :                 "to enable sync on a table before calling cloudsync_db_version_next().");
     119            1 :         } else {
     120            0 :             dbsync_set_error(context, "Unable to retrieve next_db_version (%s).", database_errmsg(data));
     121              :         }
     122            1 :         return;
     123              :     }
     124              : 
     125        25142 :     sqlite3_result_int64(context, value);
     126        25143 : }
     127              : 
     128           58 : void dbsync_seq (sqlite3_context *context, int argc, sqlite3_value **argv) {
     129              :     DEBUG_FUNCTION("cloudsync_seq");
     130              :     
     131              :     // retrieve context
     132           58 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     133           58 :     sqlite3_result_int(context, cloudsync_bumpseq(data));
     134           58 : }
     135              : 
     136            5 : void dbsync_uuid (sqlite3_context *context, int argc, sqlite3_value **argv) {
     137              :     DEBUG_FUNCTION("cloudsync_uuid");
     138              :     
     139              :     char value[UUID_STR_MAXLEN];
     140            5 :     char *uuid = cloudsync_uuid_v7_string(value, true);
     141            5 :     sqlite3_result_text(context, uuid, -1, SQLITE_TRANSIENT);
     142            5 : }
     143              : 
     144              : // MARK: -
     145              : 
     146            2 : void dbsync_set (sqlite3_context *context, int argc, sqlite3_value **argv) {
     147              :     DEBUG_FUNCTION("cloudsync_set");
     148              :     
     149              :     // sanity check parameters
     150            2 :     const char *key = (const char *)database_value_text(argv[0]);
     151            2 :     const char *value = (const char *)database_value_text(argv[1]);
     152              :     
     153              :     // silently fails
     154            2 :     if (key == NULL) return;
     155              :     
     156            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     157            2 :     dbutils_settings_set_key_value(data, key, value);
     158            2 : }
     159              : 
     160           78 : void dbsync_set_column (sqlite3_context *context, int argc, sqlite3_value **argv) {
     161              :     DEBUG_FUNCTION("cloudsync_set_column");
     162              : 
     163           78 :     const char *tbl = (const char *)database_value_text(argv[0]);
     164           78 :     const char *col = (const char *)database_value_text(argv[1]);
     165           78 :     const char *key = (const char *)database_value_text(argv[2]);
     166           78 :     const char *value = (const char *)database_value_text(argv[3]);
     167              : 
     168           78 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     169              : 
     170              :     // Handle block column setup: cloudsync_set_column('tbl', 'col', 'algo', 'block')
     171           78 :     if (key && value && strcmp(key, "algo") == 0 && strcmp(value, "block") == 0) {
     172           73 :         int rc = cloudsync_setup_block_column(data, tbl, col, NULL, true);
     173           73 :         if (rc != DBRES_OK) {
     174            0 :             sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     175            0 :         }
     176           73 :         return;
     177              :     }
     178              : 
     179              :     // Handle delimiter setting: cloudsync_set_column('tbl', 'col', 'delimiter', '\n\n')
     180            5 :     if (key && strcmp(key, "delimiter") == 0) {
     181            3 :         cloudsync_table_context *table = table_lookup(data, tbl);
     182            3 :         if (table) {
     183            3 :             int col_idx = table_col_index(table, col);
     184            3 :             if (col_idx >= 0 && table_col_algo(table, col_idx) == col_algo_block) {
     185            3 :                 table_set_col_delimiter(table, col_idx, value);
     186            3 :             }
     187            3 :         }
     188            3 :     }
     189              : 
     190            5 :     dbutils_table_settings_set_key_value(data, tbl, col, key, value);
     191           78 : }
     192              : 
     193            2 : void dbsync_set_table (sqlite3_context *context, int argc, sqlite3_value **argv) {
     194              :     DEBUG_FUNCTION("cloudsync_set_table");
     195              :     
     196            2 :     const char *tbl = (const char *)database_value_text(argv[0]);
     197            2 :     const char *key = (const char *)database_value_text(argv[1]);
     198            2 :     const char *value = (const char *)database_value_text(argv[2]);
     199              :     
     200            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     201            2 :     dbutils_table_settings_set_key_value(data, tbl, "*", key, value);
     202            2 : }
     203              : 
     204            2 : void dbsync_set_schema (sqlite3_context *context, int argc, sqlite3_value **argv) {
     205              :     DEBUG_FUNCTION("dbsync_set_schema");
     206              :     
     207            2 :     const char *schema = (const char *)database_value_text(argv[0]);
     208            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     209            2 :     cloudsync_set_schema(data, schema);
     210            2 : }
     211              : 
     212            2 : void dbsync_schema (sqlite3_context *context, int argc, sqlite3_value **argv) {
     213              :     DEBUG_FUNCTION("dbsync_schema");
     214              :     
     215            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     216            2 :     const char *schema = cloudsync_schema(data);
     217            2 :     (schema) ? sqlite3_result_text(context, schema, -1, NULL) : sqlite3_result_null(context);
     218            2 : }
     219              : 
     220            2 : void dbsync_table_schema (sqlite3_context *context, int argc, sqlite3_value **argv) {
     221              :     DEBUG_FUNCTION("dbsync_table_schema");
     222              :     
     223            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     224            2 :     const char *table_name = (const char *)database_value_text(argv[0]);
     225            2 :     const char *schema = cloudsync_table_schema(data, table_name);
     226            2 :     (schema) ? sqlite3_result_text(context, schema, -1, NULL) : sqlite3_result_null(context);
     227            2 : }
     228              : 
     229        12670 : void dbsync_is_sync (sqlite3_context *context, int argc, sqlite3_value **argv) {
     230              :     DEBUG_FUNCTION("cloudsync_is_sync");
     231              :     
     232        12670 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     233        12670 :     if (cloudsync_insync(data)) {
     234         8651 :         sqlite3_result_int(context, 1);
     235         8651 :         return;
     236              :     }
     237              :     
     238         4019 :     const char *table_name = (const char *)database_value_text(argv[0]);
     239         4019 :     cloudsync_table_context *table = table_lookup(data, table_name);
     240         4019 :     sqlite3_result_int(context, (table) ? (table_enabled(table) == 0) : 0);
     241        12670 : }
     242              : 
     243       126856 : void dbsync_col_value (sqlite3_context *context, int argc, sqlite3_value **argv) {
     244              :     // DEBUG_FUNCTION("cloudsync_col_value");
     245              :     
     246              :     // argv[0] -> table name
     247              :     // argv[1] -> column name
     248              :     // argv[2] -> encoded pk
     249              :     
     250              :     // retrieve column name
     251       126856 :     const char *col_name = (const char *)database_value_text(argv[1]);
     252       126856 :     if (!col_name) {
     253            0 :         dbsync_set_error(context, "Column name cannot be NULL");
     254            0 :         return;
     255              :     }
     256              :     
     257              :     // check for special tombstone value
     258       126856 :     if (strcmp(col_name, CLOUDSYNC_TOMBSTONE_VALUE) == 0) {
     259         4104 :         sqlite3_result_null(context);
     260         4104 :         return;
     261              :     }
     262              : 
     263              :     // lookup table
     264       122752 :     const char *table_name = (const char *)database_value_text(argv[0]);
     265       122752 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     266       122752 :     cloudsync_table_context *table = table_lookup(data, table_name);
     267       122752 :     if (!table) {
     268            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in clousdsync_colvalue.", table_name);
     269            0 :         return;
     270              :     }
     271              : 
     272              :     // Block column: if col_name contains \x1F, read from blocks table
     273       122752 :     if (block_is_block_colname(col_name) && table_has_block_cols(table)) {
     274          981 :         dbvm_t *bvm = table_block_value_read_stmt(table);
     275          981 :         if (!bvm) {
     276            0 :             sqlite3_result_null(context);
     277            0 :             return;
     278              :         }
     279          981 :         int rc = databasevm_bind_blob(bvm, 1, database_value_blob(argv[2]), database_value_bytes(argv[2]));
     280          981 :         if (rc != DBRES_OK) { databasevm_reset(bvm); sqlite3_result_error(context, database_errmsg(data), -1); return; }
     281          981 :         rc = databasevm_bind_text(bvm, 2, col_name, -1);
     282          981 :         if (rc != DBRES_OK) { databasevm_reset(bvm); sqlite3_result_error(context, database_errmsg(data), -1); return; }
     283              : 
     284          981 :         rc = databasevm_step(bvm);
     285          981 :         if (rc == SQLITE_ROW) {
     286          902 :             sqlite3_result_value(context, database_column_value(bvm, 0));
     287          981 :         } else if (rc == SQLITE_DONE) {
     288           79 :             sqlite3_result_null(context);
     289           79 :         } else {
     290            0 :             sqlite3_result_error(context, database_errmsg(data), -1);
     291              :         }
     292          981 :         databasevm_reset(bvm);
     293          981 :         return;
     294              :     }
     295              : 
     296              :     // extract the right col_value vm associated to the column name
     297       121771 :     sqlite3_stmt *vm = table_column_lookup(table, col_name, false, NULL);
     298       121771 :     if (!vm) {
     299            0 :         sqlite3_result_error(context, "Unable to retrieve column value precompiled statement in clousdsync_colvalue.", -1);
     300            0 :         return;
     301              :     }
     302              : 
     303              :     // bind primary key values
     304       121771 :     int rc = pk_decode_prikey((char *)database_value_blob(argv[2]), (size_t)database_value_bytes(argv[2]), pk_decode_bind_callback, (void *)vm);
     305       121771 :     if (rc < 0) goto cleanup;
     306              : 
     307              :     // execute vm
     308       121771 :     rc = databasevm_step(vm);
     309       243542 :     if (rc == SQLITE_DONE) {
     310            0 :         rc = SQLITE_OK;
     311            0 :         sqlite3_result_text(context, CLOUDSYNC_RLS_RESTRICTED_VALUE, -1, SQLITE_STATIC);
     312       121771 :     } else if (rc == SQLITE_ROW) {
     313              :         // store value result
     314       121771 :         rc = SQLITE_OK;
     315       121771 :         sqlite3_result_value(context, database_column_value(vm, 0));
     316       121771 :     }
     317              : 
     318              : cleanup:
     319       121771 :     if (rc != SQLITE_OK) {
     320            0 :         sqlite3_result_error(context, database_errmsg(data), -1);
     321            0 :     }
     322       121771 :     databasevm_reset(vm);
     323       126856 : }
     324              : 
     325        10569 : void dbsync_pk_encode (sqlite3_context *context, int argc, sqlite3_value **argv) {
     326        10569 :     size_t bsize = 0;
     327        10569 :     char *buffer = pk_encode_prikey((dbvalue_t **)argv, argc, NULL, &bsize);
     328        10569 :     if (!buffer || buffer == PRIKEY_NULL_CONSTRAINT_ERROR) {
     329            1 :         sqlite3_result_null(context);
     330            1 :         return;
     331              :     }
     332        10568 :     sqlite3_result_blob(context, (const void *)buffer, (int)bsize, SQLITE_TRANSIENT);
     333        10568 :     cloudsync_memory_free(buffer);
     334        10569 : }
     335              : 
     336           19 : int dbsync_pk_decode_set_result_callback (void *xdata, int index, int type, int64_t ival, double dval, char *pval) {
     337           19 :     cloudsync_pk_decode_context *decode_context = (cloudsync_pk_decode_context *)xdata;
     338              :     // decode_context->index is 1 based
     339              :     // index is 0 based
     340           19 :     if (decode_context->index != index+1) return SQLITE_OK;
     341              :     
     342            7 :     int rc = 0;
     343            7 :     sqlite3_context *context = decode_context->context;
     344            7 :     switch (type) {
     345              :         case SQLITE_INTEGER:
     346            3 :             sqlite3_result_int64(context, ival);
     347            3 :             break;
     348              : 
     349              :         case SQLITE_FLOAT:
     350            2 :             sqlite3_result_double(context, dval);
     351            2 :             break;
     352              : 
     353              :         case SQLITE_NULL:
     354            0 :             sqlite3_result_null(context);
     355            0 :             break;
     356              : 
     357              :         case SQLITE_TEXT:
     358            1 :             sqlite3_result_text(context, pval, (int)ival, SQLITE_TRANSIENT);
     359            1 :             break;
     360              : 
     361              :         case SQLITE_BLOB:
     362            1 :             sqlite3_result_blob(context, pval, (int)ival, SQLITE_TRANSIENT);
     363            1 :             break;
     364              :     }
     365              :     
     366            7 :     return rc;
     367           19 : }
     368              : 
     369              : 
     370            7 : void dbsync_pk_decode (sqlite3_context *context, int argc, sqlite3_value **argv) {
     371            7 :     const char *pk = (const char *)database_value_blob(argv[0]);
     372            7 :     int pk_len = database_value_bytes(argv[0]);
     373            7 :     int i = (int)database_value_int(argv[1]);
     374              :     
     375            7 :     cloudsync_pk_decode_context xdata = {.context = context, .index = i};
     376            7 :     pk_decode_prikey((char *)pk, (size_t)pk_len, dbsync_pk_decode_set_result_callback, &xdata);
     377            7 : }
     378              : 
     379              : // MARK: -
     380              : 
     381         3587 : void dbsync_insert (sqlite3_context *context, int argc, sqlite3_value **argv) {
     382              :     DEBUG_FUNCTION("cloudsync_insert %s", database_value_text(argv[0]));
     383              :     // debug_values(argc-1, &argv[1]);
     384              :     
     385              :     // argv[0] is table name
     386              :     // argv[1]..[N] is primary key(s)
     387              :     
     388              :     // table_cloudsync
     389              :     // pk               -> encode(argc-1, &argv[1])
     390              :     // col_name         -> name
     391              :     // col_version      -> 0/1 +1
     392              :     // db_version       -> check
     393              :     // site_id          0
     394              :     // seq              -> sqlite_master
     395              :     
     396              :     // retrieve context
     397         3587 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     398              :     
     399              :     // lookup table
     400         3587 :     const char *table_name = (const char *)database_value_text(argv[0]);
     401         3587 :     cloudsync_table_context *table = table_lookup(data, table_name);
     402         3587 :     if (!table) {
     403            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_insert.", table_name);
     404            0 :         return;
     405              :     }
     406              :     
     407              :     // encode the primary key values into a buffer
     408              :     char buffer[1024];
     409         3587 :     size_t pklen = sizeof(buffer);
     410         3587 :     char *pk = pk_encode_prikey((dbvalue_t **)&argv[1], table_count_pks(table), buffer, &pklen);
     411         3587 :     if (!pk) {
     412            0 :         sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
     413            0 :         return;
     414              :     }
     415         3587 :     if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
     416            1 :         dbsync_set_error(context, "Insert aborted because primary key in table %s contains NULL values.", table_name);
     417            1 :         return;
     418              :     }
     419              :     
     420              :     // compute the next database version for tracking changes
     421         3586 :     int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
     422              :     
     423              :     // check if a row with the same primary key already exists
     424              :     // if so, this means the row might have been previously deleted (sentinel)
     425         3586 :     bool pk_exists = table_pk_exists(table, pk, pklen);
     426         3586 :     int rc = SQLITE_OK;
     427              :     
     428         3586 :     if (table_count_cols(table) == 0) {
     429              :         // if there are no columns other than primary keys, insert a sentinel record
     430           97 :         rc = local_mark_insert_sentinel_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
     431           97 :         if (rc != SQLITE_OK) goto cleanup;
     432         3586 :     } else if (pk_exists){
     433              :         // if a row with the same primary key already exists, update the sentinel record
     434            4 :         rc = local_update_sentinel(table, pk, pklen, db_version, cloudsync_bumpseq(data));
     435            4 :         if (rc != SQLITE_OK) goto cleanup;
     436            4 :     }
     437              :     
     438              :     // process each non-primary key column for insert or update
     439        14395 :     for (int i=0; i<table_count_cols(table); ++i) {
     440        10809 :         if (table_col_algo(table, i) == col_algo_block) {
     441              :             // Block column: read value from base table, split into blocks, store each block
     442           44 :             sqlite3_stmt *val_vm = (sqlite3_stmt *)table_column_lookup(table, table_colname(table, i), false, NULL);
     443           44 :             if (!val_vm) goto cleanup;
     444              : 
     445           44 :             rc = pk_decode_prikey(pk, pklen, pk_decode_bind_callback, (void *)val_vm);
     446           44 :             if (rc < 0) { databasevm_reset((dbvm_t *)val_vm); goto cleanup; }
     447              : 
     448           44 :             rc = databasevm_step((dbvm_t *)val_vm);
     449           44 :             if (rc == DBRES_ROW) {
     450           44 :                 const char *text = database_column_text((dbvm_t *)val_vm, 0);
     451           44 :                 const char *delim = table_col_delimiter(table, i);
     452           44 :                 const char *col = table_colname(table, i);
     453              : 
     454           44 :                 block_list_t *blocks = block_split(text ? text : "", delim);
     455           44 :                 if (blocks) {
     456           44 :                     char **positions = block_initial_positions(blocks->count);
     457           44 :                     if (positions) {
     458          360 :                         for (int b = 0; b < blocks->count; b++) {
     459          316 :                             char *block_cn = block_build_colname(col, positions[b]);
     460          316 :                             if (block_cn) {
     461          316 :                                 rc = local_mark_insert_or_update_meta(table, pk, pklen, block_cn, db_version, cloudsync_bumpseq(data));
     462              : 
     463              :                                 // Store block value in blocks table
     464          316 :                                 dbvm_t *wvm = table_block_value_write_stmt(table);
     465          316 :                                 if (wvm && rc == SQLITE_OK) {
     466          316 :                                     databasevm_bind_blob(wvm, 1, pk, (int)pklen);
     467          316 :                                     databasevm_bind_text(wvm, 2, block_cn, -1);
     468          316 :                                     databasevm_bind_text(wvm, 3, blocks->entries[b].content, -1);
     469          316 :                                     databasevm_step(wvm);
     470          316 :                                     databasevm_reset(wvm);
     471          316 :                                 }
     472              : 
     473          316 :                                 cloudsync_memory_free(block_cn);
     474          316 :                             }
     475          316 :                             cloudsync_memory_free(positions[b]);
     476          316 :                             if (rc != SQLITE_OK) break;
     477          316 :                         }
     478           44 :                         cloudsync_memory_free(positions);
     479           44 :                     }
     480           44 :                     block_list_free(blocks);
     481           44 :                 }
     482           44 :             }
     483           44 :             databasevm_reset((dbvm_t *)val_vm);
     484           44 :             if (rc == DBRES_ROW || rc == DBRES_DONE) rc = SQLITE_OK;
     485           44 :             if (rc != SQLITE_OK) goto cleanup;
     486           44 :         } else {
     487              :             // Regular column: mark as inserted or updated in the metadata
     488        10765 :             rc = local_mark_insert_or_update_meta(table, pk, pklen, table_colname(table, i), db_version, cloudsync_bumpseq(data));
     489        10765 :             if (rc != SQLITE_OK) goto cleanup;
     490              :         }
     491        14395 :     }
     492              : 
     493              : cleanup:
     494         3586 :     if (rc != SQLITE_OK) sqlite3_result_error(context, database_errmsg(data), -1);
     495              :     // free memory if the primary key was dynamically allocated
     496         3586 :     if (pk != buffer) cloudsync_memory_free(pk);
     497         3587 : }
     498              : 
     499           36 : void dbsync_delete (sqlite3_context *context, int argc, sqlite3_value **argv) {
     500              :     DEBUG_FUNCTION("cloudsync_delete %s", database_value_text(argv[0]));
     501              :     // debug_values(argc-1, &argv[1]);
     502              :     
     503              :     // retrieve context
     504           36 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     505              :     
     506              :     // lookup table
     507           36 :     const char *table_name = (const char *)database_value_text(argv[0]);
     508           36 :     cloudsync_table_context *table = table_lookup(data, table_name);
     509           36 :     if (!table) {
     510            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_delete.", table_name);
     511            0 :         return;
     512              :     }
     513              :     
     514              :     // compute the next database version for tracking changes
     515           36 :     int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
     516           36 :     int rc = SQLITE_OK;
     517              :     
     518              :     // encode the primary key values into a buffer
     519              :     char buffer[1024];
     520           36 :     size_t pklen = sizeof(buffer);
     521           36 :     char *pk = pk_encode_prikey((dbvalue_t **)&argv[1], table_count_pks(table), buffer, &pklen);
     522           36 :     if (!pk) {
     523            0 :         sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
     524            0 :         return;
     525              :     }
     526              :     
     527           36 :     if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
     528            0 :         dbsync_set_error(context, "Delete aborted because primary key in table %s contains NULL values.", table_name);
     529            0 :         return;
     530              :     }
     531              :     
     532              :     // mark the row as deleted by inserting a delete sentinel into the metadata
     533           36 :     rc = local_mark_delete_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
     534           36 :     if (rc != SQLITE_OK) goto cleanup;
     535              :     
     536              :     // remove any metadata related to the old rows associated with this primary key
     537           36 :     rc = local_drop_meta(table, pk, pklen);
     538           36 :     if (rc != SQLITE_OK) goto cleanup;
     539              :     
     540              : cleanup:
     541           36 :     if (rc != SQLITE_OK) sqlite3_result_error(context, database_errmsg(data), -1);
     542              :     // free memory if the primary key was dynamically allocated
     543           36 :     if (pk != buffer) cloudsync_memory_free(pk);
     544           36 : }
     545              : 
     546              : // MARK: -
     547              : 
     548          444 : void dbsync_update_payload_free (cloudsync_update_payload *payload) {
     549         2648 :     for (int i=0; i<payload->count; i++) {
     550         2204 :         database_value_free(payload->new_values[i]);
     551         2204 :         database_value_free(payload->old_values[i]);
     552         2204 :     }
     553          444 :     cloudsync_memory_free(payload->new_values);
     554          444 :     cloudsync_memory_free(payload->old_values);
     555          444 :     database_value_free(payload->table_name);
     556          444 :     payload->new_values = NULL;
     557          444 :     payload->old_values = NULL;
     558          444 :     payload->table_name = NULL;
     559          444 :     payload->count = 0;
     560          444 :     payload->capacity = 0;
     561          444 : }
     562              : 
     563         2204 : int dbsync_update_payload_append (cloudsync_update_payload *payload, sqlite3_value *v1, sqlite3_value *v2, sqlite3_value *v3) {
     564         2204 :     if (payload->count >= payload->capacity) {
     565          447 :         int newcap = payload->capacity ? payload->capacity * 2 : 128;
     566              : 
     567          447 :         sqlite3_value **new_values_2 = (sqlite3_value **)cloudsync_memory_realloc(payload->new_values, newcap * sizeof(*new_values_2));
     568          447 :         if (!new_values_2) return SQLITE_NOMEM;
     569              : 
     570          447 :         sqlite3_value **old_values_2 = (sqlite3_value **)cloudsync_memory_realloc(payload->old_values, newcap * sizeof(*old_values_2));
     571          447 :         if (!old_values_2) {
     572              :             // new_values_2 succeeded but old_values failed; keep new_values_2 pointer
     573              :             // (it's still valid, just larger) but don't update capacity
     574            0 :             payload->new_values = new_values_2;
     575            0 :             return SQLITE_NOMEM;
     576              :         }
     577              : 
     578          447 :         payload->new_values = new_values_2;
     579          447 :         payload->old_values = old_values_2;
     580          447 :         payload->capacity = newcap;
     581          447 :     }
     582              : 
     583         2204 :     int index = payload->count;
     584         2204 :     if (payload->table_name == NULL) payload->table_name = database_value_dup(v1);
     585         1760 :     else if (dbutils_value_compare(payload->table_name, v1) != 0) return SQLITE_NOMEM;
     586              : 
     587         2204 :     payload->new_values[index] = database_value_dup(v2);
     588         2204 :     payload->old_values[index] = database_value_dup(v3);
     589              : 
     590              :     // sanity check memory allocations before committing count
     591         2204 :     bool v1_can_be_null = (database_value_type(v1) == SQLITE_NULL);
     592         2204 :     bool v2_can_be_null = (database_value_type(v2) == SQLITE_NULL);
     593         2204 :     bool v3_can_be_null = (database_value_type(v3) == SQLITE_NULL);
     594              : 
     595         2204 :     bool oom = false;
     596         2204 :     if ((payload->table_name == NULL) && (!v1_can_be_null)) oom = true;
     597         2204 :     if ((payload->new_values[index] == NULL) && (!v2_can_be_null)) oom = true;
     598         2204 :     if ((payload->old_values[index] == NULL) && (!v3_can_be_null)) oom = true;
     599              : 
     600         2204 :     if (oom) {
     601              :         // clean up partial allocations at this index to prevent leaks
     602            0 :         if (payload->new_values[index]) { database_value_free(payload->new_values[index]); payload->new_values[index] = NULL; }
     603            0 :         if (payload->old_values[index]) { database_value_free(payload->old_values[index]); payload->old_values[index] = NULL; }
     604            0 :         return SQLITE_NOMEM;
     605              :     }
     606              : 
     607         2204 :     payload->count++;
     608         2204 :     return SQLITE_OK;
     609         2204 : }
     610              : 
     611         2204 : void dbsync_update_step (sqlite3_context *context, int argc, sqlite3_value **argv) {
     612              :     // argv[0] => table_name
     613              :     // argv[1] => new_column_value
     614              :     // argv[2] => old_column_value
     615              :     
     616              :     // allocate/get the update payload
     617         2204 :     cloudsync_update_payload *payload = (cloudsync_update_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_update_payload));
     618         2204 :     if (!payload) {sqlite3_result_error_nomem(context); return;}
     619              :     
     620         2204 :     if (dbsync_update_payload_append(payload, argv[0], argv[1], argv[2]) != SQLITE_OK) {
     621            0 :         sqlite3_result_error_nomem(context);
     622            0 :     }
     623         2204 : }
     624              : 
     625          444 : void dbsync_update_final (sqlite3_context *context) {
     626          444 :     cloudsync_update_payload *payload = (cloudsync_update_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_update_payload));
     627          444 :     if (!payload || payload->count == 0) return;
     628              :     
     629              :     // retrieve context
     630          444 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     631              :     
     632              :     // lookup table
     633          444 :     const char *table_name = (const char *)database_value_text(payload->table_name);
     634          444 :     cloudsync_table_context *table = table_lookup(data, table_name);
     635          444 :     if (!table) {
     636            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_update.", table_name);
     637            0 :         dbsync_update_payload_free(payload);
     638            0 :         return;
     639              :     }
     640              : 
     641              :     // compute the next database version for tracking changes
     642          444 :     int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
     643          444 :     int rc = SQLITE_OK;
     644              :     
     645              :     // Check if the primary key(s) have changed
     646          444 :     bool prikey_changed = false;
     647         1170 :     for (int i=0; i<table_count_pks(table); ++i) {
     648          755 :         if (dbutils_value_compare(payload->old_values[i], payload->new_values[i]) != 0) {
     649           29 :             prikey_changed = true;
     650           29 :             break;
     651              :         }
     652          726 :     }
     653              : 
     654              :     // encode the NEW primary key values into a buffer (used later for indexing)
     655              :     char buffer[1024];
     656              :     char buffer2[1024];
     657          444 :     size_t pklen = sizeof(buffer);
     658          444 :     size_t oldpklen = sizeof(buffer2);
     659          444 :     char *oldpk = NULL;
     660              :     
     661          444 :     char *pk = pk_encode_prikey((dbvalue_t **)payload->new_values, table_count_pks(table), buffer, &pklen);
     662          444 :     if (!pk) {
     663            0 :         sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
     664            0 :         dbsync_update_payload_free(payload);
     665            0 :         return;
     666              :     }
     667          444 :     if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
     668            0 :         dbsync_set_error(context, "Update aborted because primary key in table %s contains NULL values.", table_name);
     669            0 :         dbsync_update_payload_free(payload);
     670            0 :         return;
     671              :     }
     672              :     
     673          444 :     if (prikey_changed) {
     674              :         // if the primary key has changed, we need to handle the row differently:
     675              :         // 1. mark the old row (OLD primary key) as deleted
     676              :         // 2. create a new row (NEW primary key)
     677              :         
     678              :         // encode the OLD primary key into a buffer
     679           29 :         oldpk = pk_encode_prikey((dbvalue_t **)payload->old_values, table_count_pks(table), buffer2, &oldpklen);
     680           29 :         if (!oldpk) {
     681              :             // no check here about PRIKEY_NULL_CONSTRAINT_ERROR because by design oldpk cannot contain NULL values
     682            0 :             if (pk != buffer) cloudsync_memory_free(pk);
     683            0 :             sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
     684            0 :             dbsync_update_payload_free(payload);
     685            0 :             return;
     686              :         }
     687              :         
     688              :         // mark the rows with the old primary key as deleted in the metadata (old row handling)
     689           29 :         rc = local_mark_delete_meta(table, oldpk, oldpklen, db_version, cloudsync_bumpseq(data));
     690           29 :         if (rc != SQLITE_OK) goto cleanup;
     691              :         
     692              :         // move non-sentinel metadata entries from OLD primary key to NEW primary key
     693              :         // handles the case where some metadata is retained across primary key change
     694              :         // see https://github.com/sqliteai/sqlite-sync/blob/main/docs/PriKey.md for more details
     695           29 :         rc = local_update_move_meta(table, pk, pklen, oldpk, oldpklen, db_version);
     696           29 :         if (rc != SQLITE_OK) goto cleanup;
     697              :         
     698              :         // mark a new sentinel row with the new primary key in the metadata
     699           29 :         rc = local_mark_insert_sentinel_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
     700           29 :         if (rc != SQLITE_OK) goto cleanup;
     701              :         
     702              :         // free memory if the OLD primary key was dynamically allocated
     703           29 :         if (oldpk != buffer2) cloudsync_memory_free(oldpk);
     704           29 :         oldpk = NULL;
     705           29 :     }
     706              :     
     707              :     // compare NEW and OLD values (excluding primary keys) to handle column updates
     708         1865 :     for (int i=0; i<table_count_cols(table); i++) {
     709         1421 :         int col_index = table_count_pks(table) + i;  // Regular columns start after primary keys
     710              : 
     711         1421 :         if (dbutils_value_compare(payload->old_values[col_index], payload->new_values[col_index]) != 0) {
     712          724 :             if (table_col_algo(table, i) == col_algo_block) {
     713              :                 // Block column: diff old and new text, emit per-block metadata changes
     714           93 :                 const char *new_text = (const char *)database_value_text(payload->new_values[col_index]);
     715           93 :                 const char *delim = table_col_delimiter(table, i);
     716           93 :                 const char *col = table_colname(table, i);
     717              : 
     718              :                 // Read existing blocks from blocks table
     719           93 :                 block_list_t *old_blocks = block_list_create_empty();
     720           93 :                 if (table_block_list_stmt(table)) {
     721           93 :                     char *like_pattern = block_build_colname(col, "%");
     722           93 :                     if (like_pattern) {
     723              :                         // Query blocks table directly for existing block names and values
     724           93 :                         char *list_sql = cloudsync_memory_mprintf(
     725              :                             "SELECT col_name, col_value FROM %s WHERE pk = ?1 AND col_name LIKE ?2 ORDER BY col_name",
     726           93 :                             table_blocks_ref(table));
     727           93 :                         if (list_sql) {
     728           93 :                             dbvm_t *list_vm = NULL;
     729           93 :                             if (databasevm_prepare(data, list_sql, &list_vm, 0) == DBRES_OK) {
     730           93 :                                 databasevm_bind_blob(list_vm, 1, pk, (int)pklen);
     731           93 :                                 databasevm_bind_text(list_vm, 2, like_pattern, -1);
     732         1447 :                                 while (databasevm_step(list_vm) == DBRES_ROW) {
     733         1354 :                                     const char *bcn = database_column_text(list_vm, 0);
     734         1354 :                                     const char *bval = database_column_text(list_vm, 1);
     735         1354 :                                     const char *pos = block_extract_position_id(bcn);
     736         1354 :                                     if (pos && old_blocks) {
     737         1354 :                                         block_list_add(old_blocks, bval ? bval : "", pos);
     738         1354 :                                     }
     739              :                                 }
     740           93 :                                 databasevm_finalize(list_vm);
     741           93 :                             }
     742           93 :                             cloudsync_memory_free(list_sql);
     743           93 :                         }
     744           93 :                         cloudsync_memory_free(like_pattern);
     745           93 :                     }
     746           93 :                 }
     747              : 
     748              :                 // Split new text into parts (NULL text = all blocks removed)
     749           93 :                 block_list_t *new_blocks = new_text ? block_split(new_text, delim) : block_list_create_empty();
     750           93 :                 if (new_blocks && old_blocks) {
     751              :                     // Build array of new content strings (NULL when count is 0)
     752           93 :                     const char **new_parts = NULL;
     753           93 :                     if (new_blocks->count > 0) {
     754           92 :                         new_parts = (const char **)cloudsync_memory_alloc(
     755           92 :                             (uint64_t)(new_blocks->count * sizeof(char *)));
     756           92 :                         if (new_parts) {
     757         1500 :                             for (int b = 0; b < new_blocks->count; b++) {
     758         1408 :                                 new_parts[b] = new_blocks->entries[b].content;
     759         1408 :                             }
     760           92 :                         }
     761           92 :                     }
     762              : 
     763           93 :                     if (new_parts || new_blocks->count == 0) {
     764          186 :                         block_diff_t *diff = block_diff(old_blocks->entries, old_blocks->count,
     765           93 :                                                          new_parts, new_blocks->count);
     766           93 :                         if (diff) {
     767          229 :                             for (int d = 0; d < diff->count; d++) {
     768          136 :                                 block_diff_entry_t *de = &diff->entries[d];
     769          136 :                                 char *block_cn = block_build_colname(col, de->position_id);
     770          136 :                                 if (!block_cn) continue;
     771              : 
     772          136 :                                 if (de->type == BLOCK_DIFF_ADDED || de->type == BLOCK_DIFF_MODIFIED) {
     773          190 :                                     rc = local_mark_insert_or_update_meta(table, pk, pklen, block_cn,
     774           95 :                                                                           db_version, cloudsync_bumpseq(data));
     775              :                                     // Store block value
     776           95 :                                     if (rc == SQLITE_OK && table_block_value_write_stmt(table)) {
     777           95 :                                         dbvm_t *wvm = table_block_value_write_stmt(table);
     778           95 :                                         databasevm_bind_blob(wvm, 1, pk, (int)pklen);
     779           95 :                                         databasevm_bind_text(wvm, 2, block_cn, -1);
     780           95 :                                         databasevm_bind_text(wvm, 3, de->content, -1);
     781           95 :                                         databasevm_step(wvm);
     782           95 :                                         databasevm_reset(wvm);
     783           95 :                                     }
     784          136 :                                 } else if (de->type == BLOCK_DIFF_REMOVED) {
     785              :                                     // Mark block as deleted in metadata (even col_version)
     786           82 :                                     rc = local_mark_delete_block_meta(table, pk, pklen, block_cn,
     787           41 :                                                                       db_version, cloudsync_bumpseq(data));
     788              :                                     // Remove from blocks table
     789           41 :                                     if (rc == SQLITE_OK) {
     790           41 :                                         block_delete_value_external(data, table, pk, pklen, block_cn);
     791           41 :                                     }
     792           41 :                                 }
     793          136 :                                 cloudsync_memory_free(block_cn);
     794          136 :                                 if (rc != SQLITE_OK) break;
     795          136 :                             }
     796           93 :                             block_diff_free(diff);
     797           93 :                         }
     798           93 :                         if (new_parts) cloudsync_memory_free((void *)new_parts);
     799           93 :                     }
     800           93 :                 }
     801           93 :                 if (new_blocks) block_list_free(new_blocks);
     802           93 :                 if (old_blocks) block_list_free(old_blocks);
     803           93 :                 if (rc != SQLITE_OK) goto cleanup;
     804           93 :             } else {
     805              :                 // Regular column: mark as updated in the metadata (columns are in cid order)
     806          631 :                 rc = local_mark_insert_or_update_meta(table, pk, pklen, table_colname(table, i), db_version, cloudsync_bumpseq(data));
     807          631 :                 if (rc != SQLITE_OK) goto cleanup;
     808              :             }
     809          724 :         }
     810         1865 :     }
     811              :     
     812              : cleanup:
     813          444 :     if (rc != SQLITE_OK) sqlite3_result_error(context, database_errmsg(data), -1);
     814          444 :     if (pk != buffer) cloudsync_memory_free(pk);
     815          444 :     if (oldpk && (oldpk != buffer2)) cloudsync_memory_free(oldpk);
     816              :     
     817          444 :     dbsync_update_payload_free(payload);
     818          444 : }
     819              : 
     820              : // MARK: -
     821              : 
     822            4 : void dbsync_cleanup (sqlite3_context *context, int argc, sqlite3_value **argv) {
     823              :     DEBUG_FUNCTION("cloudsync_cleanup");
     824              :     
     825            4 :     const char *table = (const char *)database_value_text(argv[0]);
     826            4 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     827              :     
     828            4 :     int rc = cloudsync_cleanup(data, table);
     829            4 :     if (rc != DBRES_OK) {
     830            0 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     831            0 :         sqlite3_result_error_code(context, rc);
     832            0 :     }
     833            4 : }
     834              : 
     835            6 : void dbsync_enable_disable (sqlite3_context *context, const char *table_name, bool value) {
     836              :     DEBUG_FUNCTION("cloudsync_enable_disable");
     837              :     
     838            6 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     839            6 :     cloudsync_table_context *table = table_lookup(data, table_name);
     840            6 :     if (!table) return;
     841              :     
     842            6 :     table_set_enabled(table, value);
     843            6 : }
     844              : 
     845            3 : void dbsync_enable (sqlite3_context *context, int argc, sqlite3_value **argv) {
     846              :     DEBUG_FUNCTION("cloudsync_enable");
     847              :     
     848            3 :     const char *table = (const char *)database_value_text(argv[0]);
     849            3 :     dbsync_enable_disable(context, table, true);
     850            3 : }
     851              : 
     852            3 : void dbsync_disable (sqlite3_context *context, int argc, sqlite3_value **argv) {
     853              :     DEBUG_FUNCTION("cloudsync_disable");
     854              :     
     855            3 :     const char *table = (const char *)database_value_text(argv[0]);
     856            3 :     dbsync_enable_disable(context, table, false);
     857            3 : }
     858              : 
     859         2858 : void dbsync_is_enabled (sqlite3_context *context, int argc, sqlite3_value **argv) {
     860              :     DEBUG_FUNCTION("cloudsync_is_enabled");
     861              :     
     862         2858 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     863         2858 :     const char *table_name = (const char *)database_value_text(argv[0]);
     864         2858 :     cloudsync_table_context *table = table_lookup(data, table_name);
     865              :     
     866         2858 :     int result = (table && table_enabled(table)) ? 1 : 0;
     867         2858 :     sqlite3_result_int(context, result);
     868         2858 : }
     869              : 
     870          235 : void dbsync_terminate (sqlite3_context *context, int argc, sqlite3_value **argv) {
     871              :     DEBUG_FUNCTION("cloudsync_terminate");
     872              :     
     873          235 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     874          235 :     int rc = cloudsync_terminate(data);
     875          235 :     sqlite3_result_int(context, rc);
     876          235 : }
     877              : 
     878              : // MARK: -
     879              : 
     880          266 : void dbsync_init (sqlite3_context *context, const char *table, const char *algo, CLOUDSYNC_INIT_FLAG init_flags) {
     881          266 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     882              :     
     883          266 :     int rc = database_begin_savepoint(data, "cloudsync_init");
     884          266 :     if (rc != SQLITE_OK) {
     885            0 :         dbsync_set_error(context, "Unable to create cloudsync_init savepoint. %s", database_errmsg(data));
     886            0 :         sqlite3_result_error_code(context, rc);
     887            0 :         return;
     888              :     }
     889              :     
     890          266 :     rc = cloudsync_init_table(data, table, algo, init_flags);
     891          266 :     if (rc == SQLITE_OK) {
     892          261 :         rc = database_commit_savepoint(data, "cloudsync_init");
     893          261 :         if (rc != SQLITE_OK) {
     894            0 :             dbsync_set_error(context, "Unable to release cloudsync_init savepoint. %s", database_errmsg(data));
     895            0 :             sqlite3_result_error_code(context, rc);
     896            0 :         }
     897          261 :     } else {
     898              :         // in case of error, rollback transaction
     899            5 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     900            5 :         sqlite3_result_error_code(context, rc);
     901            5 :         database_rollback_savepoint(data, "cloudsync_init");
     902            5 :         return;
     903              :     }
     904              :     
     905          261 :     cloudsync_update_schema_hash(data);
     906              :     
     907              :     // returns site_id as TEXT
     908              :     char buffer[UUID_STR_MAXLEN];
     909          261 :     cloudsync_uuid_v7_stringify(cloudsync_siteid(data), buffer, false);
     910          261 :     sqlite3_result_text(context, buffer, -1, SQLITE_TRANSIENT);
     911          266 : }
     912              : 
     913           46 : void dbsync_init3 (sqlite3_context *context, int argc, sqlite3_value **argv) {
     914              :     DEBUG_FUNCTION("cloudsync_init2");
     915              :     
     916           46 :     const char *table = (const char *)database_value_text(argv[0]);
     917           46 :     const char *algo = (const char *)database_value_text(argv[1]);
     918           46 :     int init_flags = database_value_int(argv[2]);
     919           46 :     dbsync_init(context, table, algo, init_flags);
     920           46 : }
     921              : 
     922           86 : void dbsync_init2 (sqlite3_context *context, int argc, sqlite3_value **argv) {
     923              :     DEBUG_FUNCTION("cloudsync_init2");
     924              :     
     925           86 :     const char *table = (const char *)database_value_text(argv[0]);
     926           86 :     const char *algo = (const char *)database_value_text(argv[1]);
     927           86 :     dbsync_init(context, table, algo, CLOUDSYNC_INIT_FLAG_NONE);
     928           86 : }
     929              : 
     930          134 : void dbsync_init1 (sqlite3_context *context, int argc, sqlite3_value **argv) {
     931              :     DEBUG_FUNCTION("cloudsync_init1");
     932              :     
     933          134 :     const char *table = (const char *)database_value_text(argv[0]);
     934          134 :     dbsync_init(context, table, NULL, CLOUDSYNC_INIT_FLAG_NONE);
     935          134 : }
     936              : 
     937              : // MARK: -
     938              : 
     939           24 : void dbsync_begin_alter (sqlite3_context *context, int argc, sqlite3_value **argv) {
     940              :     DEBUG_FUNCTION("dbsync_begin_alter");
     941              : 
     942           24 :     const char *table_name = (const char *)database_value_text(argv[0]);
     943           24 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     944              : 
     945           24 :     int rc = database_begin_savepoint(data, "cloudsync_alter");
     946           24 :     if (rc != DBRES_OK) {
     947            0 :         sqlite3_result_error(context, "Unable to create cloudsync_alter savepoint", -1);
     948            0 :         sqlite3_result_error_code(context, rc);
     949            0 :         return;
     950              :     }
     951              : 
     952           24 :     rc = cloudsync_begin_alter(data, table_name);
     953           24 :     if (rc != DBRES_OK) {
     954            1 :         database_rollback_savepoint(data, "cloudsync_alter");
     955            1 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     956            1 :         sqlite3_result_error_code(context, rc);
     957            1 :     }
     958           24 : }
     959              : 
     960           24 : void dbsync_commit_alter (sqlite3_context *context, int argc, sqlite3_value **argv) {
     961              :     DEBUG_FUNCTION("cloudsync_commit_alter");
     962              :     
     963              :     //retrieve table argument
     964           24 :     const char *table_name = (const char *)database_value_text(argv[0]);
     965              :     
     966              :     // retrieve context
     967           24 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
     968              :     
     969           24 :     int rc = cloudsync_commit_alter(data, table_name);
     970           24 :     if (rc != DBRES_OK) {
     971            1 :         database_rollback_savepoint(data, "cloudsync_alter");
     972            1 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
     973            1 :         sqlite3_result_error_code(context, rc);
     974            1 :         return;
     975              :     }
     976              : 
     977           23 :     rc = database_commit_savepoint(data, "cloudsync_alter");
     978           23 :     if (rc != DBRES_OK) {
     979            0 :         sqlite3_result_error(context, database_errmsg(data), -1);
     980            0 :         sqlite3_result_error_code(context, rc);
     981            0 :         return;
     982              :     }
     983              : 
     984           23 :     cloudsync_update_schema_hash(data);
     985           24 : }
     986              : 
     987              : // MARK: - Payload -
     988              : 
     989        49184 : void dbsync_payload_encode_step (sqlite3_context *context, int argc, sqlite3_value **argv) {
     990              :     // allocate/get the session context
     991        49184 :     cloudsync_payload_context *payload = (cloudsync_payload_context *)sqlite3_aggregate_context(context, (int)cloudsync_payload_context_size(NULL));
     992        49184 :     if (!payload) {
     993            0 :         sqlite3_result_error(context, "Not enough memory to allocate payload session context", -1);
     994            0 :         sqlite3_result_error_code(context, SQLITE_NOMEM);
     995            0 :         return;
     996              :     }
     997              :     
     998              :     // retrieve context
     999        49184 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1000              :     
    1001        49184 :     int rc = cloudsync_payload_encode_step(payload, data, argc, (dbvalue_t **)argv);
    1002        49184 :     if (rc != SQLITE_OK) {
    1003            0 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
    1004            0 :         sqlite3_result_error_code(context, rc);
    1005            0 :     }
    1006        49184 : }
    1007              : 
    1008          695 : void dbsync_payload_encode_final (sqlite3_context *context) {
    1009              :     // get the session context
    1010          695 :     cloudsync_payload_context *payload = (cloudsync_payload_context *)sqlite3_aggregate_context(context, (int)cloudsync_payload_context_size(NULL));
    1011          695 :     if (!payload) {
    1012            0 :         sqlite3_result_error(context, "Unable to extract payload session context", -1);
    1013            0 :         sqlite3_result_error_code(context, SQLITE_NOMEM);
    1014            0 :         return;
    1015              :     }
    1016              :     
    1017              :     // retrieve context
    1018          695 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1019              :     
    1020          695 :     int rc = cloudsync_payload_encode_final(payload, data);
    1021          695 :     if (rc != SQLITE_OK) {
    1022            0 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
    1023            0 :         sqlite3_result_error_code(context, rc);
    1024            0 :         return;
    1025              :     }
    1026              :     
    1027              :     // result is OK so get BLOB and returns it
    1028          695 :     int64_t blob_size = 0;
    1029          695 :     char *blob = cloudsync_payload_blob (payload, &blob_size, NULL);
    1030          695 :     if (!blob) {
    1031            8 :         sqlite3_result_null(context);
    1032            8 :     } else {
    1033          687 :         sqlite3_result_blob64(context, blob, blob_size, SQLITE_TRANSIENT);
    1034          687 :         cloudsync_memory_free(blob);
    1035              :     }
    1036              :     
    1037              :     // from: https://sqlite.org/c3ref/aggregate_context.html
    1038              :     // SQLite automatically frees the memory allocated by sqlite3_aggregate_context() when the aggregate query concludes.
    1039          695 : }
    1040              : 
    1041          688 : void dbsync_payload_decode (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1042              :     DEBUG_FUNCTION("dbsync_payload_decode");
    1043              :     //debug_values(argc, argv);
    1044              :     
    1045              :     // sanity check payload type
    1046          688 :     if (database_value_type(argv[0]) != SQLITE_BLOB) {
    1047            0 :         sqlite3_result_error(context, "Error on cloudsync_payload_decode: value must be a BLOB.", -1);
    1048            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    1049            0 :         return;
    1050              :     }
    1051              :     
    1052              :     // sanity check payload size
    1053          688 :     int blen = database_value_bytes(argv[0]);
    1054          688 :     size_t header_size = 0;
    1055          688 :     cloudsync_payload_context_size(&header_size);
    1056          688 :     if (blen < (int)header_size) {
    1057            3 :         sqlite3_result_error(context, "Error on cloudsync_payload_decode: invalid input size.", -1);
    1058            3 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    1059            3 :         return;
    1060              :     }
    1061              :     
    1062              :     // obtain payload
    1063          685 :     const char *payload = (const char *)database_value_blob(argv[0]);
    1064              :     
    1065              :     // apply changes
    1066          685 :     int nrows = 0;
    1067          685 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1068          685 :     int rc = cloudsync_payload_apply(data, payload, blen, &nrows);
    1069          685 :     if (rc != SQLITE_OK) {
    1070            5 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
    1071            5 :         sqlite3_result_error_code(context, rc);
    1072            5 :         return;
    1073              :     }
    1074              :     
    1075              :     // returns number of applied rows
    1076          680 :     sqlite3_result_int(context, nrows);
    1077          688 : }
    1078              : 
    1079              : #ifdef CLOUDSYNC_DESKTOP_OS
    1080            0 : void dbsync_payload_save (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1081              :     DEBUG_FUNCTION("dbsync_payload_save");
    1082              :     
    1083              :     // sanity check argument
    1084            0 :     if (database_value_type(argv[0]) != SQLITE_TEXT) {
    1085            0 :         sqlite3_result_error(context, "Unable to retrieve file path.", -1);
    1086            0 :         return;
    1087              :     }
    1088              :     
    1089              :     // retrieve full path to file
    1090            0 :     const char *payload_path = (const char *)database_value_text(argv[0]);
    1091              :     
    1092              :     // retrieve global context
    1093            0 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1094              :     
    1095            0 :     int blob_size = 0;
    1096            0 :     int rc = cloudsync_payload_save(data, payload_path, &blob_size);
    1097            0 :     if (rc == SQLITE_OK) {
    1098              :         // if OK then returns blob size
    1099            0 :         sqlite3_result_int64(context, (sqlite3_int64)blob_size);
    1100            0 :         return;
    1101              :     }
    1102              :     
    1103            0 :     sqlite3_result_error(context, cloudsync_errmsg(data), -1);
    1104            0 :     sqlite3_result_error_code(context, rc);
    1105            0 : }
    1106              : 
    1107            0 : void dbsync_payload_load (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1108              :     DEBUG_FUNCTION("dbsync_payload_load");
    1109              :     
    1110              :     // sanity check argument
    1111            0 :     if (database_value_type(argv[0]) != SQLITE_TEXT) {
    1112            0 :         sqlite3_result_error(context, "Unable to retrieve file path.", -1);
    1113            0 :         return;
    1114              :     }
    1115              :     
    1116              :     // retrieve full path to file
    1117            0 :     const char *path = (const char *)database_value_text(argv[0]);
    1118              :     
    1119            0 :     int64_t payload_size = 0;
    1120            0 :     char *payload = cloudsync_file_read(path, &payload_size);
    1121            0 :     if (!payload) {
    1122            0 :         if (payload_size < 0) {
    1123            0 :             sqlite3_result_error(context, "Unable to read payload from file path.", -1);
    1124            0 :             sqlite3_result_error_code(context, SQLITE_IOERR);
    1125            0 :             return;
    1126              :         }
    1127              :         // no rows affected but no error either
    1128            0 :         sqlite3_result_int(context, 0);
    1129            0 :         return;
    1130              :     }
    1131              :     
    1132            0 :     int nrows = 0;
    1133            0 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1134            0 :     int rc = cloudsync_payload_apply (data, payload, (int)payload_size, &nrows);
    1135            0 :     if (payload) cloudsync_memory_free(payload);
    1136              :     
    1137            0 :     if (rc != SQLITE_OK) {
    1138            0 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
    1139            0 :         sqlite3_result_error_code(context, rc);
    1140            0 :         return;
    1141              :     }
    1142              :     
    1143              :     // returns number of applied rows
    1144            0 :     sqlite3_result_int(context, nrows);
    1145            0 : }
    1146              : #endif
    1147              : 
    1148              : // MARK: - Register -
    1149              : 
    1150         8854 : 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 *)) {
    1151              : 
    1152         8854 :     int rc = sqlite3_create_function_v2(db, name, nargs, flags, ctx, xfunc, xstep, xfinal, ctx_free);
    1153              :     
    1154         8854 :     if (rc != SQLITE_OK) {
    1155            0 :         if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Error creating function %s: %s", name, sqlite3_errmsg(db));
    1156            0 :         return rc;
    1157              :     }
    1158         8854 :     return SQLITE_OK;
    1159         8854 : }
    1160              : 
    1161         7223 : 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 *)) {
    1162         7223 :     const int FLAGS_VOLATILE = SQLITE_UTF8;
    1163              :     DEBUG_DBFUNCTION("dbsync_register %s", name);
    1164         7223 :     return dbsync_register_with_flags(db, name, xfunc, xstep, xfinal, nargs, FLAGS_VOLATILE, pzErrMsg, ctx, ctx_free);
    1165              : }
    1166              : 
    1167         6990 : 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 *)) {
    1168              :     DEBUG_DBFUNCTION("dbsync_register_function %s", name);
    1169         6990 :     return dbsync_register(db, name, xfunc, NULL, NULL, nargs, pzErrMsg, ctx, ctx_free);
    1170              : }
    1171              : 
    1172          699 : 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 *)) {
    1173          699 :     const int FLAGS_PURE = SQLITE_UTF8 | SQLITE_INNOCUOUS | SQLITE_DETERMINISTIC;
    1174              :     DEBUG_DBFUNCTION("dbsync_register_pure_function %s", name);
    1175          699 :     return dbsync_register_with_flags(db, name, xfunc, NULL, NULL, nargs, FLAGS_PURE, pzErrMsg, ctx, ctx_free);
    1176              : }
    1177              : 
    1178          699 : 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 *)) {
    1179          699 :     const int FLAGS_TRIGGER = SQLITE_UTF8 | SQLITE_INNOCUOUS;
    1180              :     DEBUG_DBFUNCTION("dbsync_register_trigger_function %s", name);
    1181          699 :     return dbsync_register_with_flags(db, name, xfunc, NULL, NULL, nargs, FLAGS_TRIGGER, pzErrMsg, ctx, ctx_free);
    1182              : }
    1183              : 
    1184          233 : 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 *)) {
    1185              :     DEBUG_DBFUNCTION("dbsync_register_aggregate %s", name);
    1186          233 :     return dbsync_register(db, name, NULL, xstep, xfinal, nargs, pzErrMsg, ctx, ctx_free);
    1187              : }
    1188              : 
    1189          233 : 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 *)) {
    1190          233 :     const int FLAGS_TRIGGER = SQLITE_UTF8 | SQLITE_INNOCUOUS;
    1191              :     DEBUG_DBFUNCTION("dbsync_register_trigger_aggregate %s", name);
    1192          233 :     return dbsync_register_with_flags(db, name, NULL, xstep, xfinal, nargs, FLAGS_TRIGGER, pzErrMsg, ctx, ctx_free);
    1193              : }
    1194              : 
    1195              : // MARK: - Block-level LWW -
    1196              : 
    1197           50 : void dbsync_text_materialize (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1198              :     DEBUG_FUNCTION("cloudsync_text_materialize");
    1199              : 
    1200              :     // argv[0] -> table name
    1201              :     // argv[1] -> column name
    1202              :     // argv[2..N] -> primary key values
    1203              : 
    1204           50 :     if (argc < 3) {
    1205            0 :         sqlite3_result_error(context, "cloudsync_text_materialize requires at least 3 arguments: table, column, pk...", -1);
    1206            0 :         return;
    1207              :     }
    1208              : 
    1209           50 :     const char *table_name = (const char *)database_value_text(argv[0]);
    1210           50 :     const char *col_name = (const char *)database_value_text(argv[1]);
    1211           50 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1212              : 
    1213           50 :     cloudsync_table_context *table = table_lookup(data, table_name);
    1214           50 :     if (!table) {
    1215            0 :         dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_text_materialize.", table_name);
    1216            0 :         return;
    1217              :     }
    1218              : 
    1219           50 :     int col_idx = table_col_index(table, col_name);
    1220           50 :     if (col_idx < 0 || table_col_algo(table, col_idx) != col_algo_block) {
    1221            0 :         dbsync_set_error(context, "Column %s in table %s is not configured as block-level.", col_name, table_name);
    1222            0 :         return;
    1223              :     }
    1224              : 
    1225              :     // Encode primary keys
    1226           50 :     int npks = table_count_pks(table);
    1227           50 :     if (argc - 2 != npks) {
    1228            0 :         sqlite3_result_error(context, "Wrong number of primary key values for cloudsync_text_materialize.", -1);
    1229            0 :         return;
    1230              :     }
    1231              : 
    1232              :     char buffer[1024];
    1233           50 :     size_t pklen = sizeof(buffer);
    1234           50 :     char *pk = pk_encode_prikey((dbvalue_t **)&argv[2], npks, buffer, &pklen);
    1235           50 :     if (!pk || pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
    1236            0 :         sqlite3_result_error(context, "Failed to encode primary key(s).", -1);
    1237            0 :         return;
    1238              :     }
    1239              : 
    1240              :     // Materialize the column
    1241           50 :     int rc = block_materialize_column(data, table, pk, (int)pklen, col_name);
    1242           50 :     if (rc != DBRES_OK) {
    1243            0 :         sqlite3_result_error(context, cloudsync_errmsg(data), -1);
    1244            0 :     } else {
    1245           50 :         sqlite3_result_int(context, 1);
    1246              :     }
    1247              : 
    1248           50 :     if (pk != buffer) cloudsync_memory_free(pk);
    1249           50 : }
    1250              : 
    1251              : // MARK: - Row Filter -
    1252              : 
    1253           21 : void dbsync_set_filter (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1254              :     DEBUG_FUNCTION("cloudsync_set_filter");
    1255              : 
    1256           21 :     const char *tbl = (const char *)database_value_text(argv[0]);
    1257           21 :     const char *filter_expr = (const char *)database_value_text(argv[1]);
    1258           21 :     if (!tbl || !filter_expr) {
    1259            0 :         dbsync_set_error(context, "cloudsync_set_filter: table and filter expression required");
    1260            0 :         return;
    1261              :     }
    1262              : 
    1263           21 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1264              : 
    1265              :     // Guard against calling set_filter before the target table has been set
    1266              :     // up for sync: without this, we'd hit "no such table:
    1267              :     // cloudsync_table_settings" or "no such table: main.<tbl>" deep inside
    1268              :     // the trigger recreation path, which is not actionable.
    1269           21 :     if (!cloudsync_context_is_initialized(data) || !table_lookup(data, tbl)) {
    1270            2 :         dbsync_set_error(context,
    1271              :             "cloudsync_set_filter: table '%s' is not configured for sync. "
    1272            1 :             "Call SELECT cloudsync_init('%s') first.", tbl, tbl);
    1273            1 :         return;
    1274              :     }
    1275              : 
    1276              :     // Store filter in table settings
    1277           20 :     dbutils_table_settings_set_key_value(data, tbl, "*", "filter", filter_expr);
    1278              : 
    1279              :     // Read current algo
    1280           20 :     table_algo algo = dbutils_table_settings_get_algo(data, tbl);
    1281           20 :     if (algo == table_algo_none) algo = table_algo_crdt_cls;
    1282              : 
    1283              :     // Drop and recreate triggers with the filter
    1284           20 :     database_delete_triggers(data, tbl);
    1285           20 :     int rc = database_create_triggers(data, tbl, algo, filter_expr);
    1286           20 :     if (rc != DBRES_OK) {
    1287            0 :         dbsync_set_error(context, "cloudsync_set_filter: error recreating triggers");
    1288            0 :         sqlite3_result_error_code(context, rc);
    1289            0 :         return;
    1290              :     }
    1291              : 
    1292              :     // Clean and refill metatable with the new filter
    1293           20 :     rc = cloudsync_reset_metatable(data, tbl);
    1294           20 :     if (rc != DBRES_OK) {
    1295            0 :         dbsync_set_error(context, "cloudsync_set_filter: error resetting metatable");
    1296            0 :         sqlite3_result_error_code(context, rc);
    1297            0 :         return;
    1298              :     }
    1299              : 
    1300           20 :     sqlite3_result_int(context, 1);
    1301           21 : }
    1302              : 
    1303            5 : void dbsync_clear_filter (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1304              :     DEBUG_FUNCTION("cloudsync_clear_filter");
    1305              : 
    1306            5 :     const char *tbl = (const char *)database_value_text(argv[0]);
    1307            5 :     if (!tbl) {
    1308            0 :         dbsync_set_error(context, "cloudsync_clear_filter: table name required");
    1309            0 :         return;
    1310              :     }
    1311              : 
    1312            5 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    1313              : 
    1314              :     // Guard against calling clear_filter before the target table has been set
    1315              :     // up for sync — see dbsync_set_filter for the same rationale.
    1316            5 :     if (!cloudsync_context_is_initialized(data) || !table_lookup(data, tbl)) {
    1317            2 :         dbsync_set_error(context,
    1318              :             "cloudsync_clear_filter: table '%s' is not configured for sync. "
    1319            1 :             "Call SELECT cloudsync_init('%s') first.", tbl, tbl);
    1320            1 :         return;
    1321              :     }
    1322              : 
    1323              :     // Remove filter from table settings (set to NULL/empty)
    1324            4 :     dbutils_table_settings_set_key_value(data, tbl, "*", "filter", NULL);
    1325              : 
    1326              :     // Read current algo
    1327            4 :     table_algo algo = dbutils_table_settings_get_algo(data, tbl);
    1328            4 :     if (algo == table_algo_none) algo = table_algo_crdt_cls;
    1329              : 
    1330              :     // Drop and recreate triggers without filter
    1331            4 :     database_delete_triggers(data, tbl);
    1332            4 :     int rc = database_create_triggers(data, tbl, algo, NULL);
    1333            4 :     if (rc != DBRES_OK) {
    1334            0 :         dbsync_set_error(context, "cloudsync_clear_filter: error recreating triggers");
    1335            0 :         sqlite3_result_error_code(context, rc);
    1336            0 :         return;
    1337              :     }
    1338              : 
    1339              :     // Clean and refill metatable without filter (all rows)
    1340            4 :     rc = cloudsync_reset_metatable(data, tbl);
    1341            4 :     if (rc != DBRES_OK) {
    1342            0 :         dbsync_set_error(context, "cloudsync_clear_filter: error resetting metatable");
    1343            0 :         sqlite3_result_error_code(context, rc);
    1344            0 :         return;
    1345              :     }
    1346              : 
    1347            4 :     sqlite3_result_int(context, 1);
    1348            5 : }
    1349              : 
    1350          235 : int dbsync_register_functions (sqlite3 *db, char **pzErrMsg) {
    1351          235 :     int rc = SQLITE_OK;
    1352              :     
    1353              :     // there's no built-in way to verify if sqlite3_cloudsync_init has already been called
    1354              :     // for this specific database connection, we use a workaround: we attempt to retrieve the
    1355              :     // cloudsync_version and check for an error, an error indicates that initialization has not been performed
    1356          235 :     if (sqlite3_exec(db, "SELECT cloudsync_version();", NULL, NULL, NULL) == SQLITE_OK) return SQLITE_OK;
    1357              :     
    1358              :     // init memory debugger (NOOP in production)
    1359              :     cloudsync_memory_init(1);
    1360              : 
    1361              :     // set fractional-indexing allocator to use cloudsync memory
    1362          233 :     block_init_allocator();
    1363              : 
    1364              :     // init context
    1365          233 :     void *ctx = cloudsync_context_create(db);
    1366          233 :     if (!ctx) {
    1367            0 :         if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Not enought memory to create a database context");
    1368            0 :         return SQLITE_NOMEM;
    1369              :     }
    1370              :     
    1371              :     // register functions
    1372              :     
    1373              :     // PUBLIC functions
    1374          233 :     rc = dbsync_register_pure_function(db, "cloudsync_version", dbsync_version, 0, pzErrMsg, ctx, cloudsync_context_free);
    1375          233 :     if (rc != SQLITE_OK) return rc;
    1376              :     
    1377          233 :     rc = dbsync_register_function(db, "cloudsync_init", dbsync_init1, 1, pzErrMsg, ctx, NULL);
    1378          233 :     if (rc != SQLITE_OK) return rc;
    1379              :     
    1380          233 :     rc = dbsync_register_function(db, "cloudsync_init", dbsync_init2, 2, pzErrMsg, ctx, NULL);
    1381          233 :     if (rc != SQLITE_OK) return rc;
    1382              :     
    1383          233 :     rc = dbsync_register_function(db, "cloudsync_init", dbsync_init3, 3, pzErrMsg, ctx, NULL);
    1384          233 :     if (rc != SQLITE_OK) return rc;
    1385              : 
    1386          233 :     rc = dbsync_register_function(db, "cloudsync_enable", dbsync_enable, 1, pzErrMsg, ctx, NULL);
    1387          233 :     if (rc != SQLITE_OK) return rc;
    1388              :     
    1389          233 :     rc = dbsync_register_function(db, "cloudsync_disable", dbsync_disable, 1, pzErrMsg, ctx, NULL);
    1390          233 :     if (rc != SQLITE_OK) return rc;
    1391              :     
    1392          233 :     rc = dbsync_register_function(db, "cloudsync_is_enabled", dbsync_is_enabled, 1, pzErrMsg, ctx, NULL);
    1393          233 :     if (rc != SQLITE_OK) return rc;
    1394              :     
    1395          233 :     rc = dbsync_register_function(db, "cloudsync_cleanup", dbsync_cleanup, 1, pzErrMsg, ctx, NULL);
    1396          233 :     if (rc != SQLITE_OK) return rc;
    1397              :     
    1398          233 :     rc = dbsync_register_function(db, "cloudsync_terminate", dbsync_terminate, 0, pzErrMsg, ctx, NULL);
    1399          233 :     if (rc != SQLITE_OK) return rc;
    1400              :     
    1401          233 :     rc = dbsync_register_function(db, "cloudsync_set", dbsync_set, 2, pzErrMsg, ctx, NULL);
    1402          233 :     if (rc != SQLITE_OK) return rc;
    1403              :     
    1404          233 :     rc = dbsync_register_function(db, "cloudsync_set_table", dbsync_set_table, 3, pzErrMsg, ctx, NULL);
    1405          233 :     if (rc != SQLITE_OK) return rc;
    1406              : 
    1407          233 :     rc = dbsync_register_function(db, "cloudsync_set_filter", dbsync_set_filter, 2, pzErrMsg, ctx, NULL);
    1408          233 :     if (rc != SQLITE_OK) return rc;
    1409              : 
    1410          233 :     rc = dbsync_register_function(db, "cloudsync_clear_filter", dbsync_clear_filter, 1, pzErrMsg, ctx, NULL);
    1411          233 :     if (rc != SQLITE_OK) return rc;
    1412              : 
    1413          233 :     rc = dbsync_register_function(db, "cloudsync_set_schema", dbsync_set_schema, 1, pzErrMsg, ctx, NULL);
    1414          233 :     if (rc != SQLITE_OK) return rc;
    1415              :     
    1416          233 :     rc = dbsync_register_function(db, "cloudsync_schema", dbsync_schema, 0, pzErrMsg, ctx, NULL);
    1417          233 :     if (rc != SQLITE_OK) return rc;
    1418              :     
    1419          233 :     rc = dbsync_register_function(db, "cloudsync_table_schema", dbsync_table_schema, 1, pzErrMsg, ctx, NULL);
    1420          233 :     if (rc != SQLITE_OK) return rc;
    1421              : 
    1422          233 :     rc = dbsync_register_function(db, "cloudsync_set_column", dbsync_set_column, 4, pzErrMsg, ctx, NULL);
    1423          233 :     if (rc != SQLITE_OK) return rc;
    1424              :     
    1425          233 :     rc = dbsync_register_function(db, "cloudsync_siteid", dbsync_siteid, 0, pzErrMsg, ctx, NULL);
    1426          233 :     if (rc != SQLITE_OK) return rc;
    1427              :     
    1428          233 :     rc = dbsync_register_function(db, "cloudsync_db_version", dbsync_db_version, 0, pzErrMsg, ctx, NULL);
    1429          233 :     if (rc != SQLITE_OK) return rc;
    1430              :     
    1431          233 :     rc = dbsync_register_function(db, "cloudsync_db_version_next", dbsync_db_version_next, 0, pzErrMsg, ctx, NULL);
    1432          233 :     if (rc != SQLITE_OK) return rc;
    1433              :     
    1434          233 :     rc = dbsync_register_function(db, "cloudsync_db_version_next", dbsync_db_version_next, 1, pzErrMsg, ctx, NULL);
    1435          233 :     if (rc != SQLITE_OK) return rc;
    1436              :     
    1437          233 :     rc = dbsync_register_function(db, "cloudsync_begin_alter", dbsync_begin_alter, 1, pzErrMsg, ctx, NULL);
    1438          233 :     if (rc != SQLITE_OK) return rc;
    1439              :     
    1440          233 :     rc = dbsync_register_function(db, "cloudsync_commit_alter", dbsync_commit_alter, 1, pzErrMsg, ctx, NULL);
    1441          233 :     if (rc != SQLITE_OK) return rc;
    1442              :     
    1443          233 :     rc = dbsync_register_function(db, "cloudsync_uuid", dbsync_uuid, 0, pzErrMsg, ctx, NULL);
    1444          233 :     if (rc != SQLITE_OK) return rc;
    1445              :     
    1446              :     // PAYLOAD
    1447          233 :     rc = dbsync_register_aggregate(db, "cloudsync_payload_encode", dbsync_payload_encode_step, dbsync_payload_encode_final, -1, pzErrMsg, ctx, NULL);
    1448          233 :     if (rc != SQLITE_OK) return rc;
    1449              :     
    1450              :     // alias
    1451          233 :     rc = dbsync_register_function(db, "cloudsync_payload_decode", dbsync_payload_decode, -1, pzErrMsg, ctx, NULL);
    1452          233 :     if (rc != SQLITE_OK) return rc;
    1453          233 :     rc = dbsync_register_function(db, "cloudsync_payload_apply", dbsync_payload_decode, -1, pzErrMsg, ctx, NULL);
    1454          233 :     if (rc != SQLITE_OK) return rc;
    1455              :     
    1456              :     #ifdef CLOUDSYNC_DESKTOP_OS
    1457          233 :     rc = dbsync_register_function(db, "cloudsync_payload_save", dbsync_payload_save, 1, pzErrMsg, ctx, NULL);
    1458          233 :     if (rc != SQLITE_OK) return rc;
    1459              :     
    1460          233 :     rc = dbsync_register_function(db, "cloudsync_payload_load", dbsync_payload_load, 1, pzErrMsg, ctx, NULL);
    1461          233 :     if (rc != SQLITE_OK) return rc;
    1462              :     #endif
    1463              :     
    1464              :     // PRIVATE functions (used inside triggers — require SQLITE_INNOCUOUS)
    1465          233 :     rc = dbsync_register_trigger_function(db, "cloudsync_is_sync", dbsync_is_sync, 1, pzErrMsg, ctx, NULL);
    1466          233 :     if (rc != SQLITE_OK) return rc;
    1467              : 
    1468          233 :     rc = dbsync_register_trigger_function(db, "cloudsync_insert", dbsync_insert, -1, pzErrMsg, ctx, NULL);
    1469          233 :     if (rc != SQLITE_OK) return rc;
    1470              : 
    1471          233 :     rc = dbsync_register_trigger_aggregate(db, "cloudsync_update", dbsync_update_step, dbsync_update_final, 3, pzErrMsg, ctx, NULL);
    1472          233 :     if (rc != SQLITE_OK) return rc;
    1473              : 
    1474          233 :     rc = dbsync_register_trigger_function(db, "cloudsync_delete", dbsync_delete, -1, pzErrMsg, ctx, NULL);
    1475          233 :     if (rc != SQLITE_OK) return rc;
    1476              :     
    1477          233 :     rc = dbsync_register_function(db, "cloudsync_col_value", dbsync_col_value, 3, pzErrMsg, ctx, NULL);
    1478          233 :     if (rc != SQLITE_OK) return rc;
    1479              :     
    1480          233 :     rc = dbsync_register_pure_function(db, "cloudsync_pk_encode", dbsync_pk_encode, -1, pzErrMsg, ctx, NULL);
    1481          233 :     if (rc != SQLITE_OK) return rc;
    1482              : 
    1483          233 :     rc = dbsync_register_pure_function(db, "cloudsync_pk_decode", dbsync_pk_decode, 2, pzErrMsg, ctx, NULL);
    1484          233 :     if (rc != SQLITE_OK) return rc;
    1485              :     
    1486          233 :     rc = dbsync_register_function(db, "cloudsync_seq", dbsync_seq, 0, pzErrMsg, ctx, NULL);
    1487          233 :     if (rc != SQLITE_OK) return rc;
    1488              : 
    1489          233 :     rc = dbsync_register_function(db, "cloudsync_text_materialize", dbsync_text_materialize, -1, pzErrMsg, ctx, NULL);
    1490          233 :     if (rc != SQLITE_OK) return rc;
    1491              : 
    1492              :     // NETWORK LAYER
    1493              :     #ifndef CLOUDSYNC_OMIT_NETWORK
    1494              :     rc = cloudsync_network_register(db, pzErrMsg, ctx);
    1495              :     if (rc != SQLITE_OK) return rc;
    1496              :     #endif
    1497              :     
    1498          233 :     cloudsync_context *data = (cloudsync_context *)ctx;
    1499          233 :     sqlite3_commit_hook(db, cloudsync_commit_hook, ctx);
    1500          233 :     sqlite3_rollback_hook(db, cloudsync_rollback_hook, ctx);
    1501              :     
    1502              :     // register eponymous only changes virtual table
    1503          233 :     rc = cloudsync_vtab_register_changes (db, data);
    1504          233 :     if (rc != SQLITE_OK) {
    1505            0 :         if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Error creating changes virtual table: %s", sqlite3_errmsg(db));
    1506            0 :         return rc;
    1507              :     }
    1508              :     
    1509              :     // load config, if exists
    1510          233 :     if (cloudsync_config_exists(data)) {
    1511            4 :         if (cloudsync_context_init(data) == NULL) {
    1512              :             // Do not free ctx here: it is already owned by the cloudsync_version
    1513              :             // function (registered above with cloudsync_context_free as its
    1514              :             // destructor). SQLite will release it when the connection is closed.
    1515              :             // Freeing it manually would cause a double-free on sqlite3_close.
    1516            0 :             if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("An error occurred while trying to initialize context");
    1517            0 :             return SQLITE_ERROR;
    1518              :         }
    1519              :         
    1520              :         // update schema hash if upgrading from an older version
    1521            4 :         if (dbutils_settings_check_version(data, NULL) != 0) {
    1522            0 :             cloudsync_update_schema_hash(data);
    1523            0 :         }
    1524              : 
    1525              :         // make sure to update internal version to current version
    1526            4 :         dbutils_settings_set_key_value(data, CLOUDSYNC_KEY_LIBVERSION, CLOUDSYNC_VERSION);
    1527            4 :     }
    1528              :     
    1529          233 :     return SQLITE_OK;
    1530          235 : }
    1531              : 
    1532              : // MARK: - Main Entrypoint -
    1533              : 
    1534          235 : APIEXPORT int sqlite3_cloudsync_init (sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
    1535              :     DEBUG_FUNCTION("sqlite3_cloudsync_init");
    1536              :     
    1537              :     #ifndef SQLITE_CORE
    1538              :     SQLITE_EXTENSION_INIT2(pApi);
    1539              :     #endif
    1540              :     
    1541          235 :     return dbsync_register_functions(db, pzErrMsg);
    1542              : }
        

Generated by: LCOV version 2.4-0