LCOV - code coverage report
Current view: top level - src - cloudsync.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 84.1 % 1912 1608
Test Date: 2026-03-06 23:11:47 Functions: 95.5 % 110 105

            Line data    Source code
       1              : //
       2              : //  cloudsync.c
       3              : //  cloudsync
       4              : //
       5              : //  Created by Marco Bambini on 16/05/24.
       6              : //
       7              : 
       8              : #include <inttypes.h>
       9              : #include <stdbool.h>
      10              : #include <limits.h>
      11              : #include <stdint.h>
      12              : #include <stdlib.h>
      13              : #include <string.h>
      14              : #include <assert.h>
      15              : #include <stdio.h>
      16              : #include <errno.h>
      17              : #include <math.h>
      18              : 
      19              : #include "cloudsync.h"
      20              : #include "cloudsync_private.h"
      21              : #include "lz4.h"
      22              : #include "pk.h"
      23              : #include "vtab.h"
      24              : #include "utils.h"
      25              : #include "dbutils.h"
      26              : 
      27              : #ifndef CLOUDSYNC_OMIT_NETWORK
      28              : #include "network.h"
      29              : #endif
      30              : 
      31              : #ifdef _WIN32
      32              : #include <winsock2.h>
      33              : #include <ws2tcpip.h>
      34              : #else
      35              : #include <arpa/inet.h>                          // for htonl, htons, ntohl, ntohs
      36              : #include <netinet/in.h>                         // for struct sockaddr_in, INADDR_ANY, etc. (if needed)
      37              : #endif
      38              : 
      39              : #ifndef htonll
      40              : #if __BIG_ENDIAN__
      41              : #define htonll(x)                               (x)
      42              : #define ntohll(x)                               (x)
      43              : #else
      44              : #ifndef htobe64
      45              : #define htonll(x)                               ((uint64_t)htonl((x) & 0xFFFFFFFF) << 32 | (uint64_t)htonl((x) >> 32))
      46              : #define ntohll(x)                               ((uint64_t)ntohl((x) & 0xFFFFFFFF) << 32 | (uint64_t)ntohl((x) >> 32))
      47              : #else
      48              : #define htonll(x)                               htobe64(x)
      49              : #define ntohll(x)                               be64toh(x)
      50              : #endif
      51              : #endif
      52              : #endif
      53              : 
      54              : #ifndef SQLITE_CORE
      55              : SQLITE_EXTENSION_INIT1
      56              : #endif
      57              : 
      58              : #ifndef UNUSED_PARAMETER
      59              : #define UNUSED_PARAMETER(X) (void)(X)
      60              : #endif
      61              : 
      62              : #ifdef _WIN32
      63              : #define APIEXPORT   __declspec(dllexport)
      64              : #else
      65              : #define APIEXPORT
      66              : #endif
      67              : 
      68              : #define CLOUDSYNC_DEFAULT_ALGO                  "cls"
      69              : #define CLOUDSYNC_INIT_NTABLES                  128
      70              : #define CLOUDSYNC_VALUE_NOTSET                  -1
      71              : #define CLOUDSYNC_MIN_DB_VERSION                0
      72              : 
      73              : #define CLOUDSYNC_PAYLOAD_MINBUF_SIZE           512*1024
      74              : #define CLOUDSYNC_PAYLOAD_VERSION               1
      75              : #define CLOUDSYNC_PAYLOAD_SIGNATURE             'CLSY'
      76              : #define CLOUDSYNC_PAYLOAD_APPLY_CALLBACK_KEY    "cloudsync_payload_apply_callback"
      77              : 
      78              : #ifndef MAX
      79              : #define MAX(a, b)                               (((a)>(b))?(a):(b))
      80              : #endif
      81              : 
      82              : #define DEBUG_SQLITE_ERROR(_rc, _fn, _db)   do {if (_rc != SQLITE_OK) printf("Error in %s: %s\n", _fn, sqlite3_errmsg(_db));} while (0)
      83              : 
      84              : typedef enum {
      85              :     CLOUDSYNC_PK_INDEX_TBL          = 0,
      86              :     CLOUDSYNC_PK_INDEX_PK           = 1,
      87              :     CLOUDSYNC_PK_INDEX_COLNAME      = 2,
      88              :     CLOUDSYNC_PK_INDEX_COLVALUE     = 3,
      89              :     CLOUDSYNC_PK_INDEX_COLVERSION   = 4,
      90              :     CLOUDSYNC_PK_INDEX_DBVERSION    = 5,
      91              :     CLOUDSYNC_PK_INDEX_SITEID       = 6,
      92              :     CLOUDSYNC_PK_INDEX_CL           = 7,
      93              :     CLOUDSYNC_PK_INDEX_SEQ          = 8
      94              : } CLOUDSYNC_PK_INDEX;
      95              : 
      96              : typedef enum {
      97              :     CLOUDSYNC_STMT_VALUE_ERROR      = -1,
      98              :     CLOUDSYNC_STMT_VALUE_UNCHANGED  = 0,
      99              :     CLOUDSYNC_STMT_VALUE_CHANGED    = 1,
     100              : } CLOUDSYNC_STMT_VALUE;
     101              : 
     102              : typedef struct {
     103              :     sqlite3_context *context;
     104              :     int             index;
     105              : } cloudsync_pk_decode_context;
     106              : 
     107              : #define SYNCBIT_SET(_data)                  _data->insync = 1
     108              : #define SYNCBIT_RESET(_data)                _data->insync = 0
     109              : #define BUMP_SEQ(_data)                     ((_data)->seq += 1, (_data)->seq - 1)
     110              : 
     111              : // MARK: -
     112              : 
     113              : typedef struct {
     114              :     table_algo      algo;                           // CRDT algoritm associated to the table
     115              :     char            *name;                          // table name
     116              :     char            **col_name;                     // array of column names
     117              :     sqlite3_stmt    **col_merge_stmt;               // array of merge insert stmt (indexed by col_name)
     118              :     sqlite3_stmt    **col_value_stmt;               // array of column value stmt (indexed by col_name)
     119              :     int             *col_id;                        // array of column id
     120              :     int             ncols;                          // number of non primary key cols
     121              :     int             npks;                           // number of primary key cols
     122              :     bool            enabled;                        // flag to check if a table is enabled or disabled
     123              :     #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     124              :     bool            rowid_only;                     // a table with no primary keys other than the implicit rowid
     125              :     #endif
     126              :     
     127              :     char            **pk_name;                      // array of primary key names
     128              :     
     129              :     // precompiled statements
     130              :     sqlite3_stmt    *meta_pkexists_stmt;            // check if a primary key already exist in the augmented table
     131              :     sqlite3_stmt    *meta_sentinel_update_stmt;     // update a local sentinel row
     132              :     sqlite3_stmt    *meta_sentinel_insert_stmt;     // insert a local sentinel row
     133              :     sqlite3_stmt    *meta_row_insert_update_stmt;   // insert/update a local row
     134              :     sqlite3_stmt    *meta_row_drop_stmt;            // delete rows from meta
     135              :     sqlite3_stmt    *meta_update_move_stmt;         // update rows in meta when pk changes
     136              :     sqlite3_stmt    *meta_local_cl_stmt;            // compute local cl value
     137              :     sqlite3_stmt    *meta_winner_clock_stmt;        // get the rowid of the last inserted/updated row in the meta table
     138              :     sqlite3_stmt    *meta_merge_delete_drop;
     139              :     sqlite3_stmt    *meta_zero_clock_stmt;
     140              :     sqlite3_stmt    *meta_col_version_stmt;
     141              :     sqlite3_stmt    *meta_site_id_stmt;
     142              :     
     143              :     sqlite3_stmt    *real_col_values_stmt;          // retrieve all column values based on pk
     144              :     sqlite3_stmt    *real_merge_delete_stmt;
     145              :     sqlite3_stmt    *real_merge_sentinel_stmt;
     146              : 
     147              :     bool            is_altering;
     148              : 
     149              : } cloudsync_table_context;
     150              : 
     151              : struct cloudsync_pk_decode_bind_context {
     152              :     sqlite3_stmt    *vm;
     153              :     char            *tbl;
     154              :     int64_t         tbl_len;
     155              :     const void      *pk;
     156              :     int64_t         pk_len;
     157              :     char            *col_name;
     158              :     int64_t         col_name_len;
     159              :     int64_t         col_version;
     160              :     int64_t         db_version;
     161              :     const void      *site_id;
     162              :     int64_t         site_id_len;
     163              :     int64_t         cl;
     164              :     int64_t         seq;
     165              : };
     166              : 
     167              : struct cloudsync_context {
     168              :     sqlite3_context *sqlite_ctx;
     169              :     
     170              :     char            *libversion;
     171              :     uint8_t         site_id[UUID_LEN];
     172              :     int             insync;
     173              :     int             debug;
     174              :     bool            merge_equal_values;
     175              :     bool            temp_bool;                  // temporary value used in callback
     176              :     void            *aux_data;
     177              :     
     178              :     // stmts and context values
     179              :     bool            pragma_checked;             // we need to check PRAGMAs only once per transaction
     180              :     sqlite3_stmt    *schema_version_stmt;
     181              :     sqlite3_stmt    *data_version_stmt;
     182              :     sqlite3_stmt    *db_version_stmt;
     183              :     sqlite3_stmt    *getset_siteid_stmt;
     184              :     int             data_version;
     185              :     int             schema_version;
     186              :     uint64_t        schema_hash;
     187              :     
     188              :     // set at the start of each transaction on the first invocation and
     189              :     // re-set on transaction commit or rollback
     190              :     sqlite3_int64   db_version;
     191              :     // the version that the db will be set to at the end of the transaction
     192              :     // if that transaction were to commit at the time this value is checked
     193              :     sqlite3_int64   pending_db_version;
     194              :     // used to set an order inside each transaction
     195              :     int             seq;
     196              :     
     197              :     // augmented tables are stored in-memory so we do not need to retrieve information about col names and cid
     198              :     // from the disk each time a write statement is performed
     199              :     // we do also not need to use an hash map here because for few tables the direct in-memory comparison with table name is faster
     200              :     cloudsync_table_context **tables;
     201              :     int tables_count;
     202              :     int tables_alloc;
     203              : };
     204              : 
     205              : typedef struct {
     206              :     char        *buffer;
     207              :     size_t      balloc;
     208              :     size_t      bused;
     209              :     uint64_t    nrows;
     210              :     uint16_t    ncols;
     211              : } cloudsync_data_payload;
     212              : 
     213              : #ifdef _MSC_VER
     214              :     #pragma pack(push, 1) // For MSVC: pack struct with 1-byte alignment
     215              :     #define PACKED
     216              : #else
     217              :     #define PACKED __attribute__((__packed__))
     218              : #endif
     219              : 
     220              : typedef struct PACKED {
     221              :     uint32_t    signature;         // 'CLSY'
     222              :     uint8_t     version;           // protocol version
     223              :     uint8_t     libversion[3];     // major.minor.patch
     224              :     uint32_t    expanded_size;
     225              :     uint16_t    ncols;
     226              :     uint32_t    nrows;
     227              :     uint64_t    schema_hash;
     228              :     uint8_t     unused[6];        // padding to ensure the struct is exactly 32 bytes
     229              : } cloudsync_payload_header;
     230              : 
     231              : typedef struct {
     232              :     sqlite3_value   *table_name;
     233              :     sqlite3_value   **new_values;
     234              :     sqlite3_value   **old_values;
     235              :     int             count;
     236              :     int             capacity;
     237              : } cloudsync_update_payload;
     238              : 
     239              : #ifdef _MSC_VER
     240              :     #pragma pack(pop)
     241              : #endif
     242              : 
     243              : #if CLOUDSYNC_UNITTEST
     244              : bool force_uncompressed_blob = false;
     245              : #define CHECK_FORCE_UNCOMPRESSED_BUFFER()   if (force_uncompressed_blob) use_uncompressed_buffer = true
     246              : #else
     247              : #define CHECK_FORCE_UNCOMPRESSED_BUFFER()
     248              : #endif
     249              : 
     250              : int db_version_rebuild_stmt (sqlite3 *db, cloudsync_context *data);
     251              : int cloudsync_load_siteid (sqlite3 *db, cloudsync_context *data);
     252              : int local_mark_insert_or_update_meta (sqlite3 *db, cloudsync_table_context *table, const char *pk, size_t pklen, const char *col_name, sqlite3_int64 db_version, int seq);
     253              : 
     254              : // MARK: - STMT Utils -
     255              : 
     256        30480 : CLOUDSYNC_STMT_VALUE stmt_execute (sqlite3_stmt *stmt, cloudsync_context *data) {
     257        30480 :     int rc = sqlite3_step(stmt);
     258        30480 :     if (rc != SQLITE_ROW && rc != SQLITE_DONE) {
     259            2 :         if (data) DEBUG_SQLITE_ERROR(rc, "stmt_execute", sqlite3_db_handle(stmt));
     260            2 :         sqlite3_reset(stmt);
     261            2 :         return CLOUDSYNC_STMT_VALUE_ERROR;
     262              :     }
     263              :     
     264        30478 :     CLOUDSYNC_STMT_VALUE result = CLOUDSYNC_STMT_VALUE_CHANGED;
     265        30478 :     if (stmt == data->data_version_stmt) {
     266        28660 :         int version = sqlite3_column_int(stmt, 0);
     267        28660 :         if (version != data->data_version) {
     268          108 :             data->data_version = version;
     269          108 :         } else {
     270        28552 :             result = CLOUDSYNC_STMT_VALUE_UNCHANGED;
     271              :         }
     272        30478 :     } else if (stmt == data->schema_version_stmt) {
     273          909 :         int version = sqlite3_column_int(stmt, 0);
     274          909 :         if (version > data->schema_version) {
     275          166 :             data->schema_version = version;
     276          166 :         } else {
     277          743 :             result = CLOUDSYNC_STMT_VALUE_UNCHANGED;
     278              :         }
     279              :         
     280         1818 :     } else if (stmt == data->db_version_stmt) {
     281          909 :         data->db_version = (rc == SQLITE_DONE) ? CLOUDSYNC_MIN_DB_VERSION : sqlite3_column_int64(stmt, 0);
     282          909 :     }
     283              :     
     284        30478 :     sqlite3_reset(stmt);
     285        30478 :     return result;
     286        30480 : }
     287              : 
     288         3469 : int stmt_count (sqlite3_stmt *stmt, const char *value, size_t len, int type) {
     289         3469 :     int result = -1;
     290         3469 :     int rc = SQLITE_OK;
     291              :     
     292         3469 :     if (value) {
     293         3468 :         rc = (type == SQLITE_TEXT) ? sqlite3_bind_text(stmt, 1, value, (int)len, SQLITE_STATIC) : sqlite3_bind_blob(stmt, 1, value, (int)len, SQLITE_STATIC);
     294         3468 :         if (rc != SQLITE_OK) goto cleanup;
     295         3468 :     }
     296              : 
     297         3469 :     rc = sqlite3_step(stmt);
     298         6938 :     if (rc == SQLITE_DONE) {
     299            1 :         result = 0;
     300            1 :         rc = SQLITE_OK;
     301         3469 :     } else if (rc == SQLITE_ROW) {
     302         3468 :         result = sqlite3_column_int(stmt, 0);
     303         3468 :         rc = SQLITE_OK;
     304         3468 :     }
     305              :     
     306              : cleanup:
     307         3469 :     DEBUG_SQLITE_ERROR(rc, "stmt_count", sqlite3_db_handle(stmt));
     308         3469 :     sqlite3_reset(stmt);
     309         3469 :     return result;
     310              : }
     311              : 
     312       236202 : sqlite3_stmt *stmt_reset (sqlite3_stmt *stmt) {
     313       236202 :     sqlite3_clear_bindings(stmt);
     314       236202 :     sqlite3_reset(stmt);
     315       236202 :     return NULL;
     316              : }
     317              : 
     318          279 : int stmts_add_tocontext (sqlite3 *db, cloudsync_context *data) {
     319              :     DEBUG_DBFUNCTION("cloudsync_add_stmts");
     320              :     
     321          279 :     if (data->data_version_stmt == NULL) {
     322          109 :         const char *sql = "PRAGMA data_version;";
     323          109 :         int rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &data->data_version_stmt, NULL);
     324              :         DEBUG_STMT("data_version_stmt %p", data->data_version_stmt);
     325          109 :         if (rc != SQLITE_OK) return rc;
     326              :         DEBUG_SQL("data_version_stmt: %s", sql);
     327          109 :     }
     328              :     
     329          279 :     if (data->schema_version_stmt == NULL) {
     330          109 :         const char *sql = "PRAGMA schema_version;";
     331          109 :         int rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &data->schema_version_stmt, NULL);
     332              :         DEBUG_STMT("schema_version_stmt %p", data->schema_version_stmt);
     333          109 :         if (rc != SQLITE_OK) return rc;
     334              :         DEBUG_SQL("schema_version_stmt: %s", sql);
     335          109 :     }
     336              :     
     337          279 :     if (data->getset_siteid_stmt == NULL) {
     338              :         // get and set index of the site_id
     339              :         // in SQLite, we can’t directly combine an INSERT and a SELECT to both insert a row and return an identifier (rowid) in a single statement,
     340              :         // however, we can use a workaround by leveraging the INSERT statement with ON CONFLICT DO UPDATE and then combining it with RETURNING rowid
     341          109 :         const char *sql = "INSERT INTO cloudsync_site_id (site_id) VALUES (?) ON CONFLICT(site_id) DO UPDATE SET site_id = site_id RETURNING rowid;";
     342          109 :         int rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &data->getset_siteid_stmt, NULL);
     343              :         DEBUG_STMT("getset_siteid_stmt %p", data->getset_siteid_stmt);
     344          109 :         if (rc != SQLITE_OK) return rc;
     345              :         DEBUG_SQL("getset_siteid_stmt: %s", sql);
     346          109 :     }
     347              :     
     348          279 :     return db_version_rebuild_stmt(db, data);
     349          279 : }
     350              : 
     351              : // MARK: - Database Version -
     352              : 
     353          335 : char *db_version_build_query (sqlite3 *db) {
     354              :     // this function must be manually called each time tables changes
     355              :     // because the query plan changes too and it must be re-prepared
     356              :     // unfortunately there is no other way
     357              :     
     358              :     // we need to execute a query like:
     359              :     /*
     360              :      SELECT max(version) as version FROM (
     361              :          SELECT max(db_version) as version FROM "table1_cloudsync"
     362              :          UNION ALL
     363              :          SELECT max(db_version) as version FROM "table2_cloudsync"
     364              :          UNION ALL
     365              :          SELECT max(db_version) as version FROM "table3_cloudsync"
     366              :          UNION
     367              :          SELECT value as version FROM cloudsync_settings WHERE key = 'pre_alter_dbversion'
     368              :      )
     369              :      */
     370              :     
     371              :     // the good news is that the query can be computed in SQLite without the need to do any extra computation from the host language
     372          335 :     const char *sql = "WITH table_names AS ("
     373              :                       "SELECT format('%w', name) as tbl_name "
     374              :                       "FROM sqlite_master "
     375              :                       "WHERE type='table' "
     376              :                       "AND name LIKE '%_cloudsync'"
     377              :                       "), "
     378              :                       "query_parts AS ("
     379              :                       "SELECT 'SELECT max(db_version) as version FROM \"' || tbl_name || '\"' as part FROM table_names"
     380              :                       "), "
     381              :                       "combined_query AS ("
     382              :                       "SELECT GROUP_CONCAT(part, ' UNION ALL ') || ' UNION SELECT value as version FROM cloudsync_settings WHERE key = ''pre_alter_dbversion''' as full_query FROM query_parts"
     383              :                       ") "
     384              :                       "SELECT 'SELECT max(version) as version FROM (' || full_query || ');' FROM combined_query;";
     385          335 :     return dbutils_text_select(db, sql);
     386              : }
     387              : 
     388          445 : int db_version_rebuild_stmt (sqlite3 *db, cloudsync_context *data) {
     389          445 :     if (data->db_version_stmt) {
     390          226 :         sqlite3_finalize(data->db_version_stmt);
     391          226 :         data->db_version_stmt = NULL;
     392          226 :     }
     393              :     
     394          445 :     sqlite3_int64 count = dbutils_table_settings_count_tables(db);
     395          445 :     if (count == 0) return SQLITE_OK;
     396          335 :     else if (count == -1) {
     397            0 :         dbutils_context_result_error(data->sqlite_ctx, "%s", sqlite3_errmsg(db));
     398            0 :         return SQLITE_ERROR;
     399              :     }
     400              :     
     401          335 :     char *sql = db_version_build_query(db);
     402          335 :     if (!sql) return SQLITE_NOMEM;
     403              :     DEBUG_SQL("db_version_stmt: %s", sql);
     404              :     
     405          335 :     int rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &data->db_version_stmt, NULL);
     406              :     DEBUG_STMT("db_version_stmt %p", data->db_version_stmt);
     407          335 :     cloudsync_memory_free(sql);
     408          335 :     return rc;
     409          445 : }
     410              : 
     411          909 : int db_version_rerun (sqlite3 *db, cloudsync_context *data) {
     412          909 :     CLOUDSYNC_STMT_VALUE schema_changed = stmt_execute(data->schema_version_stmt, data);
     413          909 :     if (schema_changed == CLOUDSYNC_STMT_VALUE_ERROR) return -1;
     414              :     
     415          909 :     if (schema_changed == CLOUDSYNC_STMT_VALUE_CHANGED) {
     416          166 :         int rc = db_version_rebuild_stmt(db, data);
     417          166 :         if (rc != SQLITE_OK) return -1;
     418          166 :     }
     419              :     
     420          909 :     CLOUDSYNC_STMT_VALUE rc = stmt_execute(data->db_version_stmt, data);
     421          909 :     if (rc == CLOUDSYNC_STMT_VALUE_ERROR) return -1;
     422          909 :     return 0;
     423          909 : }
     424              : 
     425        28660 : int db_version_check_uptodate (sqlite3 *db, cloudsync_context *data) {
     426              :     // perform a PRAGMA data_version to check if some other process write any data
     427        28660 :     CLOUDSYNC_STMT_VALUE rc = stmt_execute(data->data_version_stmt, data);
     428        28660 :     if (rc == CLOUDSYNC_STMT_VALUE_ERROR) return -1;
     429              :     
     430              :     // db_version is already set and there is no need to update it
     431        28660 :     if (data->db_version != CLOUDSYNC_VALUE_NOTSET && rc == CLOUDSYNC_STMT_VALUE_UNCHANGED) return 0;
     432              :     
     433          909 :     return db_version_rerun(db, data);
     434        28660 : }
     435              : 
     436        28634 : sqlite3_int64 db_version_next (sqlite3 *db, cloudsync_context *data, sqlite3_int64 merging_version) {
     437        28634 :     int rc = db_version_check_uptodate(db, data);
     438        28634 :     if (rc != SQLITE_OK) return -1;
     439              :     
     440        28634 :     sqlite3_int64 result = data->db_version + 1;
     441        28634 :     if (result < data->pending_db_version) result = data->pending_db_version;
     442        28634 :     if (merging_version != CLOUDSYNC_VALUE_NOTSET && result < merging_version) result = merging_version;
     443        28634 :     data->pending_db_version = result;
     444              :     
     445        28634 :     return result;
     446        28634 : }
     447              : 
     448              : // MARK: -
     449              : 
     450            0 : void *cloudsync_get_auxdata (sqlite3_context *context) {
     451            0 :     cloudsync_context *data = (context) ? (cloudsync_context *)sqlite3_user_data(context) : NULL;
     452            0 :     return (data) ? data->aux_data : NULL;
     453              : }
     454              : 
     455            0 : void cloudsync_set_auxdata (sqlite3_context *context, void *xdata) {
     456            0 :     cloudsync_context *data = (context) ? (cloudsync_context *)sqlite3_user_data(context) : NULL;
     457            0 :     if (data) data->aux_data = xdata;
     458            0 : }
     459              : 
     460              : // MARK: - PK Context -
     461              : 
     462        16416 : char *cloudsync_pk_context_tbl (cloudsync_pk_decode_bind_context *ctx, int64_t *tbl_len) {
     463        16416 :     *tbl_len = ctx->tbl_len;
     464        16416 :     return ctx->tbl;
     465              : }
     466              : 
     467        16416 : void *cloudsync_pk_context_pk (cloudsync_pk_decode_bind_context *ctx, int64_t *pk_len) {
     468        16416 :     *pk_len = ctx->pk_len;
     469        16416 :     return (void *)ctx->pk;
     470              : }
     471              : 
     472        16416 : char *cloudsync_pk_context_colname (cloudsync_pk_decode_bind_context *ctx, int64_t *colname_len) {
     473        16416 :     *colname_len = ctx->col_name_len;
     474        16416 :     return ctx->col_name;
     475              : }
     476              : 
     477        16416 : int64_t cloudsync_pk_context_cl (cloudsync_pk_decode_bind_context *ctx) {
     478        16416 :     return ctx->cl;
     479              : }
     480              : 
     481        16416 : int64_t cloudsync_pk_context_dbversion (cloudsync_pk_decode_bind_context *ctx) {
     482        16416 :     return ctx->db_version;
     483              : }
     484              : 
     485              : // MARK: - Table Utils -
     486              : 
     487          128 : char *table_build_values_sql (sqlite3 *db, cloudsync_table_context *table) {
     488          128 :     char *sql = NULL;
     489              :     
     490              :     /*
     491              :     This SQL statement dynamically generates a SELECT query for a specified table.
     492              :     It uses Common Table Expressions (CTEs) to construct the column names and
     493              :     primary key conditions based on the table schema, which is obtained through
     494              :     the `pragma_table_info` function.
     495              : 
     496              :     1. `col_names` CTE:
     497              :        - Retrieves a comma-separated list of non-primary key column names from
     498              :          the specified table's schema.
     499              : 
     500              :     2. `pk_where` CTE:
     501              :        - Retrieves a condition string representing the primary key columns in the
     502              :          format: "column1=? AND column2=? AND ...", used to create the WHERE clause
     503              :          for selecting rows based on primary key values.
     504              : 
     505              :     3. Final SELECT:
     506              :        - Constructs the complete SELECT statement as a string, combining:
     507              :          - Column names from `col_names`.
     508              :          - The target table name.
     509              :          - The WHERE clause conditions from `pk_where`.
     510              : 
     511              :     The resulting query can be used to select rows from the table based on primary
     512              :     key values, and can be executed within the application to retrieve data dynamically.
     513              :     */
     514              : 
     515              :     // Unfortunately in SQLite column names (or table names) cannot be bound parameters in a SELECT statement
     516              :     // otherwise we should have used something like SELECT 'SELECT ? FROM %w WHERE rowid=?';
     517              : 
     518          128 :     char *singlequote_escaped_table_name = cloudsync_memory_mprintf("%q", table->name);
     519              : 
     520              :     #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     521              :     if (table->rowid_only) {
     522              :         sql = memory_mprintf("WITH col_names AS (SELECT group_concat('\"' || format('%%w', name) || '\"', ',') AS cols FROM pragma_table_info('%q') WHERE pk=0 ORDER BY cid) SELECT 'SELECT ' || (SELECT cols FROM col_names) || ' FROM \"%w\" WHERE rowid=?;'", table->name, table->name);
     523              :         goto process_process;
     524              :     }
     525              :     #endif
     526              :     
     527          128 :     sql = cloudsync_memory_mprintf("WITH col_names AS (SELECT group_concat('\"' || format('%%w', name) || '\"', ',') AS cols FROM pragma_table_info('%q') WHERE pk=0 ORDER BY cid), pk_where AS (SELECT group_concat('\"' || format('%%w', name) || '\"', '=? AND ') || '=?' AS pk_clause FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk) SELECT 'SELECT ' || (SELECT cols FROM col_names) || ' FROM \"%w\" WHERE ' || (SELECT pk_clause FROM pk_where) || ';'", table->name, table->name, singlequote_escaped_table_name);
     528              :     
     529              : #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     530              : process_process:
     531              : #endif
     532          128 :     cloudsync_memory_free(singlequote_escaped_table_name);
     533          128 :     if (!sql) return NULL;
     534          128 :     char *query = dbutils_text_select(db, sql);
     535          128 :     cloudsync_memory_free(sql);
     536              :     
     537          128 :     return query;
     538          128 : }
     539              : 
     540          163 : char *table_build_mergedelete_sql (sqlite3 *db, cloudsync_table_context *table) {
     541              :     #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     542              :     if (table->rowid_only) {
     543              :         char *sql = memory_mprintf("DELETE FROM \"%w\" WHERE rowid=?;", table->name);
     544              :         return sql;
     545              :     }
     546              :     #endif
     547              :     
     548          163 :     char *singlequote_escaped_table_name = cloudsync_memory_mprintf("%q", table->name);
     549          163 :     char *sql = cloudsync_memory_mprintf("WITH pk_where AS (SELECT group_concat('\"' || format('%%w', name) || '\"', '=? AND ') || '=?' AS pk_clause FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk) SELECT 'DELETE FROM \"%w\" WHERE ' || (SELECT pk_clause FROM pk_where) || ';'", table->name, singlequote_escaped_table_name);
     550          163 :     cloudsync_memory_free(singlequote_escaped_table_name);
     551          163 :     if (!sql) return NULL;
     552              :     
     553          163 :     char *query = dbutils_text_select(db, sql);
     554          163 :     cloudsync_memory_free(sql);
     555              :     
     556          163 :     return query;
     557          163 : }
     558              : 
     559         1100 : char *table_build_mergeinsert_sql (sqlite3 *db, cloudsync_table_context *table, const char *colname) {
     560         1100 :     char *sql = NULL;
     561              :     
     562              :     #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     563              :     if (table->rowid_only) {
     564              :         if (colname == NULL) {
     565              :             // INSERT OR IGNORE INTO customers (first_name,last_name) VALUES (?,?);
     566              :             sql = memory_mprintf("INSERT OR IGNORE INTO \"%w\" (rowid) VALUES (?);", table->name);
     567              :         } else {
     568              :             // INSERT INTO customers (first_name,last_name,age) VALUES (?,?,?) ON CONFLICT DO UPDATE SET age=?;
     569              :             sql = memory_mprintf("INSERT INTO \"%w\" (rowid, \"%w\") VALUES (?, ?) ON CONFLICT DO UPDATE SET \"%w\"=?;", table->name, colname, colname);
     570              :         }
     571              :         return sql;
     572              :     }
     573              :     #endif
     574              :     
     575         1100 :     char *singlequote_escaped_table_name = cloudsync_memory_mprintf("%q", table->name);
     576              :     
     577         1100 :     if (colname == NULL) {
     578              :         // is sentinel insert
     579          163 :         sql = cloudsync_memory_mprintf("WITH pk_where AS (SELECT group_concat('\"' || format('%%w', name) || '\"') AS pk_clause FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk), pk_bind AS (SELECT group_concat('?') AS pk_binding FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk) SELECT 'INSERT OR IGNORE INTO \"%w\" (' || (SELECT pk_clause FROM pk_where) || ') VALUES ('  || (SELECT pk_binding FROM pk_bind) || ');'", table->name, table->name, singlequote_escaped_table_name);
     580          163 :     } else {
     581          937 :         char *singlequote_escaped_col_name = cloudsync_memory_mprintf("%q", colname);
     582          937 :         sql = cloudsync_memory_mprintf("WITH pk_where AS (SELECT group_concat('\"' || format('%%w', name) || '\"') AS pk_clause FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk), pk_bind AS (SELECT group_concat('?') AS pk_binding FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk) SELECT 'INSERT INTO \"%w\" (' || (SELECT pk_clause FROM pk_where) || ',\"%w\") VALUES ('  || (SELECT pk_binding FROM pk_bind) || ',?) ON CONFLICT DO UPDATE SET \"%w\"=?;'", table->name, table->name, singlequote_escaped_table_name, singlequote_escaped_col_name, singlequote_escaped_col_name);
     583          937 :         cloudsync_memory_free(singlequote_escaped_col_name);
     584              : 
     585              :     }
     586         1100 :     cloudsync_memory_free(singlequote_escaped_table_name);
     587         1100 :     if (!sql) return NULL;
     588              :     
     589         1100 :     char *query = dbutils_text_select(db, sql);
     590         1100 :     cloudsync_memory_free(sql);
     591              :     
     592         1100 :     return query;
     593         1100 : }
     594              : 
     595          937 : char *table_build_value_sql (sqlite3 *db, cloudsync_table_context *table, const char *colname) {
     596          937 :     char *colnamequote = dbutils_is_star_table(colname) ? "" : "\"";
     597              : 
     598              :     #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     599              :     if (table->rowid_only) {
     600              :         char *sql = memory_mprintf("SELECT %s%w%s FROM \"%w\" WHERE rowid=?;", colnamequote, colname, colnamequote, table->name);
     601              :         return sql;
     602              :     }
     603              :     #endif
     604              :         
     605              :     // SELECT age FROM customers WHERE first_name=? AND last_name=?;
     606          937 :     char *singlequote_escaped_table_name = cloudsync_memory_mprintf("%q", table->name);
     607          937 :     char *singlequote_escaped_col_name = cloudsync_memory_mprintf("%q", colname);
     608          937 :     char *sql = cloudsync_memory_mprintf("WITH pk_where AS (SELECT group_concat('\"' || format('%%w', name) || '\"', '=? AND ') || '=?' AS pk_clause FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk) SELECT 'SELECT %s%w%s FROM \"%w\" WHERE ' || (SELECT pk_clause FROM pk_where) || ';'", table->name, colnamequote, singlequote_escaped_col_name, colnamequote, singlequote_escaped_table_name);
     609          937 :     cloudsync_memory_free(singlequote_escaped_col_name);
     610          937 :     cloudsync_memory_free(singlequote_escaped_table_name);
     611          937 :     if (!sql) return NULL;
     612              :     
     613          937 :     char *query = dbutils_text_select(db, sql);
     614          937 :     cloudsync_memory_free(sql);
     615              :     
     616          937 :     return query;
     617          937 : }
     618              :     
     619          163 : cloudsync_table_context *table_create (const char *name, table_algo algo) {
     620              :     DEBUG_DBFUNCTION("table_create %s", name);
     621              :     
     622          163 :     cloudsync_table_context *table = (cloudsync_table_context *)cloudsync_memory_zeroalloc(sizeof(cloudsync_table_context));
     623          163 :     if (!table) return NULL;
     624              :     
     625          163 :     table->algo = algo;
     626          163 :     table->name = cloudsync_string_dup(name, true);
     627          163 :     if (!table->name) {
     628            0 :         cloudsync_memory_free(table);
     629            0 :         return NULL;
     630              :     }
     631          163 :     table->enabled = true;
     632              :         
     633          163 :     return table;
     634          163 : }
     635              : 
     636          163 : void table_free (cloudsync_table_context *table) {
     637              :     DEBUG_DBFUNCTION("table_free %s", (table) ? (table->name) : "NULL");
     638          163 :     if (!table) return;
     639              :     
     640          163 :     if (table->ncols > 0) {
     641          128 :         if (table->col_name) {
     642         1065 :             for (int i=0; i<table->ncols; ++i) {
     643          937 :                 cloudsync_memory_free(table->col_name[i]);
     644          937 :             }
     645          128 :             cloudsync_memory_free(table->col_name);
     646          128 :         }
     647          128 :         if (table->col_merge_stmt) {
     648         1065 :             for (int i=0; i<table->ncols; ++i) {
     649          937 :                 sqlite3_finalize(table->col_merge_stmt[i]);
     650          937 :             }
     651          128 :             cloudsync_memory_free(table->col_merge_stmt);
     652          128 :         }
     653          128 :         if (table->col_value_stmt) {
     654         1065 :             for (int i=0; i<table->ncols; ++i) {
     655          937 :                 sqlite3_finalize(table->col_value_stmt[i]);
     656          937 :             }
     657          128 :             cloudsync_memory_free(table->col_value_stmt);
     658          128 :         }
     659          128 :         if (table->col_id) {
     660          128 :             cloudsync_memory_free(table->col_id);
     661          128 :         }
     662          128 :     }
     663              :     
     664          163 :     if (table->pk_name) sqlite3_free_table(table->pk_name);
     665          163 :     if (table->name) cloudsync_memory_free(table->name);
     666          163 :     if (table->meta_pkexists_stmt) sqlite3_finalize(table->meta_pkexists_stmt);
     667          163 :     if (table->meta_sentinel_update_stmt) sqlite3_finalize(table->meta_sentinel_update_stmt);
     668          163 :     if (table->meta_sentinel_insert_stmt) sqlite3_finalize(table->meta_sentinel_insert_stmt);
     669          163 :     if (table->meta_row_insert_update_stmt) sqlite3_finalize(table->meta_row_insert_update_stmt);
     670          163 :     if (table->meta_row_drop_stmt) sqlite3_finalize(table->meta_row_drop_stmt);
     671          163 :     if (table->meta_update_move_stmt) sqlite3_finalize(table->meta_update_move_stmt);
     672          163 :     if (table->meta_local_cl_stmt) sqlite3_finalize(table->meta_local_cl_stmt);
     673          163 :     if (table->meta_winner_clock_stmt) sqlite3_finalize(table->meta_winner_clock_stmt);
     674          163 :     if (table->meta_merge_delete_drop) sqlite3_finalize(table->meta_merge_delete_drop);
     675          163 :     if (table->meta_zero_clock_stmt) sqlite3_finalize(table->meta_zero_clock_stmt);
     676          163 :     if (table->meta_col_version_stmt) sqlite3_finalize(table->meta_col_version_stmt);
     677          163 :     if (table->meta_site_id_stmt) sqlite3_finalize(table->meta_site_id_stmt);
     678              :     
     679          163 :     if (table->real_col_values_stmt) sqlite3_finalize(table->real_col_values_stmt);
     680          163 :     if (table->real_merge_delete_stmt) sqlite3_finalize(table->real_merge_delete_stmt);
     681          163 :     if (table->real_merge_sentinel_stmt) sqlite3_finalize(table->real_merge_sentinel_stmt);
     682              :     
     683          163 :     cloudsync_memory_free(table);
     684          163 : }
     685              : 
     686          163 : int table_add_stmts (sqlite3 *db, cloudsync_table_context *table, int ncols) {
     687          163 :     int rc = SQLITE_OK;
     688          163 :     char *sql = NULL;
     689              :     
     690              :     // META TABLE statements
     691              :     
     692              :     // CREATE TABLE IF NOT EXISTS \"%w_cloudsync\" (pk BLOB NOT NULL, col_name TEXT NOT NULL, col_version INTEGER, db_version INTEGER, site_id INTEGER DEFAULT 0, seq INTEGER, PRIMARY KEY (pk, col_name));
     693              :     
     694              :     // precompile the pk exists statement
     695              :     // we do not need an index on the pk column because it is already covered by the fact that it is part of the prikeys
     696              :     // EXPLAIN QUERY PLAN reports: SEARCH table_name USING PRIMARY KEY (pk=?)
     697          163 :     sql = cloudsync_memory_mprintf("SELECT EXISTS(SELECT 1 FROM \"%w_cloudsync\" WHERE pk = ? LIMIT 1);", table->name);
     698          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     699              :     DEBUG_SQL("meta_pkexists_stmt: %s", sql);
     700              :     
     701          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_pkexists_stmt, NULL);
     702              :     
     703          163 :     cloudsync_memory_free(sql);
     704          163 :     if (rc != SQLITE_OK) goto cleanup;
     705              :     
     706              :     // precompile the update local sentinel statement
     707          163 :     sql = cloudsync_memory_mprintf("UPDATE \"%w_cloudsync\" SET col_version = CASE col_version %% 2 WHEN 0 THEN col_version + 1 ELSE col_version + 2 END, db_version = ?, seq = ?, site_id = 0 WHERE pk = ? AND col_name = '%s';", table->name, CLOUDSYNC_TOMBSTONE_VALUE);
     708          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     709              :     DEBUG_SQL("meta_sentinel_update_stmt: %s", sql);
     710              :     
     711          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_sentinel_update_stmt, NULL);
     712          163 :     cloudsync_memory_free(sql);
     713          163 :     if (rc != SQLITE_OK) goto cleanup;
     714              :     
     715              :     // precompile the insert local sentinel statement
     716          163 :     sql = cloudsync_memory_mprintf("INSERT INTO \"%w_cloudsync\" (pk, col_name, col_version, db_version, seq, site_id) SELECT ?, '%s', 1, ?, ?, 0 WHERE 1 ON CONFLICT DO UPDATE SET col_version = CASE col_version %% 2 WHEN 0 THEN col_version + 1 ELSE col_version + 2 END, db_version = ?, seq = ?, site_id = 0;", table->name, CLOUDSYNC_TOMBSTONE_VALUE);
     717          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     718              :     DEBUG_SQL("meta_sentinel_insert_stmt: %s", sql);
     719              :     
     720          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_sentinel_insert_stmt, NULL);
     721          163 :     cloudsync_memory_free(sql);
     722          163 :     if (rc != SQLITE_OK) goto cleanup;
     723              : 
     724              :     // precompile the insert/update local row statement
     725          163 :     sql = cloudsync_memory_mprintf("INSERT INTO \"%w_cloudsync\" (pk, col_name, col_version, db_version, seq, site_id ) SELECT ?, ?, ?, ?, ?, 0 WHERE 1 ON CONFLICT DO UPDATE SET col_version = col_version + 1, db_version = ?, seq = ?, site_id = 0;", table->name);
     726          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     727              :     DEBUG_SQL("meta_row_insert_update_stmt: %s", sql);
     728              :     
     729          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_row_insert_update_stmt, NULL);
     730          163 :     cloudsync_memory_free(sql);
     731          163 :     if (rc != SQLITE_OK) goto cleanup;
     732              :     
     733              :     // precompile the delete rows from meta
     734          163 :     sql = cloudsync_memory_mprintf("DELETE FROM \"%w_cloudsync\" WHERE pk=? AND col_name!='%s';", table->name, CLOUDSYNC_TOMBSTONE_VALUE);
     735          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     736              :     DEBUG_SQL("meta_row_drop_stmt: %s", sql);
     737              :     
     738          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_row_drop_stmt, NULL);
     739          163 :     cloudsync_memory_free(sql);
     740          163 :     if (rc != SQLITE_OK) goto cleanup;
     741              :     
     742              :     // precompile the update rows from meta when pk changes
     743              :     // see https://github.com/sqliteai/sqlite-sync/blob/main/docs/PriKey.md for more details
     744          163 :     sql = cloudsync_memory_mprintf("UPDATE OR REPLACE \"%w_cloudsync\" SET pk=?, db_version=?, col_version=1, seq=cloudsync_seq(), site_id=0 WHERE (pk=? AND col_name!='%s');", table->name, CLOUDSYNC_TOMBSTONE_VALUE);
     745          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     746              :     DEBUG_SQL("meta_update_move_stmt: %s", sql);
     747              :     
     748          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_update_move_stmt, NULL);
     749          163 :     cloudsync_memory_free(sql);
     750          163 :     if (rc != SQLITE_OK) goto cleanup;
     751              :     
     752              :     // local cl
     753          163 :     sql = cloudsync_memory_mprintf("SELECT COALESCE((SELECT col_version FROM \"%w_cloudsync\" WHERE pk=? AND col_name='%s'), (SELECT 1 FROM \"%w_cloudsync\" WHERE pk=?));", table->name, CLOUDSYNC_TOMBSTONE_VALUE, table->name);
     754          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     755              :     DEBUG_SQL("meta_local_cl_stmt: %s", sql);
     756              :     
     757          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_local_cl_stmt, NULL);
     758          163 :     cloudsync_memory_free(sql);
     759          163 :     if (rc != SQLITE_OK) goto cleanup;
     760              :     
     761              :     // rowid of the last inserted/updated row in the meta table
     762          163 :     sql = cloudsync_memory_mprintf("INSERT OR REPLACE INTO \"%w_cloudsync\" (pk, col_name, col_version, db_version, seq, site_id) VALUES (?, ?, ?, cloudsync_db_version_next(?), ?, ?) RETURNING ((db_version << 30) | seq);", table->name);
     763          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     764              :     DEBUG_SQL("meta_winner_clock_stmt: %s", sql);
     765              :     
     766          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_winner_clock_stmt, NULL);
     767          163 :     cloudsync_memory_free(sql);
     768          163 :     if (rc != SQLITE_OK) goto cleanup;
     769              :     
     770          163 :     sql = cloudsync_memory_mprintf("DELETE FROM \"%w_cloudsync\" WHERE pk=? AND col_name!='%s';", table->name, CLOUDSYNC_TOMBSTONE_VALUE);
     771          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     772              :     DEBUG_SQL("meta_merge_delete_drop: %s", sql);
     773              :     
     774          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_merge_delete_drop, NULL);
     775          163 :     cloudsync_memory_free(sql);
     776          163 :     if (rc != SQLITE_OK) goto cleanup;
     777              :     
     778              :     // zero clock
     779          163 :     sql = cloudsync_memory_mprintf("UPDATE \"%w_cloudsync\" SET col_version = 0, db_version = cloudsync_db_version_next(?) WHERE pk=? AND col_name!='%s';", table->name, CLOUDSYNC_TOMBSTONE_VALUE);
     780          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     781              :     DEBUG_SQL("meta_zero_clock_stmt: %s", sql);
     782              :     
     783          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_zero_clock_stmt, NULL);
     784          163 :     cloudsync_memory_free(sql);
     785          163 :     if (rc != SQLITE_OK) goto cleanup;
     786              :     
     787              :     // col_version
     788          163 :     sql = cloudsync_memory_mprintf("SELECT col_version FROM \"%w_cloudsync\" WHERE pk=? AND col_name=?;", table->name);
     789          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     790              :     DEBUG_SQL("meta_col_version_stmt: %s", sql);
     791              :     
     792          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_col_version_stmt, NULL);
     793          163 :     cloudsync_memory_free(sql);
     794          163 :     if (rc != SQLITE_OK) goto cleanup;
     795              :     
     796              :     // site_id
     797          163 :     sql = cloudsync_memory_mprintf("SELECT site_id FROM \"%w_cloudsync\" WHERE pk=? AND col_name=?;", table->name);
     798          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     799              :     DEBUG_SQL("meta_site_id_stmt: %s", sql);
     800              :     
     801          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->meta_site_id_stmt, NULL);
     802          163 :     cloudsync_memory_free(sql);
     803          163 :     if (rc != SQLITE_OK) goto cleanup;
     804              :     
     805              :     // REAL TABLE statements
     806              :     
     807              :     // precompile the get column value statement
     808          163 :     if (ncols > 0) {
     809          128 :         sql = table_build_values_sql(db, table);
     810          128 :         if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     811              :         DEBUG_SQL("real_col_values_stmt: %s", sql);
     812              :         
     813          128 :         rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->real_col_values_stmt, NULL);
     814          128 :         cloudsync_memory_free(sql);
     815          128 :         if (rc != SQLITE_OK) goto cleanup;
     816          128 :     }
     817              :     
     818          163 :     sql = table_build_mergedelete_sql(db, table);
     819          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     820              :     DEBUG_SQL("real_merge_delete: %s", sql);
     821              :     
     822          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->real_merge_delete_stmt, NULL);
     823          163 :     cloudsync_memory_free(sql);
     824          163 :     if (rc != SQLITE_OK) goto cleanup;
     825              :     
     826          163 :     sql = table_build_mergeinsert_sql(db, table, NULL);
     827          163 :     if (!sql) {rc = SQLITE_NOMEM; goto cleanup;}
     828              :     DEBUG_SQL("real_merge_sentinel: %s", sql);
     829              :     
     830          163 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->real_merge_sentinel_stmt, NULL);
     831          163 :     cloudsync_memory_free(sql);
     832          163 :     if (rc != SQLITE_OK) goto cleanup;
     833              :     
     834              : cleanup:
     835          163 :     if (rc != SQLITE_OK) printf("table_add_stmts error: %s\n", sqlite3_errmsg(db));
     836          163 :     return rc;
     837              : }
     838              : 
     839       188811 : cloudsync_table_context *table_lookup (cloudsync_context *data, const char *table_name) {
     840              :     DEBUG_DBFUNCTION("table_lookup %s", table_name);
     841              :     
     842       189739 :     for (int i=0; i<data->tables_count; ++i) {
     843       189556 :         const char *name = (data->tables[i]) ? data->tables[i]->name : NULL;
     844       189556 :         if ((name) && (strcasecmp(name, table_name) == 0)) {
     845       188628 :             return data->tables[i];
     846              :         }
     847          928 :     }
     848              :     
     849          183 :     return NULL;
     850       188811 : }
     851              : 
     852       172586 : sqlite3_stmt *table_column_lookup (cloudsync_table_context *table, const char *col_name, bool is_merge, int *index) {
     853              :     DEBUG_DBFUNCTION("table_column_lookup %s", col_name);
     854              :     
     855       318374 :     for (int i=0; i<table->ncols; ++i) {
     856       318374 :         if (strcasecmp(table->col_name[i], col_name) == 0) {
     857       172586 :             if (index) *index = i;
     858       172586 :             return (is_merge) ? table->col_merge_stmt[i] : table->col_value_stmt[i];
     859              :         }
     860       145788 :     }
     861              :     
     862            0 :     if (index) *index = -1;
     863            0 :     return NULL;
     864       172586 : }
     865              : 
     866           29 : int table_remove (cloudsync_context *data, const char *table_name) {
     867              :     DEBUG_DBFUNCTION("table_remove %s", table_name);
     868              :     
     869           52 :     for (int i=0; i<data->tables_count; ++i) {
     870           52 :         const char *name = (data->tables[i]) ? data->tables[i]->name : NULL;
     871           52 :         if ((name) && (strcasecmp(name, table_name) == 0)) {
     872           29 :             data->tables[i] = NULL;
     873           29 :             return i;
     874              :         }
     875           23 :     }
     876            0 :     return -1;
     877           29 : }
     878              : 
     879          937 : int table_add_to_context_cb (void *xdata, int ncols, char **values, char **names) {
     880          937 :     cloudsync_table_context *table = (cloudsync_table_context *)xdata;
     881              :     
     882          937 :     sqlite3 *db = sqlite3_db_handle(table->meta_pkexists_stmt);
     883          937 :     if (!db) return SQLITE_ERROR;
     884              :     
     885          937 :     int index = table->ncols;
     886         1874 :     for (int i=0; i<ncols; i+=2) {
     887          937 :         const char *name = values[i];
     888          937 :         int cid = (int)strtol(values[i+1], NULL, 0);
     889              :         
     890          937 :         table->col_id[index] = cid;
     891          937 :         table->col_name[index] = cloudsync_string_dup(name, true);
     892          937 :         if (!table->col_name[index]) return 1;
     893              :         
     894          937 :         char *sql = table_build_mergeinsert_sql(db, table, name);
     895          937 :         if (!sql) return SQLITE_NOMEM;
     896              :         DEBUG_SQL("col_merge_stmt[%d]: %s", index, sql);
     897              :         
     898          937 :         int rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->col_merge_stmt[index], NULL);
     899          937 :         cloudsync_memory_free(sql);
     900          937 :         if (rc != SQLITE_OK) return rc;
     901          937 :         if (!table->col_merge_stmt[index]) return SQLITE_MISUSE;
     902              :         
     903          937 :         sql = table_build_value_sql(db, table, name);
     904          937 :         if (!sql) return SQLITE_NOMEM;
     905              :         DEBUG_SQL("col_value_stmt[%d]: %s", index, sql);
     906              :         
     907          937 :         rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &table->col_value_stmt[index], NULL);
     908          937 :         cloudsync_memory_free(sql);
     909          937 :         if (rc != SQLITE_OK) return rc;
     910          937 :         if (!table->col_value_stmt[index]) return SQLITE_MISUSE;
     911          937 :     }
     912          937 :     table->ncols += 1;
     913              :     
     914          937 :     return 0;
     915          937 : }
     916              : 
     917          169 : bool table_add_to_context (sqlite3 *db, cloudsync_context *data, table_algo algo, const char *table_name) {
     918              :     DEBUG_DBFUNCTION("cloudsync_context_add_table %s", table_name);
     919              :     
     920              :     // check if table is already in the global context and in that case just return
     921          169 :     cloudsync_table_context *table = table_lookup(data, table_name);
     922          169 :     if (table) return true;
     923              :     
     924              :     // is there any space available?
     925          163 :     if (data->tables_alloc <= data->tables_count + 1) {
     926              :         // realloc tables
     927            0 :         cloudsync_table_context **clone = (cloudsync_table_context **)cloudsync_memory_realloc(data->tables, sizeof(cloudsync_table_context) * data->tables_alloc + CLOUDSYNC_INIT_NTABLES);
     928            0 :         if (!clone) goto abort_add_table;
     929              :         
     930              :         // reset new entries
     931            0 :         for (int i=data->tables_alloc; i<data->tables_alloc + CLOUDSYNC_INIT_NTABLES; ++i) {
     932            0 :             clone[i] = NULL;
     933            0 :         }
     934              :         
     935              :         // replace old ptr
     936            0 :         data->tables = clone;
     937            0 :         data->tables_alloc += CLOUDSYNC_INIT_NTABLES;
     938            0 :     }
     939              :     
     940              :     // setup a new table context
     941          163 :     table = table_create(table_name, algo);
     942          163 :     if (!table) return false;
     943              :     
     944              :     // fill remaining metadata in the table
     945          163 :     char *sql = cloudsync_memory_mprintf("SELECT count(*) FROM pragma_table_info('%q') WHERE pk>0;", table_name);
     946          163 :     if (!sql) goto abort_add_table;
     947          163 :     table->npks = (int)dbutils_int_select(db, sql);
     948          163 :     cloudsync_memory_free(sql);
     949          163 :     if (table->npks == -1) {
     950            0 :         dbutils_context_result_error(data->sqlite_ctx, "%s", sqlite3_errmsg(db));
     951            0 :         goto abort_add_table;
     952              :     }
     953              :     
     954          163 :     if (table->npks == 0) {
     955              :         #if CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     956            0 :         return false;
     957              :         #else
     958              :         table->rowid_only = true;
     959              :         table->npks = 1; // rowid
     960              :         #endif
     961              :     }
     962              :     
     963          163 :     sql = cloudsync_memory_mprintf("SELECT count(*) FROM pragma_table_info('%q') WHERE pk=0;", table_name);
     964          163 :     if (!sql) goto abort_add_table;
     965          163 :     int64_t ncols = (int64_t)dbutils_int_select(db, sql);
     966          163 :     cloudsync_memory_free(sql);
     967          163 :     if (ncols == -1) {
     968            0 :         dbutils_context_result_error(data->sqlite_ctx, "%s", sqlite3_errmsg(db));
     969            0 :         goto abort_add_table;
     970              :     }
     971              :     
     972          163 :     int rc = table_add_stmts(db, table, (int)ncols);
     973          163 :     if (rc != SQLITE_OK) goto abort_add_table;
     974              :     
     975              :     // a table with only pk(s) is totally legal
     976          163 :     if (ncols > 0) {
     977          128 :         table->col_name = (char **)cloudsync_memory_alloc((sqlite3_uint64)(sizeof(char *) * ncols));
     978          128 :         if (!table->col_name) goto abort_add_table;
     979              :         
     980          128 :         table->col_id = (int *)cloudsync_memory_alloc((sqlite3_uint64)(sizeof(int) * ncols));
     981          128 :         if (!table->col_id) goto abort_add_table;
     982              :         
     983          128 :         table->col_merge_stmt = (sqlite3_stmt **)cloudsync_memory_alloc((sqlite3_uint64)(sizeof(sqlite3_stmt *) * ncols));
     984          128 :         if (!table->col_merge_stmt) goto abort_add_table;
     985              :         
     986          128 :         table->col_value_stmt = (sqlite3_stmt **)cloudsync_memory_alloc((sqlite3_uint64)(sizeof(sqlite3_stmt *) * ncols));
     987          128 :         if (!table->col_value_stmt) goto abort_add_table;
     988              :         
     989          128 :         sql = cloudsync_memory_mprintf("SELECT name, cid FROM pragma_table_info('%q') WHERE pk=0 ORDER BY cid;", table_name);
     990          128 :         if (!sql) goto abort_add_table;
     991          128 :         int rc = sqlite3_exec(db, sql, table_add_to_context_cb, (void *)table, NULL);
     992          128 :         cloudsync_memory_free(sql);
     993          128 :         if (rc == SQLITE_ABORT) goto abort_add_table;
     994          128 :     }
     995              :     
     996              :     // lookup the first free slot
     997          212 :     for (int i=0; i<data->tables_alloc; ++i) {
     998          212 :         if (data->tables[i] == NULL) {
     999          163 :             data->tables[i] = table;
    1000          163 :             if (i > data->tables_count - 1) ++data->tables_count;
    1001          163 :             break;
    1002              :         }
    1003           49 :     }
    1004              :     
    1005          163 :     return true;
    1006              :     
    1007              : abort_add_table:
    1008            0 :     table_free(table);
    1009            0 :     return false;
    1010          169 : }
    1011              : 
    1012            6 : bool table_remove_from_context (cloudsync_context *data, cloudsync_table_context *table) {
    1013            6 :     return (table_remove(data, table->name) != -1);
    1014              : }
    1015              : 
    1016         3709 : sqlite3_stmt *cloudsync_colvalue_stmt (sqlite3 *db, cloudsync_context *data, const char *tbl_name, bool *persistent) {
    1017         3709 :     sqlite3_stmt *vm = NULL;
    1018              :     
    1019         3709 :     cloudsync_table_context *table = table_lookup(data, tbl_name);
    1020         3709 :     if (table) {
    1021         3709 :         char *col_name = NULL;
    1022         3709 :         if (table->ncols > 0) {
    1023         3709 :             col_name = table->col_name[0];
    1024              :             // retrieve col_value precompiled statement
    1025         3709 :             vm = table_column_lookup(table, col_name, false, NULL);
    1026         3709 :             *persistent = true;
    1027         3709 :         } else {
    1028            0 :             char *sql = table_build_value_sql(db, table, "*");
    1029            0 :             sqlite3_prepare_v2(db, sql, -1, &vm, NULL);
    1030            0 :             cloudsync_memory_free(sql);
    1031            0 :             *persistent = false;
    1032              :         }
    1033         3709 :     }
    1034              :     
    1035         3709 :     return vm;
    1036              : }
    1037              : 
    1038              : // MARK: - Merge Insert -
    1039              : 
    1040        45449 : sqlite3_int64 merge_get_local_cl (cloudsync_table_context *table, const char *pk, int pklen, const char **err) {
    1041        45449 :     sqlite3_stmt *vm = table->meta_local_cl_stmt;
    1042        45449 :     sqlite3_int64 result = -1;
    1043              :     
    1044        45449 :     int rc = sqlite3_bind_blob(vm, 1, (const void *)pk, pklen, SQLITE_STATIC);
    1045        45449 :     if (rc != SQLITE_OK) goto cleanup;
    1046              :     
    1047        45449 :     rc = sqlite3_bind_blob(vm, 2, (const void *)pk, pklen, SQLITE_STATIC);
    1048        45449 :     if (rc != SQLITE_OK) goto cleanup;
    1049              :     
    1050        45449 :     rc = sqlite3_step(vm);
    1051        45449 :     if (rc == SQLITE_ROW) result = sqlite3_column_int64(vm, 0);
    1052            0 :     else if (rc == SQLITE_DONE) result = 0;
    1053              :     
    1054              : cleanup:
    1055        45449 :     if (result == -1) *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1056        45449 :     stmt_reset(vm);
    1057        45449 :     return result;
    1058              : }
    1059              : 
    1060        44464 : int merge_get_col_version (cloudsync_table_context *table, const char *col_name, const char *pk, int pklen, sqlite3_int64 *version, const char **err) {
    1061        44464 :     sqlite3_stmt *vm = table->meta_col_version_stmt;
    1062              :     
    1063        44464 :     int rc = sqlite3_bind_blob(vm, 1, (const void *)pk, pklen, SQLITE_STATIC);
    1064        44464 :     if (rc != SQLITE_OK) goto cleanup;
    1065              :     
    1066        44464 :     rc = sqlite3_bind_text(vm, 2, col_name, -1, SQLITE_STATIC);
    1067        44464 :     if (rc != SQLITE_OK) goto cleanup;
    1068              :     
    1069        44464 :     rc = sqlite3_step(vm);
    1070        69228 :     if (rc == SQLITE_ROW) {
    1071        24764 :         *version = sqlite3_column_int64(vm, 0);
    1072        24764 :         rc = SQLITE_OK;
    1073        24764 :     }
    1074              :     
    1075              : cleanup:
    1076        44464 :     if ((rc != SQLITE_OK) && (rc != SQLITE_DONE)) *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1077        44464 :     stmt_reset(vm);
    1078        44464 :     return rc;
    1079              : }
    1080              : 
    1081        24632 : int merge_set_winner_clock (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pk_len, const char *colname, sqlite3_int64 col_version, sqlite3_int64 db_version, const char *site_id, int site_len, sqlite3_int64 seq, sqlite3_int64 *rowid, const char **err) {
    1082              :     
    1083              :     // get/set site_id
    1084        24632 :     sqlite3_stmt *vm = data->getset_siteid_stmt;
    1085        24632 :     int rc = sqlite3_bind_blob(vm, 1, (const void *)site_id, site_len, SQLITE_STATIC);
    1086        24632 :     if (rc != SQLITE_OK) goto cleanup_merge;
    1087              :     
    1088        24632 :     rc = sqlite3_step(vm);
    1089        24632 :     if (rc != SQLITE_ROW) goto cleanup_merge;
    1090              :     
    1091        24632 :     int64_t ord = sqlite3_column_int64(vm, 0);
    1092        24632 :     stmt_reset(vm);
    1093              :     
    1094        24632 :     vm = table->meta_winner_clock_stmt;
    1095        24632 :     rc = sqlite3_bind_blob(vm, 1, (const void *)pk, pk_len, SQLITE_STATIC);
    1096        24632 :     if (rc != SQLITE_OK) goto cleanup_merge;
    1097              :     
    1098        24632 :     rc = sqlite3_bind_text(vm, 2, (colname) ? colname : CLOUDSYNC_TOMBSTONE_VALUE, -1, SQLITE_STATIC);
    1099        24632 :     if (rc != SQLITE_OK) goto cleanup_merge;
    1100              :     
    1101        24632 :     rc = sqlite3_bind_int64(vm, 3, col_version);
    1102        24632 :     if (rc != SQLITE_OK) goto cleanup_merge;
    1103              :     
    1104        24632 :     rc = sqlite3_bind_int64(vm, 4, db_version);
    1105        24632 :     if (rc != SQLITE_OK) goto cleanup_merge;
    1106              :     
    1107        24632 :     rc = sqlite3_bind_int64(vm, 5, seq);
    1108        24632 :     if (rc != SQLITE_OK) goto cleanup_merge;
    1109              :     
    1110        24632 :     rc = sqlite3_bind_int64(vm, 6, ord);
    1111        24632 :     if (rc != SQLITE_OK) goto cleanup_merge;
    1112              :     
    1113        24632 :     rc = sqlite3_step(vm);
    1114        49264 :     if (rc == SQLITE_ROW) {
    1115        24632 :         *rowid = sqlite3_column_int64(vm, 0);
    1116        24632 :         rc = SQLITE_OK;
    1117        24632 :     }
    1118              :     
    1119              : cleanup_merge:
    1120        24632 :     if (rc != SQLITE_OK) *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1121        24632 :     stmt_reset(vm);
    1122        24632 :     return rc;
    1123              : }
    1124              : 
    1125        24440 : int merge_insert_col (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pklen, const char *col_name, sqlite3_value *col_value, sqlite3_int64 col_version, sqlite3_int64 db_version, const char *site_id, int site_len, sqlite3_int64 seq, sqlite3_int64 *rowid, const char **err) {
    1126              :     int index;
    1127        24440 :     sqlite3_stmt *vm = table_column_lookup(table, col_name, true, &index);
    1128        24440 :     if (vm == NULL) {
    1129            0 :         *err = "Unable to retrieve column merge precompiled statement in merge_insert_col.";
    1130            0 :         return SQLITE_MISUSE;
    1131              :     }
    1132              :     
    1133              :     // INSERT INTO table (pk1, pk2, col_name) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET col_name=?;"
    1134              :     
    1135              :     // bind primary key(s)
    1136        24440 :     int rc = pk_decode_prikey((char *)pk, (size_t)pklen, pk_decode_bind_callback, vm);
    1137        24440 :     if (rc < 0) {
    1138            0 :         *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1139            0 :         rc = sqlite3_errcode(sqlite3_db_handle(vm));
    1140            0 :         stmt_reset(vm);
    1141            0 :         return rc;
    1142              :     }
    1143              :     
    1144              :     // bind value
    1145        24440 :     if (col_value) {
    1146        24440 :         rc = sqlite3_bind_value(vm, table->npks+1, col_value);
    1147        24440 :         if (rc == SQLITE_OK) rc = sqlite3_bind_value(vm, table->npks+2, col_value);
    1148        24440 :         if (rc != SQLITE_OK) {
    1149            0 :             *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1150            0 :             stmt_reset(vm);
    1151            0 :             return rc;
    1152              :         }
    1153              :         
    1154        24440 :     }
    1155              :     
    1156              :     // perform real operation and disable triggers
    1157              :     
    1158              :     // in case of GOS we reused the table->col_merge_stmt statement
    1159              :     // which looks like: INSERT INTO table (pk1, pk2, col_name) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET col_name=?;"
    1160              :     // but the UPDATE in the CONFLICT statement would return SQLITE_CONSTRAINT because the trigger raises the error
    1161              :     // the trick is to disable that trigger before executing the statement
    1162        24440 :     if (table->algo == table_algo_crdt_gos) table->enabled = 0;
    1163        24440 :     SYNCBIT_SET(data);
    1164        24440 :     rc = sqlite3_step(vm);
    1165              :     DEBUG_MERGE("merge_insert(%02x%02x): %s (%d)", data->site_id[UUID_LEN-2], data->site_id[UUID_LEN-1], sqlite3_expanded_sql(vm), rc);
    1166        24440 :     stmt_reset(vm);
    1167        24440 :     SYNCBIT_RESET(data);
    1168        24440 :     if (table->algo == table_algo_crdt_gos) table->enabled = 1;
    1169              :     
    1170        24440 :     if (rc != SQLITE_DONE) {
    1171            0 :         *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1172            0 :         return rc;
    1173              :     }
    1174              :     
    1175        24440 :     return merge_set_winner_clock(data, table, pk, pklen, col_name, col_version, db_version, site_id, site_len, seq, rowid, err);
    1176        24440 : }
    1177              : 
    1178          164 : int merge_delete (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pklen, const char *colname, sqlite3_int64 cl, sqlite3_int64 db_version, const char *site_id, int site_len, sqlite3_int64 seq, sqlite3_int64 *rowid, const char **err) {
    1179          164 :     int rc = SQLITE_OK;
    1180              :     
    1181              :     // reset return value
    1182          164 :     *rowid = 0;
    1183              :     
    1184              :     // bind pk
    1185          164 :     sqlite3_stmt *vm = table->real_merge_delete_stmt;
    1186          164 :     rc = pk_decode_prikey((char *)pk, (size_t)pklen, pk_decode_bind_callback, vm);
    1187          164 :     if (rc < 0) {
    1188            0 :         *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1189            0 :         rc = sqlite3_errcode(sqlite3_db_handle(vm));
    1190            0 :         stmt_reset(vm);
    1191            0 :         return rc;
    1192              :     }
    1193              :     
    1194              :     // perform real operation and disable triggers
    1195          164 :     SYNCBIT_SET(data);
    1196          164 :     rc = sqlite3_step(vm);
    1197              :     DEBUG_MERGE("merge_delete(%02x%02x): %s (%d)", data->site_id[UUID_LEN-2], data->site_id[UUID_LEN-1], sqlite3_expanded_sql(vm), rc);
    1198          164 :     stmt_reset(vm);
    1199          164 :     SYNCBIT_RESET(data);
    1200          164 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    1201          164 :     if (rc != SQLITE_OK) {
    1202            0 :         *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1203            0 :         return rc;
    1204              :     }
    1205              :     
    1206          164 :     rc = merge_set_winner_clock(data, table, pk, pklen, colname, cl, db_version, site_id, site_len, seq, rowid, err);
    1207          164 :     if (rc != SQLITE_OK) return rc;
    1208              :     
    1209              :     // drop clocks _after_ setting the winner clock so we don't lose track of the max db_version!!
    1210              :     // this must never come before `set_winner_clock`
    1211          164 :     vm = table->meta_merge_delete_drop;
    1212          164 :     rc = sqlite3_bind_blob(vm, 1, (const void *)pk, pklen, SQLITE_STATIC);
    1213          164 :     if (rc == SQLITE_OK) rc = sqlite3_step(vm);
    1214          164 :     stmt_reset(vm);
    1215          164 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    1216          164 :     if (rc != SQLITE_OK) {
    1217            0 :         *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1218            0 :     }
    1219              :     
    1220          164 :     return rc;
    1221          164 : }
    1222              : 
    1223           28 : int merge_zeroclock_on_resurrect(cloudsync_table_context *table, sqlite3_int64 db_version, const char *pk, int pklen, const char **err) {
    1224           28 :     sqlite3_stmt *vm = table->meta_zero_clock_stmt;
    1225              :     
    1226           28 :     int rc = sqlite3_bind_int64(vm, 1, db_version);
    1227           28 :     if (rc != SQLITE_OK) goto cleanup;
    1228              :     
    1229           28 :     rc = sqlite3_bind_blob(vm, 2, (const void *)pk, pklen, SQLITE_STATIC);
    1230           28 :     if (rc != SQLITE_OK) goto cleanup;
    1231              :     
    1232           28 :     rc = sqlite3_step(vm);
    1233           28 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    1234              :     
    1235              : cleanup:
    1236           28 :     if (rc != SQLITE_OK) *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1237           28 :     stmt_reset(vm);
    1238           28 :     return rc;
    1239              : }
    1240              : 
    1241              : // executed only if insert_cl == local_cl
    1242        44464 : int merge_did_cid_win (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pklen, sqlite3_value *insert_value, const char *site_id, int site_len, const char *col_name, sqlite3_int64 col_version, bool *didwin_flag, const char **err) {
    1243              :     
    1244        44464 :     if (col_name == NULL) col_name = CLOUDSYNC_TOMBSTONE_VALUE;
    1245              :     
    1246              :     sqlite3_int64 local_version;
    1247        44464 :     int rc = merge_get_col_version(table, col_name, pk, pklen, &local_version, err);
    1248        44464 :     if (rc == SQLITE_DONE) {
    1249              :         // no rows returned, the incoming change wins if there's nothing there locally
    1250        19700 :         *didwin_flag = true;
    1251        19700 :         return SQLITE_OK;
    1252              :     }
    1253        24764 :     if (rc != SQLITE_OK) return rc;
    1254              :     
    1255              :     // rc == SQLITE_OK, means that a row with a version exists
    1256        24764 :     if (local_version != col_version) {
    1257         1885 :         if (col_version > local_version) {*didwin_flag = true; return SQLITE_OK;}
    1258          708 :         if (col_version < local_version) {*didwin_flag = false; return SQLITE_OK;}
    1259            0 :     }
    1260              :     
    1261              :     // rc == SQLITE_ROW and col_version == local_version, need to compare values
    1262              :     
    1263              :     // retrieve col_value precompiled statement
    1264        22879 :     sqlite3_stmt *vm = table_column_lookup(table, col_name, false, NULL);
    1265        22879 :     if (!vm) {
    1266            0 :         *err = "Unable to retrieve column value precompiled statement in merge_did_cid_win.";
    1267            0 :         return SQLITE_ERROR;
    1268              :     }
    1269              :     
    1270              :     // bind primary key values
    1271        22879 :     rc = pk_decode_prikey((char *)pk, (size_t)pklen, pk_decode_bind_callback, (void *)vm);
    1272        22879 :     if (rc < 0) {
    1273            0 :         *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1274            0 :         rc = sqlite3_errcode(sqlite3_db_handle(vm));
    1275            0 :         stmt_reset(vm);
    1276            0 :         return rc;
    1277              :     }
    1278              :         
    1279              :     // execute vm
    1280              :     sqlite3_value *local_value;
    1281        22879 :     rc = sqlite3_step(vm);
    1282        22879 :     if (rc == SQLITE_DONE) {
    1283              :         // meta entry exists but the actual value is missing
    1284              :         // we should allow the value_compare function to make a decision
    1285              :         // value_compare has been modified to handle the case where lvalue is NULL
    1286            0 :         local_value = NULL;
    1287            0 :         rc = SQLITE_OK;
    1288        22879 :     } else if (rc == SQLITE_ROW) {
    1289        22879 :         local_value = sqlite3_column_value(vm, 0);
    1290        22879 :         rc = SQLITE_OK;
    1291        22879 :     } else {
    1292            0 :         goto cleanup;
    1293              :     }
    1294              :     
    1295              :     // compare values
    1296        22879 :     int ret = dbutils_value_compare(insert_value, local_value);
    1297              :     // reset after compare, otherwise local value would be deallocated
    1298        22879 :     vm = stmt_reset(vm);
    1299              :     
    1300        22879 :     bool compare_site_id = (ret == 0 && data->merge_equal_values == true);
    1301        22879 :     if (!compare_site_id) {
    1302        22879 :         *didwin_flag = (ret > 0);
    1303        22879 :         goto cleanup;
    1304              :     }
    1305              :     
    1306              :     // values are the same and merge_equal_values is true
    1307            0 :     vm = table->meta_site_id_stmt;
    1308            0 :     rc = sqlite3_bind_blob(vm, 1, (const void *)pk, pklen, SQLITE_STATIC);
    1309            0 :     if (rc != SQLITE_OK) goto cleanup;
    1310              :     
    1311            0 :     rc = sqlite3_bind_text(vm, 2, col_name, -1, SQLITE_STATIC);
    1312            0 :     if (rc != SQLITE_OK) goto cleanup;
    1313              :     
    1314            0 :     rc = sqlite3_step(vm);
    1315            0 :     if (rc == SQLITE_ROW) {
    1316            0 :         const void *local_site_id = sqlite3_column_blob(vm, 0);
    1317            0 :         ret = memcmp(site_id, local_site_id, site_len);
    1318            0 :         *didwin_flag = (ret > 0);
    1319            0 :         stmt_reset(vm);
    1320            0 :         return SQLITE_OK;
    1321              :     }
    1322              :     
    1323              :     // handle error condition here
    1324            0 :     stmt_reset(vm);
    1325            0 :     *err = "Unable to find site_id for previous change. The cloudsync table is probably corrupted.";
    1326            0 :     return SQLITE_ERROR;
    1327              :     
    1328              : cleanup:
    1329        22879 :     if (rc != SQLITE_OK) *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1330        22879 :     if (vm) stmt_reset(vm);
    1331        22879 :     return rc;
    1332        44464 : }
    1333              : 
    1334           28 : int merge_sentinel_only_insert (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pklen, sqlite3_int64 cl, sqlite3_int64 db_version, const char *site_id, int site_len, sqlite3_int64 seq, sqlite3_int64 *rowid, const char **err) {
    1335              :     
    1336              :     // reset return value
    1337           28 :     *rowid = 0;
    1338              :     
    1339              :     // bind pk
    1340           28 :     sqlite3_stmt *vm = table->real_merge_sentinel_stmt;
    1341           28 :     int rc = pk_decode_prikey((char *)pk, (size_t)pklen, pk_decode_bind_callback, vm);
    1342           28 :     if (rc < 0) {
    1343            0 :         *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1344            0 :         rc = sqlite3_errcode(sqlite3_db_handle(vm));
    1345            0 :         stmt_reset(vm);
    1346            0 :         return rc;
    1347              :     }
    1348              :     
    1349              :     // perform real operation and disable triggers
    1350           28 :     SYNCBIT_SET(data);
    1351           28 :     rc = sqlite3_step(vm);
    1352           28 :     stmt_reset(vm);
    1353           28 :     SYNCBIT_RESET(data);
    1354           28 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    1355           28 :     if (rc != SQLITE_OK) {
    1356            0 :         *err = sqlite3_errmsg(sqlite3_db_handle(vm));
    1357            0 :         return rc;
    1358              :     }
    1359              :     
    1360           28 :     rc = merge_zeroclock_on_resurrect(table, db_version, pk, pklen, err);
    1361           28 :     if (rc != SQLITE_OK) return rc;
    1362              :     
    1363           28 :     return merge_set_winner_clock(data, table, pk, pklen, NULL, cl, db_version, site_id, site_len, seq, rowid, err);
    1364           28 : }
    1365              : 
    1366         3150 : int cloudsync_merge_insert_gos (sqlite3_vtab *vtab, cloudsync_context *data, cloudsync_table_context *table, const char *insert_pk, int insert_pk_len, const char *insert_name, sqlite3_value *insert_value, sqlite3_int64 insert_col_version, sqlite3_int64 insert_db_version, const char *insert_site_id, int insert_site_id_len, sqlite3_int64 insert_seq, sqlite3_int64 *rowid) {
    1367              :     // Grow-Only Set (GOS) Algorithm: Only insertions are allowed, deletions and updates are prevented from a trigger.
    1368              :     
    1369         3150 :     const char *err = NULL;
    1370         6300 :     int rc = merge_insert_col(data, table, insert_pk, insert_pk_len, insert_name, insert_value, insert_col_version, insert_db_version,
    1371         3150 :                               insert_site_id, insert_site_id_len, insert_seq, rowid, &err);
    1372         3150 :     if (rc != SQLITE_OK) {
    1373            0 :         cloudsync_vtab_set_error(vtab, "Unable to perform GOS merge_insert_col: %s", err);
    1374            0 :     }
    1375              :     
    1376         3150 :     return rc;
    1377              : }
    1378              : 
    1379        48599 : int cloudsync_merge_insert (sqlite3_vtab *vtab, int argc, sqlite3_value **argv, sqlite3_int64 *rowid) {
    1380              :     // this function performs the merging logic for an insert in a cloud-synchronized table. It handles
    1381              :     // different scenarios including conflicts, causal lengths, delete operations, and resurrecting rows
    1382              :     // based on the incoming data (from remote nodes or clients) and the local database state
    1383              : 
    1384              :     // this function handles different CRDT algorithms (GOS, DWS, AWS, and CLS).
    1385              :     // the merging strategy is determined based on the table->algo value.
    1386              :     
    1387              :     // meta table declaration:
    1388              :     // tbl TEXT NOT NULL, pk BLOB NOT NULL, col_name TEXT NOT NULL,"
    1389              :     // "col_value ANY, col_version INTEGER NOT NULL, db_version INTEGER NOT NULL,"
    1390              :     // "site_id BLOB NOT NULL, cl INTEGER NOT NULL, seq INTEGER NOT NULL
    1391              :     
    1392              :     // meta information to retrieve from arguments:
    1393              :     // argv[0] -> table name (TEXT)
    1394              :     // argv[1] -> primary key (BLOB)
    1395              :     // argv[2] -> column name (TEXT or NULL if sentinel)
    1396              :     // argv[3] -> column value (ANY)
    1397              :     // argv[4] -> column version (INTEGER)
    1398              :     // argv[5] -> database version (INTEGER)
    1399              :     // argv[6] -> site ID (BLOB, identifies the origin of the update)
    1400              :     // argv[7] -> causal length (INTEGER, tracks the order of operations)
    1401              :     // argv[8] -> sequence number (INTEGER, unique per operation)
    1402              :     
    1403              :     // extract table name
    1404        48599 :     const char *insert_tbl = (const char *)sqlite3_value_text(argv[0]);
    1405              :     
    1406              :     // lookup table
    1407        48599 :     cloudsync_context *data = cloudsync_vtab_get_context(vtab);
    1408        48599 :     cloudsync_table_context *table = table_lookup(data, insert_tbl);
    1409        48599 :     if (!table) return cloudsync_vtab_set_error(vtab, "Unable to find table %s,", insert_tbl);
    1410              :     
    1411              :     // extract the remaining fields from the input values
    1412        48599 :     const char *insert_pk = (const char *)sqlite3_value_blob(argv[1]);
    1413        48599 :     int insert_pk_len = sqlite3_value_bytes(argv[1]);
    1414        48599 :     const char *insert_name = (sqlite3_value_type(argv[2]) == SQLITE_NULL) ? CLOUDSYNC_TOMBSTONE_VALUE : (const char *)sqlite3_value_text(argv[2]);
    1415        48599 :     sqlite3_value *insert_value = argv[3];
    1416        48599 :     sqlite3_int64 insert_col_version = sqlite3_value_int64(argv[4]);
    1417        48599 :     sqlite3_int64 insert_db_version = sqlite3_value_int64(argv[5]);
    1418        48599 :     const char *insert_site_id = (const char *)sqlite3_value_blob(argv[6]);
    1419        48599 :     int insert_site_id_len = sqlite3_value_bytes(argv[6]);
    1420        48599 :     sqlite3_int64 insert_cl = sqlite3_value_int64(argv[7]);
    1421        48599 :     sqlite3_int64 insert_seq = sqlite3_value_int64(argv[8]);
    1422        48599 :     const char *err = NULL;
    1423              :     
    1424              :     // perform different logic for each different table algorithm
    1425        48599 :     if (table->algo == table_algo_crdt_gos) return cloudsync_merge_insert_gos(vtab, data, table, insert_pk, insert_pk_len, insert_name, insert_value, insert_col_version, insert_db_version, insert_site_id, insert_site_id_len, insert_seq, rowid);
    1426              :     
    1427              :     // Handle DWS and AWS algorithms here
    1428              :     // Delete-Wins Set (DWS): table_algo_crdt_dws
    1429              :     // Add-Wins Set (AWS): table_algo_crdt_aws
    1430              :     
    1431              :     // Causal-Length Set (CLS) Algorithm (default)
    1432              :     
    1433              :     // compute the local causal length for the row based on the primary key
    1434              :     // the causal length is used to determine the order of operations and resolve conflicts.
    1435        45449 :     sqlite3_int64 local_cl = merge_get_local_cl(table, insert_pk, insert_pk_len, &err);
    1436        45449 :     if (local_cl < 0) {
    1437            0 :         return cloudsync_vtab_set_error(vtab, "Unable to compute local causal length: %s", err);
    1438              :     }
    1439              :     
    1440              :     // if the incoming causal length is older than the local causal length, we can safely ignore it
    1441              :     // because the local changes are more recent
    1442        45449 :     if (insert_cl < local_cl) return SQLITE_OK;
    1443              :     
    1444              :     // check if the operation is a delete by examining the causal length
    1445              :     // even causal lengths typically signify delete operations
    1446        45232 :     bool is_delete = (insert_cl % 2 == 0);
    1447        45232 :     if (is_delete) {
    1448              :         // if it's a delete, check if the local state is at the same causal length
    1449              :         // if it is, no further action is needed
    1450          613 :         if (local_cl == insert_cl) return SQLITE_OK;
    1451              :         
    1452              :         // perform a delete merge if the causal length is newer than the local one
    1453          328 :         int rc = merge_delete(data, table, insert_pk, insert_pk_len, insert_name, insert_col_version,
    1454          164 :                               insert_db_version, insert_site_id, insert_site_id_len, insert_seq, rowid, &err);
    1455          164 :         if (rc != SQLITE_OK) cloudsync_vtab_set_error(vtab, "Unable to perform merge_delete: %s", err);
    1456          164 :         return rc;
    1457              :     }
    1458              :     
    1459              :     // if the operation is a sentinel-only insert (indicating a new row or resurrected row with no column update), handle it separately.
    1460        44619 :     bool is_sentinel_only = (strcmp(insert_name, CLOUDSYNC_TOMBSTONE_VALUE) == 0);
    1461        44619 :     if (is_sentinel_only) {
    1462          155 :         if (local_cl == insert_cl) return SQLITE_OK;
    1463              :         
    1464              :         // perform a sentinel-only insert to track the existence of the row
    1465           56 :         int rc = merge_sentinel_only_insert(data, table, insert_pk, insert_pk_len, insert_col_version,
    1466           28 :                                             insert_db_version, insert_site_id, insert_site_id_len, insert_seq, rowid, &err);
    1467           28 :         if (rc != SQLITE_OK) cloudsync_vtab_set_error(vtab, "Unable to perform merge_sentinel_only_insert: %s", err);
    1468           28 :         return rc;
    1469              :     }
    1470              :     
    1471              :     // from this point I can be sure that insert_name is not sentinel
    1472              :     
    1473              :     // handle the case where a row is being resurrected (e.g., after a delete, a new insert for the same row)
    1474              :     // odd causal lengths can "resurrect" rows
    1475        44464 :     bool needs_resurrect = (insert_cl > local_cl && insert_cl % 2 == 1);
    1476        44464 :     bool row_exists_locally = local_cl != 0;
    1477              :    
    1478              :     // if a resurrection is needed, insert a sentinel to mark the row as alive
    1479              :     // this handles out-of-order deliveries where the row was deleted and is now being re-inserted
    1480        44464 :     if (needs_resurrect && (row_exists_locally || (!row_exists_locally && insert_cl > 1))) {
    1481            0 :         int rc = merge_sentinel_only_insert(data, table, insert_pk, insert_pk_len, insert_cl,
    1482            0 :                                             insert_db_version, insert_site_id, insert_site_id_len, insert_seq, rowid, &err);
    1483            0 :         if (rc != SQLITE_OK) {
    1484            0 :             cloudsync_vtab_set_error(vtab, "Unable to perform merge_sentinel_only_insert: %s", err);
    1485            0 :             return rc;
    1486              :         }
    1487            0 :     }
    1488              :     
    1489              :     // at this point, we determine whether the incoming change wins based on causal length
    1490              :     // this can be due to a resurrection, a non-existent local row, or a conflict resolution
    1491        44464 :     bool flag = false;
    1492        44464 :     int rc = merge_did_cid_win(data, table, insert_pk, insert_pk_len, insert_value, insert_site_id, insert_site_id_len, insert_name, insert_col_version, &flag, &err);
    1493        44464 :     if (rc != SQLITE_OK) {
    1494            0 :         cloudsync_vtab_set_error(vtab, "Unable to perform merge_did_cid_win: %s", err);
    1495            0 :         return rc;
    1496              :     }
    1497              :     
    1498              :     // check if the incoming change wins and should be applied
    1499        44464 :     bool does_cid_win = ((needs_resurrect) || (!row_exists_locally) || (flag));
    1500        44464 :     if (!does_cid_win) return SQLITE_OK;
    1501              :     
    1502              :     // perform the final column insert or update if the incoming change wins
    1503        21290 :     rc = merge_insert_col(data, table, insert_pk, insert_pk_len, insert_name, insert_value, insert_col_version, insert_db_version, insert_site_id, insert_site_id_len, insert_seq, rowid, &err);
    1504        21290 :     if (rc != SQLITE_OK) cloudsync_vtab_set_error(vtab, "Unable to perform merge_insert_col: %s", err);
    1505        21290 :     return rc;
    1506        48599 : }
    1507              : 
    1508              : // MARK: - Private -
    1509              : 
    1510          109 : bool cloudsync_config_exists (sqlite3 *db) {
    1511          109 :     return dbutils_table_exists(db, CLOUDSYNC_SITEID_NAME) == true;
    1512              : }
    1513              : 
    1514          109 : void *cloudsync_context_create (void) {
    1515          109 :     cloudsync_context *data = (cloudsync_context *)cloudsync_memory_zeroalloc((uint64_t)(sizeof(cloudsync_context)));
    1516              :     DEBUG_SETTINGS("cloudsync_context_create %p", data);
    1517              :     
    1518          109 :     data->libversion = CLOUDSYNC_VERSION;
    1519          109 :     data->pending_db_version = CLOUDSYNC_VALUE_NOTSET;
    1520              :     #if CLOUDSYNC_DEBUG
    1521              :     data->debug = 1;
    1522              :     #endif
    1523              :     
    1524              :     // allocate space for 128 tables (it can grow if needed)
    1525          109 :     data->tables = (cloudsync_table_context **)cloudsync_memory_zeroalloc((uint64_t)(CLOUDSYNC_INIT_NTABLES * sizeof(cloudsync_table_context *)));
    1526          109 :     if (!data->tables) {
    1527            0 :         cloudsync_memory_free(data);
    1528            0 :         return NULL;
    1529              :     }
    1530          109 :     data->tables_alloc = CLOUDSYNC_INIT_NTABLES;
    1531          109 :     data->tables_count = 0;
    1532              :         
    1533          109 :     return data;
    1534          109 : }
    1535              : 
    1536          109 : void cloudsync_context_free (void *ptr) {
    1537              :     DEBUG_SETTINGS("cloudsync_context_free %p", ptr);
    1538          109 :     if (!ptr) return;
    1539              :         
    1540          109 :     cloudsync_context *data = (cloudsync_context*)ptr;
    1541          109 :     cloudsync_memory_free(data->tables);
    1542          109 :     cloudsync_memory_free(data);
    1543          109 : }
    1544              : 
    1545          223 : const char *cloudsync_context_init (sqlite3 *db, cloudsync_context *data, sqlite3_context *context) {
    1546          223 :     if (!data && context) data = (cloudsync_context *)sqlite3_user_data(context);
    1547              : 
    1548              :     // perform init just the first time, if the site_id field is not set.
    1549              :     // The data->site_id value could exists while settings tables don't exists if the
    1550              :     // cloudsync_context_init was previously called in init transaction that was rolled back
    1551              :     // because of an error during the init process.
    1552          223 :     if (data->site_id[0] == 0 || !dbutils_table_exists(db, CLOUDSYNC_SITEID_NAME)) {
    1553          111 :         if (dbutils_settings_init(db, data, context) != SQLITE_OK) return NULL;
    1554          111 :         if (stmts_add_tocontext(db, data) != SQLITE_OK) return NULL;
    1555          111 :         if (cloudsync_load_siteid(db, data) != SQLITE_OK) return NULL;
    1556              :         
    1557          111 :         data->sqlite_ctx = context;
    1558          111 :         data->schema_hash = dbutils_schema_hash(db);
    1559          111 :     }
    1560              :     
    1561          223 :     return (const char *)data->site_id;
    1562          223 : }
    1563              : 
    1564         1029 : void cloudsync_sync_key(cloudsync_context *data, const char *key, const char *value) {
    1565              :     DEBUG_SETTINGS("cloudsync_sync_key key: %s value: %s", key, value);
    1566              :     
    1567              :     // sync data
    1568         1029 :     if (strcmp(key, CLOUDSYNC_KEY_SCHEMAVERSION) == 0) {
    1569          111 :         data->schema_version = (int)strtol(value, NULL, 0);
    1570          111 :         return;
    1571              :     }
    1572              :     
    1573          918 :     if (strcmp(key, CLOUDSYNC_KEY_DEBUG) == 0) {
    1574            0 :         data->debug = 0;
    1575            0 :         if (value && (value[0] != 0) && (value[0] != '0')) data->debug = 1;
    1576            0 :         return;
    1577              :     }
    1578         1029 : }
    1579              : 
    1580              : #if 0
    1581              : void cloudsync_sync_table_key(cloudsync_context *data, const char *table, const char *column, const char *key, const char *value) {
    1582              :     DEBUG_SETTINGS("cloudsync_sync_table_key table: %s column: %s key: %s value: %s", table, column, key, value);
    1583              :     // Unused in this version
    1584              :     return;
    1585              : }
    1586              : #endif
    1587              : 
    1588         6904 : int cloudsync_commit_hook (void *ctx) {
    1589         6904 :     cloudsync_context *data = (cloudsync_context *)ctx;
    1590              :     
    1591         6904 :     data->db_version = data->pending_db_version;
    1592         6904 :     data->pending_db_version = CLOUDSYNC_VALUE_NOTSET;
    1593         6904 :     data->seq = 0;
    1594              :     
    1595         6904 :     return SQLITE_OK;
    1596              : }
    1597              : 
    1598            2 : void cloudsync_rollback_hook (void *ctx) {
    1599            2 :     cloudsync_context *data = (cloudsync_context *)ctx;
    1600              :     
    1601            2 :     data->pending_db_version = CLOUDSYNC_VALUE_NOTSET;
    1602            2 :     data->seq = 0;
    1603            2 : }
    1604              : 
    1605           23 : int cloudsync_finalize_alter (sqlite3_context *context, cloudsync_context *data, cloudsync_table_context *table) {
    1606           23 :     int rc = SQLITE_OK;
    1607           23 :     sqlite3 *db = sqlite3_context_db_handle(context);
    1608              : 
    1609           23 :     db_version_check_uptodate(db, data);
    1610              : 
    1611              :     // If primary key columns change (in the schema)
    1612              :     // We need to drop, re-create and backfill
    1613              :     // the clock table.
    1614              :     // A change in pk columns means a change in all identities
    1615              :     // of all rows.
    1616              :     // We can determine this by comparing unique index on lookaside table vs
    1617              :     // pks on source table
    1618           23 :     char *errmsg = NULL;
    1619           23 :     char **result = NULL;
    1620              :     int nrows, ncols;
    1621           23 :     char *sql = cloudsync_memory_mprintf("SELECT name FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk;", table->name);
    1622           23 :     rc = sqlite3_get_table(db, sql, &result, &nrows, &ncols, NULL);
    1623           23 :     cloudsync_memory_free(sql);
    1624           23 :     if (rc != SQLITE_OK) {
    1625            0 :         DEBUG_SQLITE_ERROR(rc, "cloudsync_finalize_alter", db);
    1626            0 :         goto finalize;
    1627           23 :     } else if (errmsg || ncols != 1) {
    1628            0 :         rc = SQLITE_MISUSE;
    1629            0 :         goto finalize;
    1630              :     }
    1631              :     
    1632           23 :     bool pk_diff = false;
    1633           23 :     if (nrows != table->npks) {
    1634            6 :         pk_diff = true;
    1635            6 :     } else {
    1636           51 :         for (int i=0; i<nrows; ++i) {
    1637           34 :             if (strcmp(table->pk_name[i], result[i]) != 0) {
    1638            0 :                 pk_diff = true;
    1639            0 :                 break;
    1640              :             }
    1641           34 :         }
    1642              :     }
    1643              :     
    1644           23 :     if (pk_diff) {
    1645              :         // drop meta-table, it will be recreated
    1646            6 :         char *sql = cloudsync_memory_mprintf("DROP TABLE IF EXISTS \"%w_cloudsync\";", table->name);
    1647            6 :         rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
    1648            6 :         cloudsync_memory_free(sql);
    1649            6 :         if (rc != SQLITE_OK) {
    1650            0 :             DEBUG_SQLITE_ERROR(rc, "cloudsync_finalize_alter", db);
    1651            0 :             goto finalize;
    1652              :         }
    1653            6 :     } else {
    1654              :         // compact meta-table
    1655              :         // delete entries for removed columns
    1656           17 :         char *sql = cloudsync_memory_mprintf("DELETE FROM \"%w_cloudsync\" WHERE \"col_name\" NOT IN ("
    1657              :                                              "SELECT name FROM pragma_table_info('%q') UNION SELECT '%s'"
    1658           17 :                                              ")", table->name, table->name, CLOUDSYNC_TOMBSTONE_VALUE);
    1659           17 :         rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
    1660           17 :         cloudsync_memory_free(sql);
    1661           17 :         if (rc != SQLITE_OK) {
    1662            0 :             DEBUG_SQLITE_ERROR(rc, "cloudsync_finalize_alter", db);
    1663            0 :             goto finalize;
    1664              :         }
    1665              :         
    1666           17 :         char *singlequote_escaped_table_name = cloudsync_memory_mprintf("%q", table->name);
    1667           17 :         sql = cloudsync_memory_mprintf("SELECT group_concat('\"%w\".\"' || format('%%w', name) || '\"', ',') FROM pragma_table_info('%s') WHERE pk>0 ORDER BY pk;", singlequote_escaped_table_name, singlequote_escaped_table_name);
    1668           17 :         cloudsync_memory_free(singlequote_escaped_table_name);
    1669           17 :         if (!sql) {
    1670            0 :             rc = SQLITE_NOMEM;
    1671            0 :             goto finalize;
    1672              :         }
    1673           17 :         char *pkclause = dbutils_text_select(db, sql);
    1674           17 :         char *pkvalues = (pkclause) ? pkclause : "rowid";
    1675           17 :         cloudsync_memory_free(sql);
    1676              :         
    1677              :         // delete entries related to rows that no longer exist in the original table, but preserve tombstone
    1678           17 :         sql = cloudsync_memory_mprintf("DELETE FROM \"%w_cloudsync\" WHERE (\"col_name\" != '%s' OR (\"col_name\" = '%s' AND col_version %% 2 != 0)) AND NOT EXISTS (SELECT 1 FROM \"%w\" WHERE \"%w_cloudsync\".pk = cloudsync_pk_encode(%s) LIMIT 1);", table->name, CLOUDSYNC_TOMBSTONE_VALUE, CLOUDSYNC_TOMBSTONE_VALUE, table->name, table->name, pkvalues);
    1679           17 :         rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
    1680           17 :         if (pkclause) cloudsync_memory_free(pkclause);
    1681           17 :         cloudsync_memory_free(sql);
    1682           17 :         if (rc != SQLITE_OK) {
    1683            0 :             DEBUG_SQLITE_ERROR(rc, "cloudsync_finalize_alter", db);
    1684            0 :             goto finalize;
    1685              :         }
    1686              : 
    1687              :     }
    1688              :     
    1689              :     char buf[256];
    1690           23 :     snprintf(buf, sizeof(buf), "%lld", data->db_version);
    1691           23 :     dbutils_settings_set_key_value(db, context, "pre_alter_dbversion", buf);
    1692              :     
    1693              : finalize:
    1694           23 :     sqlite3_free_table(result);
    1695           23 :     sqlite3_free(errmsg);
    1696              :     
    1697           23 :     return rc;
    1698              : }
    1699              : 
    1700          168 : int cloudsync_refill_metatable (sqlite3 *db, cloudsync_context *data, const char *table_name) {
    1701          168 :     cloudsync_table_context *table = table_lookup(data, table_name);
    1702          168 :     if (!table) return SQLITE_INTERNAL;
    1703              :     
    1704          168 :     sqlite3_stmt *vm = NULL;
    1705          168 :     sqlite3_int64 db_version = db_version_next(db, data, CLOUDSYNC_VALUE_NOTSET);
    1706              :     
    1707          168 :     char *sql = cloudsync_memory_mprintf("SELECT group_concat('\"' || format('%%w', name) || '\"', ',') FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk;", table_name);
    1708          168 :     char *pkclause_identifiers = dbutils_text_select(db, sql);
    1709          168 :     char *pkvalues_identifiers = (pkclause_identifiers) ? pkclause_identifiers : "rowid";
    1710          168 :     cloudsync_memory_free(sql);
    1711              :     
    1712          168 :     sql = cloudsync_memory_mprintf("SELECT group_concat('cloudsync_pk_decode(pk, ' || pk || ') AS ' || '\"' || format('%%w', name) || '\"', ',') FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk;", table_name);
    1713          168 :     char *pkdecode = dbutils_text_select(db, sql);
    1714          168 :     char *pkdecodeval = (pkdecode) ? pkdecode : "cloudsync_pk_decode(pk, 1) AS rowid";
    1715          168 :     cloudsync_memory_free(sql);
    1716              :      
    1717          168 :     sql = cloudsync_memory_mprintf("SELECT cloudsync_insert('%q', %s) FROM (SELECT %s FROM \"%w\" EXCEPT SELECT %s FROM \"%w_cloudsync\");", table_name, pkvalues_identifiers, pkvalues_identifiers, table_name, pkdecodeval, table_name);
    1718          168 :     int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
    1719          168 :     cloudsync_memory_free(sql);
    1720          168 :     if (rc != SQLITE_OK) goto finalize;
    1721              :     
    1722              :     // fill missing colums
    1723              :     // for each non-pk column:
    1724              :     // The new query does 1 encode per source row and one indexed NOT-EXISTS probe.
    1725              :     // The old plan does many decodes per candidate and can’t use an index to rule out matches quickly—so it burns CPU and I/O.
    1726              :     
    1727          168 :     sql = cloudsync_memory_mprintf("WITH _cstemp1 AS (SELECT cloudsync_pk_encode(%s) AS pk FROM \"%w\") SELECT _cstemp1.pk FROM _cstemp1 WHERE NOT EXISTS (SELECT 1 FROM \"%w_cloudsync\" _cstemp2 WHERE _cstemp2.pk = _cstemp1.pk AND _cstemp2.col_name = ?);", pkvalues_identifiers, table_name, table_name);
    1728          168 :     rc = sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &vm, NULL);
    1729          168 :     cloudsync_memory_free(sql);
    1730          168 :     if (rc != SQLITE_OK) goto finalize;
    1731              :      
    1732         1117 :     for (int i=0; i<table->ncols; ++i) {
    1733          949 :         char *col_name = table->col_name[i];
    1734              : 
    1735          949 :         rc = sqlite3_bind_text(vm, 1, col_name, -1, SQLITE_STATIC);
    1736          949 :         if (rc != SQLITE_OK) goto finalize;
    1737              :         
    1738          987 :         while (1) {
    1739          987 :             rc = sqlite3_step(vm);
    1740          987 :             if (rc == SQLITE_ROW) {
    1741           38 :                 const char *pk = (const char *)sqlite3_column_text(vm, 0);
    1742           38 :                 size_t pklen = strlen(pk);
    1743           38 :                 rc = local_mark_insert_or_update_meta(db, table, pk, pklen, col_name, db_version, BUMP_SEQ(data));
    1744          987 :             } else if (rc == SQLITE_DONE) {
    1745          949 :                 rc = SQLITE_OK;
    1746          949 :                 break;
    1747              :             } else {
    1748            0 :                 break;
    1749              :             }
    1750              :         }
    1751          949 :         if (rc != SQLITE_OK) goto finalize;
    1752              : 
    1753          949 :         sqlite3_reset(vm);
    1754         1117 :     }
    1755              :     
    1756              : finalize:
    1757          168 :     if (rc != SQLITE_OK) DEBUG_ALWAYS("cloudsync_refill_metatable error: %s", sqlite3_errmsg(db));
    1758          168 :     if (pkclause_identifiers) cloudsync_memory_free(pkclause_identifiers);
    1759          168 :     if (pkdecode) cloudsync_memory_free(pkdecode);
    1760          168 :     if (vm) sqlite3_finalize(vm);
    1761          168 :     return rc;
    1762          168 : }
    1763              : 
    1764              : // MARK: - Local -
    1765              : 
    1766            1 : int local_update_sentinel (sqlite3 *db, cloudsync_table_context *table, const char *pk, size_t pklen, sqlite3_int64 db_version, int seq) {
    1767            1 :     sqlite3_stmt *vm = table->meta_sentinel_update_stmt;
    1768            1 :     if (!vm) return -1;
    1769              :     
    1770            1 :     int rc = sqlite3_bind_int64(vm, 1, db_version);
    1771            1 :     if (rc != SQLITE_OK) goto cleanup;
    1772              :     
    1773            1 :     rc = sqlite3_bind_int(vm, 2, seq);
    1774            1 :     if (rc != SQLITE_OK) goto cleanup;
    1775              :     
    1776            1 :     rc = sqlite3_bind_blob(vm, 3, pk, (int)pklen, SQLITE_STATIC);
    1777            1 :     if (rc != SQLITE_OK) goto cleanup;
    1778              :     
    1779            1 :     rc = sqlite3_step(vm);
    1780            1 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    1781              :     
    1782              : cleanup:
    1783            1 :     DEBUG_SQLITE_ERROR(rc, "local_update_sentinel", db);
    1784            1 :     sqlite3_reset(vm);
    1785            1 :     return rc;
    1786            1 : }
    1787              : 
    1788          121 : int local_mark_insert_sentinel_meta (sqlite3 *db, cloudsync_table_context *table, const char *pk, size_t pklen, sqlite3_int64 db_version, int seq) {
    1789          121 :     sqlite3_stmt *vm = table->meta_sentinel_insert_stmt;
    1790          121 :     if (!vm) return -1;
    1791              :     
    1792          121 :     int rc = sqlite3_bind_blob(vm, 1, pk, (int)pklen, SQLITE_STATIC);
    1793          121 :     if (rc != SQLITE_OK) goto cleanup;
    1794              :     
    1795          121 :     rc = sqlite3_bind_int64(vm, 2, db_version);
    1796          121 :     if (rc != SQLITE_OK) goto cleanup;
    1797              :     
    1798          121 :     rc = sqlite3_bind_int(vm, 3, seq);
    1799          121 :     if (rc != SQLITE_OK) goto cleanup;
    1800              :     
    1801          121 :     rc = sqlite3_bind_int64(vm, 4, db_version);
    1802          121 :     if (rc != SQLITE_OK) goto cleanup;
    1803              :     
    1804          121 :     rc = sqlite3_bind_int(vm, 5, seq);
    1805          121 :     if (rc != SQLITE_OK) goto cleanup;
    1806              :     
    1807          121 :     rc = sqlite3_step(vm);
    1808          121 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    1809              :     
    1810              : cleanup:
    1811          121 :     DEBUG_SQLITE_ERROR(rc, "local_insert_sentinel", db);
    1812          121 :     sqlite3_reset(vm);
    1813          121 :     return rc;
    1814          121 : }
    1815              : 
    1816        11286 : int local_mark_insert_or_update_meta_impl (sqlite3 *db, cloudsync_table_context *table, const char *pk, size_t pklen, const char *col_name, int col_version, sqlite3_int64 db_version, int seq) {
    1817              :     
    1818        11286 :     sqlite3_stmt *vm = table->meta_row_insert_update_stmt;
    1819        11286 :     if (!vm) return -1;
    1820              :     
    1821        11286 :     int rc = sqlite3_bind_blob(vm, 1, pk, (int)pklen, SQLITE_STATIC);
    1822        11286 :     if (rc != SQLITE_OK) goto cleanup;
    1823              :     
    1824        11286 :     rc = sqlite3_bind_text(vm, 2, (col_name) ? col_name : CLOUDSYNC_TOMBSTONE_VALUE, -1, SQLITE_STATIC);
    1825        11286 :     if (rc != SQLITE_OK) goto cleanup;
    1826              :     
    1827        11286 :     rc = sqlite3_bind_int(vm, 3, col_version);
    1828        11286 :     if (rc != SQLITE_OK) goto cleanup;
    1829              :     
    1830        11286 :     rc = sqlite3_bind_int64(vm, 4, db_version);
    1831        11286 :     if (rc != SQLITE_OK) goto cleanup;
    1832              :     
    1833        11286 :     rc = sqlite3_bind_int(vm, 5, seq);
    1834        11286 :     if (rc != SQLITE_OK) goto cleanup;
    1835              :     
    1836        11286 :     rc = sqlite3_bind_int64(vm, 6, db_version);
    1837        11286 :     if (rc != SQLITE_OK) goto cleanup;
    1838              :     
    1839        11286 :     rc = sqlite3_bind_int(vm, 7, seq);
    1840        11286 :     if (rc != SQLITE_OK) goto cleanup;
    1841              :     
    1842        11286 :     rc = sqlite3_step(vm);
    1843        11286 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    1844              :     
    1845              : cleanup:
    1846        11286 :     DEBUG_SQLITE_ERROR(rc, "local_insert_or_update", db);
    1847        11286 :     sqlite3_reset(vm);
    1848        11286 :     return rc;
    1849        11286 : }
    1850              : 
    1851        11228 : int local_mark_insert_or_update_meta (sqlite3 *db, cloudsync_table_context *table, const char *pk, size_t pklen, const char *col_name, sqlite3_int64 db_version, int seq) {
    1852        11228 :     return local_mark_insert_or_update_meta_impl(db, table, pk, pklen, col_name, 1, db_version, seq);
    1853              : }
    1854              : 
    1855           58 : int local_mark_delete_meta (sqlite3 *db, cloudsync_table_context *table, const char *pk, size_t pklen, sqlite3_int64 db_version, int seq) {
    1856           58 :     return local_mark_insert_or_update_meta_impl(db, table, pk, pklen, NULL, 2, db_version, seq);
    1857              : }
    1858              : 
    1859           28 : int local_drop_meta (sqlite3 *db, cloudsync_table_context *table, const char *pk, size_t pklen) {
    1860           28 :     sqlite3_stmt *vm = table->meta_row_drop_stmt;
    1861           28 :     if (!vm) return -1;
    1862              :     
    1863           28 :     int rc = sqlite3_bind_blob(vm, 1, pk, (int)pklen, SQLITE_STATIC);
    1864           28 :     if (rc != SQLITE_OK) goto cleanup;
    1865              :     
    1866           28 :     rc = sqlite3_step(vm);
    1867           28 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    1868              :     
    1869              : cleanup:
    1870           28 :     DEBUG_SQLITE_ERROR(rc, "local_drop_meta", db);
    1871           28 :     sqlite3_reset(vm);
    1872           28 :     return rc;
    1873           28 : }
    1874              : 
    1875           30 : int local_update_move_meta (sqlite3 *db, cloudsync_table_context *table, const char *pk, size_t pklen, const char *pk2, size_t pklen2, sqlite3_int64 db_version) {
    1876              :     /*
    1877              :       * This function moves non-sentinel metadata entries from an old primary key (OLD.pk)
    1878              :       * to a new primary key (NEW.pk) when a primary key change occurs.
    1879              :       *
    1880              :       * To ensure consistency and proper conflict resolution in a CRDT (Conflict-free Replicated Data Type) system,
    1881              :       * each non-sentinel metadata entry involved in the move must have a unique sequence value (seq).
    1882              :       *
    1883              :       * The `seq` is crucial for tracking the order of operations and for detecting and resolving conflicts
    1884              :       * during synchronization between replicas. Without a unique `seq` for each entry, concurrent updates
    1885              :       * may be applied incorrectly, leading to data inconsistency.
    1886              :       *
    1887              :       * When performing the update, a unique `seq` must be assigned to each metadata row. This can be achieved
    1888              :       * by either incrementing the maximum sequence value in the table or using a function (e.g., `bump_seq(data)`)
    1889              :       * that generates a unique sequence for each row. The update query should ensure that each row moved
    1890              :       * from OLD.pk to NEW.pk gets a distinct `seq` to maintain proper versioning and ordering of changes.
    1891              :      */
    1892              :     
    1893              :     // see https://github.com/sqliteai/sqlite-sync/blob/main/docs/PriKey.md for more details
    1894              :     // pk2 is the old pk
    1895              :     
    1896           30 :     sqlite3_stmt *vm = table->meta_update_move_stmt;
    1897           30 :     if (!vm) return -1;
    1898              :     
    1899              :     // new primary key
    1900           30 :     int rc = sqlite3_bind_blob(vm, 1, pk, (int)pklen, SQLITE_STATIC);
    1901           30 :     if (rc != SQLITE_OK) goto cleanup;
    1902              :     
    1903              :     // new db_version
    1904           30 :     rc = sqlite3_bind_int64(vm, 2, db_version);
    1905           30 :     if (rc != SQLITE_OK) goto cleanup;
    1906              :     
    1907              :     // old primary key
    1908           30 :     rc = sqlite3_bind_blob(vm, 3, pk2, (int)pklen2, SQLITE_STATIC);
    1909           30 :     if (rc != SQLITE_OK) goto cleanup;
    1910              :     
    1911           30 :     rc = sqlite3_step(vm);
    1912           30 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    1913              :     
    1914              : cleanup:
    1915           30 :     DEBUG_SQLITE_ERROR(rc, "local_update_move_meta", db);
    1916           30 :     sqlite3_reset(vm);
    1917           30 :     return rc;
    1918           30 : }
    1919              : 
    1920              : // MARK: - Payload Encode / Decode -
    1921              : 
    1922          615 : bool cloudsync_buffer_free (cloudsync_data_payload *payload) {
    1923          615 :     if (payload) {
    1924          615 :         if (payload->buffer) cloudsync_memory_free(payload->buffer);
    1925          615 :         memset(payload, 0, sizeof(cloudsync_data_payload));
    1926          615 :     }
    1927              :         
    1928          615 :     return false;
    1929              : }
    1930              : 
    1931        48710 : bool cloudsync_buffer_check (cloudsync_data_payload *payload, size_t needed) {
    1932        48710 :     if (payload->nrows == 0) needed += sizeof(cloudsync_payload_header);
    1933              :     
    1934              :     // alloc/resize buffer
    1935        48710 :     if (payload->bused + needed > payload->balloc) {
    1936          624 :         if (needed < CLOUDSYNC_PAYLOAD_MINBUF_SIZE) needed = CLOUDSYNC_PAYLOAD_MINBUF_SIZE;
    1937          624 :         size_t balloc = payload->balloc + needed;
    1938              :         
    1939          624 :         char *buffer = cloudsync_memory_realloc(payload->buffer, balloc);
    1940          624 :         if (!buffer) return cloudsync_buffer_free(payload);
    1941              :         
    1942          624 :         payload->buffer = buffer;
    1943          624 :         payload->balloc = balloc;
    1944          624 :         if (payload->nrows == 0) payload->bused = sizeof(cloudsync_payload_header);
    1945          624 :     }
    1946              :     
    1947        48710 :     return true;
    1948        48710 : }
    1949              : 
    1950          615 : void cloudsync_payload_header_init (cloudsync_payload_header *header, uint32_t expanded_size, uint16_t ncols, uint32_t nrows, uint64_t hash) {
    1951          615 :     memset(header, 0, sizeof(cloudsync_payload_header));
    1952              :     assert(sizeof(cloudsync_payload_header)==32);
    1953              :     
    1954              :     int major, minor, patch;
    1955          615 :     sscanf(CLOUDSYNC_VERSION, "%d.%d.%d", &major, &minor, &patch);
    1956              :     
    1957          615 :     header->signature = htonl(CLOUDSYNC_PAYLOAD_SIGNATURE);
    1958          615 :     header->version = CLOUDSYNC_PAYLOAD_VERSION;
    1959          615 :     header->libversion[0] = major;
    1960          615 :     header->libversion[1] = minor;
    1961          615 :     header->libversion[2] = patch;
    1962          615 :     header->expanded_size = htonl(expanded_size);
    1963          615 :     header->ncols = htons(ncols);
    1964          615 :     header->nrows = htonl(nrows);
    1965          615 :     header->schema_hash = htonll(hash);
    1966          615 : }
    1967              : 
    1968        48710 : void cloudsync_payload_encode_step (sqlite3_context *context, int argc, sqlite3_value **argv) {
    1969              :     DEBUG_FUNCTION("cloudsync_payload_encode_step");
    1970              :     // debug_values(argc, argv);
    1971              :     
    1972              :     // allocate/get the session context
    1973        48710 :     cloudsync_data_payload *payload = (cloudsync_data_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_data_payload));
    1974        48710 :     if (!payload) return;
    1975              :     
    1976              :     // check if the step function is called for the first time
    1977        48710 :     if (payload->nrows == 0) payload->ncols = argc;
    1978              :     
    1979        48710 :     size_t breq = pk_encode_size(argv, argc, 0);
    1980        48710 :     if (cloudsync_buffer_check(payload, breq) == false) return;
    1981              :     
    1982        48710 :     char *buffer = payload->buffer + payload->bused;
    1983        48710 :     char *ptr = pk_encode(argv, argc, buffer, false, NULL);
    1984        48710 :     assert(buffer == ptr);
    1985              :     
    1986              :     // update buffer
    1987        48710 :     payload->bused += breq;
    1988              :     
    1989              :     // increment row counter
    1990        48710 :     ++payload->nrows;
    1991        48710 : }
    1992              : 
    1993          623 : void cloudsync_payload_encode_final (sqlite3_context *context) {
    1994              :     DEBUG_FUNCTION("cloudsync_payload_encode_final");
    1995              : 
    1996              :     // get the session context
    1997          623 :     cloudsync_data_payload *payload = (cloudsync_data_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_data_payload));
    1998          623 :     if (!payload) return;
    1999              :     
    2000          623 :     if (payload->nrows == 0) {
    2001            8 :         sqlite3_result_null(context);
    2002            8 :         return;
    2003              :     }
    2004              :     
    2005              :     // encode payload
    2006          615 :     int header_size = (int)sizeof(cloudsync_payload_header);
    2007          615 :     int real_buffer_size = (int)(payload->bused - header_size);
    2008          615 :     int zbound = LZ4_compressBound(real_buffer_size);
    2009          615 :     char *buffer = cloudsync_memory_alloc(zbound + header_size);
    2010          615 :     if (!buffer) {
    2011            0 :         cloudsync_buffer_free(payload);
    2012            0 :         sqlite3_result_error_code(context, SQLITE_NOMEM);
    2013            0 :         return;
    2014              :     }
    2015              :     
    2016              :     // adjust buffer to compress to skip the reserved header
    2017          615 :     char *src_buffer = payload->buffer + sizeof(cloudsync_payload_header);
    2018          615 :     int zused = LZ4_compress_default(src_buffer, buffer+header_size, real_buffer_size, zbound);
    2019          615 :     bool use_uncompressed_buffer = (!zused || zused > real_buffer_size);
    2020          615 :     CHECK_FORCE_UNCOMPRESSED_BUFFER();
    2021              :     
    2022              :     // setup payload header
    2023          615 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2024              :     cloudsync_payload_header header;
    2025          615 :     cloudsync_payload_header_init(&header, (use_uncompressed_buffer) ? 0 : real_buffer_size, payload->ncols, (uint32_t)payload->nrows, data->schema_hash);
    2026              :     
    2027              :     // if compression fails or if compressed size is bigger than original buffer, then use the uncompressed buffer
    2028          615 :     if (use_uncompressed_buffer) {
    2029            2 :         cloudsync_memory_free(buffer);
    2030            2 :         buffer = payload->buffer;
    2031            2 :         zused = real_buffer_size;
    2032            2 :     }
    2033              :     
    2034              :     // copy header and data to SQLite BLOB
    2035          615 :     memcpy(buffer, &header, sizeof(cloudsync_payload_header));
    2036          615 :     int blob_size = zused+sizeof(cloudsync_payload_header);
    2037          615 :     sqlite3_result_blob(context, buffer, blob_size, SQLITE_TRANSIENT);
    2038              :     
    2039              :     // cleanup memory
    2040          615 :     cloudsync_buffer_free(payload);
    2041          615 :     if (!use_uncompressed_buffer) cloudsync_memory_free(buffer);
    2042          623 : }
    2043              : 
    2044          611 : cloudsync_payload_apply_callback_t cloudsync_get_payload_apply_callback(sqlite3 *db) {
    2045          611 :     return (sqlite3_libversion_number() >= 3044000) ? sqlite3_get_clientdata(db, CLOUDSYNC_PAYLOAD_APPLY_CALLBACK_KEY) : NULL;
    2046              : }
    2047              : 
    2048           22 : void cloudsync_set_payload_apply_callback(sqlite3 *db, cloudsync_payload_apply_callback_t callback) {
    2049           22 :     if (sqlite3_libversion_number() >= 3044000) {
    2050           22 :         sqlite3_set_clientdata(db, CLOUDSYNC_PAYLOAD_APPLY_CALLBACK_KEY, (void*)callback, NULL);
    2051           22 :     }
    2052           22 : }
    2053              : 
    2054       438318 : int cloudsync_pk_decode_bind_callback (void *xdata, int index, int type, int64_t ival, double dval, char *pval) {
    2055       438318 :     cloudsync_pk_decode_bind_context *decode_context = (cloudsync_pk_decode_bind_context*)xdata;
    2056       438318 :     int rc = pk_decode_bind_callback(decode_context->vm, index, type, ival, dval, pval);
    2057              :     
    2058       438318 :     if (rc == SQLITE_OK) {
    2059              :         // the dbversion index is smaller than seq index, so it is processed first
    2060              :         // when processing the dbversion column: save the value to the tmp_dbversion field
    2061              :         // when processing the seq column: update the dbversion and seq fields only if the current dbversion is greater than the last max value
    2062       438318 :         switch (index) {
    2063              :             case CLOUDSYNC_PK_INDEX_TBL:
    2064        48702 :                 if (type == SQLITE_TEXT) {
    2065        48702 :                     decode_context->tbl = pval;
    2066        48702 :                     decode_context->tbl_len = ival;
    2067        48702 :                 }
    2068        48702 :                 break;
    2069              :             case CLOUDSYNC_PK_INDEX_PK:
    2070        48702 :                 if (type == SQLITE_BLOB) {
    2071        48702 :                     decode_context->pk = pval;
    2072        48702 :                     decode_context->pk_len = ival;
    2073        48702 :                 }
    2074        48702 :                 break;
    2075              :             case CLOUDSYNC_PK_INDEX_COLNAME:
    2076        48702 :                 if (type == SQLITE_TEXT) {
    2077        48702 :                     decode_context->col_name = pval;
    2078        48702 :                     decode_context->col_name_len = ival;
    2079        48702 :                 }
    2080        48702 :                 break;
    2081              :             case CLOUDSYNC_PK_INDEX_COLVERSION:
    2082        48702 :                 if (type == SQLITE_INTEGER) decode_context->col_version = ival;
    2083        48702 :                 break;
    2084              :             case CLOUDSYNC_PK_INDEX_DBVERSION:
    2085        48702 :                 if (type == SQLITE_INTEGER) decode_context->db_version = ival;
    2086        48702 :                 break;
    2087              :             case CLOUDSYNC_PK_INDEX_SITEID:
    2088        48702 :                 if (type == SQLITE_BLOB) {
    2089        48702 :                     decode_context->site_id = pval;
    2090        48702 :                     decode_context->site_id_len = ival;
    2091        48702 :                 }
    2092        48702 :                 break;
    2093              :             case CLOUDSYNC_PK_INDEX_CL:
    2094        48702 :                 if (type == SQLITE_INTEGER) decode_context->cl = ival;
    2095        48702 :                 break;
    2096              :             case CLOUDSYNC_PK_INDEX_SEQ:
    2097        48702 :                 if (type == SQLITE_INTEGER) decode_context->seq = ival;
    2098        48702 :                 break;
    2099              :         }
    2100       438318 :     }
    2101              :         
    2102       438318 :     return rc;
    2103              : }
    2104              : 
    2105              : // #ifndef CLOUDSYNC_OMIT_RLS_VALIDATION
    2106              : 
    2107          611 : int cloudsync_payload_apply (sqlite3_context *context, const char *payload, int blen) {
    2108              :     // decode header
    2109              :     cloudsync_payload_header header;
    2110          611 :     memcpy(&header, payload, sizeof(cloudsync_payload_header));
    2111              : 
    2112          611 :     header.signature = ntohl(header.signature);
    2113          611 :     header.expanded_size = ntohl(header.expanded_size);
    2114          611 :     header.ncols = ntohs(header.ncols);
    2115          611 :     header.nrows = ntohl(header.nrows);
    2116          611 :     header.schema_hash = ntohll(header.schema_hash);
    2117              :     
    2118          611 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2119              :     
    2120              :     // sanity check header
    2121          611 :     if ((header.signature != CLOUDSYNC_PAYLOAD_SIGNATURE) || (header.ncols == 0)) {
    2122            0 :         dbutils_context_result_error(context, "Error on cloudsync_payload_apply: invalid signature or column size.");
    2123            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    2124            0 :         return -1;
    2125              :     }
    2126              :     
    2127          611 :     const char *buffer = payload + sizeof(cloudsync_payload_header);
    2128          611 :     blen -= sizeof(cloudsync_payload_header);
    2129              :     
    2130              :     // check if payload is compressed
    2131          611 :     char *clone = NULL;
    2132          611 :     if (header.expanded_size != 0) {
    2133          609 :         clone = (char *)cloudsync_memory_alloc(header.expanded_size);
    2134          609 :         if (!clone) {sqlite3_result_error_code(context, SQLITE_NOMEM); return -1;}
    2135              :         
    2136          609 :         uint32_t rc = LZ4_decompress_safe(buffer, clone, blen, header.expanded_size);
    2137          609 :         if (rc <= 0 || rc != header.expanded_size) {
    2138            0 :             dbutils_context_result_error(context, "Error on cloudsync_payload_apply: unable to decompress BLOB (%d).", rc);
    2139            0 :             sqlite3_result_error_code(context, SQLITE_MISUSE);
    2140            0 :             return -1;
    2141              :         }
    2142              :         
    2143          609 :         buffer = (const char *)clone;
    2144          609 :     }
    2145              :     
    2146          611 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2147              :     
    2148              :     // precompile the insert statement
    2149          611 :     sqlite3_stmt *vm = NULL;
    2150          611 :     const char *sql = "INSERT INTO cloudsync_changes(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq) VALUES (?,?,?,?,?,?,?,?,?);";
    2151          611 :     int rc = sqlite3_prepare(db, sql, -1, &vm, NULL);
    2152          611 :     if (rc != SQLITE_OK) {
    2153            0 :         dbutils_context_result_error(context, "Error on cloudsync_payload_apply: error while compiling SQL statement (%s).", sqlite3_errmsg(db));
    2154            0 :         if (clone) cloudsync_memory_free(clone);
    2155            0 :         return -1;
    2156              :     }
    2157              :     
    2158              :     // process buffer, one row at a time
    2159          611 :     uint16_t ncols = header.ncols;
    2160          611 :     uint32_t nrows = header.nrows;
    2161          611 :     int64_t last_payload_db_version = -1;
    2162          611 :     bool in_savepoint = false;
    2163          611 :     int dbversion = dbutils_settings_get_int_value(db, CLOUDSYNC_KEY_CHECK_DBVERSION);
    2164          611 :     int seq = dbutils_settings_get_int_value(db, CLOUDSYNC_KEY_CHECK_SEQ);
    2165          611 :     cloudsync_pk_decode_bind_context decoded_context = {.vm = vm};
    2166          611 :     void *payload_apply_xdata = NULL;
    2167          611 :     cloudsync_payload_apply_callback_t payload_apply_callback = cloudsync_get_payload_apply_callback(db);
    2168              :     
    2169        49313 :     for (uint32_t i=0; i<nrows; ++i) {
    2170        48702 :         size_t seek = 0;
    2171        48702 :         pk_decode((char *)buffer, blen, ncols, &seek, cloudsync_pk_decode_bind_callback, &decoded_context);
    2172              :         // n is the pk_decode return value, I don't think I should assert here because in any case the next sqlite3_step would fail
    2173              :         // assert(n == ncols);
    2174              :                 
    2175        48702 :         bool approved = true;
    2176        48702 :         if (payload_apply_callback) approved = payload_apply_callback(&payload_apply_xdata, &decoded_context, db, data, CLOUDSYNC_PAYLOAD_APPLY_WILL_APPLY, SQLITE_OK);
    2177              :         
    2178              :         // Apply consecutive rows with the same db_version inside a transaction if no
    2179              :         // transaction has already been opened.
    2180              :         // The user may have already opened a transaction before applying the payload,
    2181              :         // and the `payload_apply_callback` may have already opened a savepoint.
    2182              :         // Nested savepoints work, but overlapping savepoints could alter the expected behavior.
    2183              :         // This savepoint ensures that the db_version value remains consistent for all
    2184              :         // rows with the same original db_version in the payload.
    2185              : 
    2186        48702 :         bool db_version_changed = (last_payload_db_version != decoded_context.db_version);
    2187              : 
    2188              :         // Release existing savepoint if db_version changed
    2189        48702 :         if (in_savepoint && db_version_changed) {
    2190         1539 :             rc = sqlite3_exec(db, "RELEASE cloudsync_payload_apply;", NULL, NULL, NULL);
    2191         1539 :             if (rc != SQLITE_OK) {
    2192            0 :                 dbutils_context_result_error(context, "Error on cloudsync_payload_apply: unable to release a savepoint (%s).", sqlite3_errmsg(db));
    2193            0 :                 if (clone) cloudsync_memory_free(clone);
    2194            0 :                 return -1;
    2195              :             }
    2196         1539 :             in_savepoint = false;
    2197         1539 :         }
    2198              : 
    2199              :         // Start new savepoint if needed
    2200        48702 :         bool in_transaction = sqlite3_get_autocommit(db) != true;
    2201        48702 :         if (!in_transaction && db_version_changed) {
    2202         1682 :             rc = sqlite3_exec(db, "SAVEPOINT cloudsync_payload_apply;", NULL, NULL, NULL);
    2203         1682 :             if (rc != SQLITE_OK) {
    2204            0 :                 dbutils_context_result_error(context, "Error on cloudsync_payload_apply: unable to start a transaction (%s).", sqlite3_errmsg(db));
    2205            0 :                 if (clone) cloudsync_memory_free(clone);
    2206            0 :                 return -1;
    2207              :             }
    2208         1682 :             last_payload_db_version = decoded_context.db_version;
    2209         1682 :             in_savepoint = true;
    2210         1682 :         }
    2211              :         
    2212        48702 :         if (approved) {
    2213        48581 :             rc = sqlite3_step(vm);
    2214        48581 :             if (rc != SQLITE_DONE) {
    2215              :                 // don't "break;", the error can be due to a RLS policy.
    2216              :                 // in case of error we try to apply the following changes
    2217            0 :                 printf("cloudsync_payload_apply error on db_version %lld/%lld: (%d) %s\n", decoded_context.db_version, decoded_context.seq, rc, sqlite3_errmsg(db));
    2218            0 :             }
    2219        48581 :         }
    2220              :         
    2221        48702 :         if (payload_apply_callback) payload_apply_callback(&payload_apply_xdata, &decoded_context, db, data, CLOUDSYNC_PAYLOAD_APPLY_DID_APPLY, rc);
    2222              :         
    2223        48702 :         buffer += seek;
    2224        48702 :         blen -= seek;
    2225        48702 :         stmt_reset(vm);
    2226        48702 :     }
    2227              :     
    2228          611 :     if (in_savepoint) {
    2229          143 :         sql = "RELEASE cloudsync_payload_apply;";
    2230          143 :         int rc1 = sqlite3_exec(db, sql, NULL, NULL, NULL);
    2231          143 :         if (rc1 != SQLITE_OK) rc = rc1;
    2232          143 :     }
    2233              : 
    2234          611 :     char *lasterr = (rc != SQLITE_OK && rc != SQLITE_DONE) ? cloudsync_string_dup(sqlite3_errmsg(db), false) : NULL;
    2235              :     
    2236          611 :     if (payload_apply_callback) {
    2237          468 :         payload_apply_callback(&payload_apply_xdata, &decoded_context, db, data, CLOUDSYNC_PAYLOAD_APPLY_CLEANUP, rc);
    2238          468 :     }
    2239              : 
    2240          611 :     if (rc == SQLITE_DONE) rc = SQLITE_OK;
    2241          611 :     if (rc == SQLITE_OK) {
    2242              :         char buf[256];
    2243          611 :         if (decoded_context.db_version >= dbversion) {
    2244          479 :             snprintf(buf, sizeof(buf), "%lld", decoded_context.db_version);
    2245          479 :             dbutils_settings_set_key_value(db, context, CLOUDSYNC_KEY_CHECK_DBVERSION, buf);
    2246              :             
    2247          479 :             if (decoded_context.seq != seq) {
    2248          304 :                 snprintf(buf, sizeof(buf), "%lld", decoded_context.seq);
    2249          304 :                 dbutils_settings_set_key_value(db, context, CLOUDSYNC_KEY_CHECK_SEQ, buf);
    2250          304 :             }
    2251          479 :         }
    2252          611 :     }
    2253              : 
    2254              :     // cleanup vm
    2255          611 :     if (vm) sqlite3_finalize(vm);
    2256              :     
    2257              :     // cleanup memory
    2258          611 :     if (clone) cloudsync_memory_free(clone);
    2259              :     
    2260          611 :     if (rc != SQLITE_OK) {
    2261            0 :         sqlite3_result_error(context, lasterr, -1);
    2262            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    2263            0 :         cloudsync_memory_free(lasterr);
    2264            0 :         return -1;
    2265              :     }
    2266              :     
    2267              :     // return the number of processed rows
    2268          611 :     sqlite3_result_int(context, nrows);
    2269          611 :     return nrows;
    2270          611 : }
    2271              : 
    2272          611 : void cloudsync_payload_decode (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2273              :     DEBUG_FUNCTION("cloudsync_payload_decode");
    2274              :     //debug_values(argc, argv);
    2275              :     
    2276              :     // sanity check payload type
    2277          611 :     if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) {
    2278            0 :         dbutils_context_result_error(context, "Error on cloudsync_payload_decode: value must be a BLOB.");
    2279            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    2280            0 :         return;
    2281              :     }
    2282              :     
    2283              :     // sanity check payload size
    2284          611 :     int blen = sqlite3_value_bytes(argv[0]);
    2285          611 :     if (blen < (int)sizeof(cloudsync_payload_header)) {
    2286            0 :         dbutils_context_result_error(context, "Error on cloudsync_payload_decode: invalid input size.");
    2287            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    2288            0 :         return;
    2289              :     }
    2290              :     
    2291              :     // obtain payload
    2292          611 :     const char *payload = (const char *)sqlite3_value_blob(argv[0]);
    2293              :     
    2294              :     // apply changes
    2295          611 :     cloudsync_payload_apply(context, payload, blen);
    2296          611 : }
    2297              : 
    2298              : // MARK: - Payload load/store -
    2299              : 
    2300            0 : int cloudsync_payload_get (sqlite3_context *context, char **blob, int *blob_size, int *db_version, int *seq, sqlite3_int64 *new_db_version, sqlite3_int64 *new_seq) {
    2301            0 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2302              : 
    2303            0 :     *db_version = dbutils_settings_get_int_value(db, CLOUDSYNC_KEY_SEND_DBVERSION);
    2304            0 :     if (*db_version < 0) {sqlite3_result_error(context, "Unable to retrieve db_version.", -1); return SQLITE_ERROR;}
    2305              : 
    2306            0 :     *seq = dbutils_settings_get_int_value(db, CLOUDSYNC_KEY_SEND_SEQ);
    2307            0 :     if (*seq < 0) {sqlite3_result_error(context, "Unable to retrieve seq.", -1); return SQLITE_ERROR;}
    2308              :     
    2309              :     // retrieve BLOB
    2310              :     char sql[1024];
    2311            0 :     snprintf(sql, sizeof(sql), "WITH max_db_version AS (SELECT MAX(db_version) AS max_db_version FROM cloudsync_changes) "
    2312              :                                "SELECT cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq), max_db_version AS max_db_version, MAX(IIF(db_version = max_db_version, seq, NULL)) FROM cloudsync_changes, max_db_version WHERE site_id=cloudsync_siteid() AND (db_version>%d OR (db_version=%d AND seq>%d))", *db_version, *db_version, *seq);
    2313              :     
    2314            0 :     int rc = dbutils_blob_int_int_select(db, sql, blob, blob_size, new_db_version, new_seq);
    2315            0 :     if (rc != SQLITE_OK) {
    2316            0 :         sqlite3_result_error(context, "cloudsync_network_send_changes unable to get changes", -1);
    2317            0 :         sqlite3_result_error_code(context, rc);
    2318            0 :         return rc;
    2319              :     }
    2320              :     
    2321              :     // exit if there is no data to send
    2322            0 :     if (blob == NULL || blob_size == 0) return SQLITE_OK;
    2323            0 :     return rc;
    2324            0 : }
    2325              : 
    2326              : #ifdef CLOUDSYNC_DESKTOP_OS
    2327              : 
    2328            0 : void cloudsync_payload_save (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2329              :     DEBUG_FUNCTION("cloudsync_payload_save");
    2330              :     
    2331              :     // sanity check argument
    2332            0 :     if (sqlite3_value_type(argv[0]) != SQLITE_TEXT) {
    2333            0 :         sqlite3_result_error(context, "Unable to retrieve file path.", -1);
    2334            0 :         return;
    2335              :     }
    2336              :     
    2337              :     // retrieve full path to file
    2338            0 :     const char *path = (const char *)sqlite3_value_text(argv[0]);
    2339            0 :     cloudsync_file_delete(path);
    2340              :     
    2341              :     // retrieve payload
    2342            0 :     char *blob = NULL;
    2343            0 :     int blob_size = 0, db_version = 0, seq = 0;
    2344            0 :     sqlite3_int64 new_db_version = 0, new_seq = 0;
    2345            0 :     int rc = cloudsync_payload_get(context, &blob, &blob_size, &db_version, &seq, &new_db_version, &new_seq);
    2346            0 :     if (rc != SQLITE_OK) return;
    2347              :     
    2348              :     // exit if there is no data to send
    2349            0 :     if (blob == NULL || blob_size == 0) return;
    2350              :     
    2351              :     // write payload to file
    2352            0 :     bool res = cloudsync_file_write(path, blob, (size_t)blob_size);
    2353            0 :     sqlite3_free(blob);
    2354              :     
    2355            0 :     if (res == false) {
    2356            0 :         sqlite3_result_error(context, "Unable to write payload to file path.", -1);
    2357            0 :         return;
    2358              :     }
    2359              :     
    2360              :     // update db_version and seq
    2361              :     char buf[256];
    2362            0 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2363            0 :     if (new_db_version != db_version) {
    2364            0 :         snprintf(buf, sizeof(buf), "%lld", new_db_version);
    2365            0 :         dbutils_settings_set_key_value(db, context, CLOUDSYNC_KEY_SEND_DBVERSION, buf);
    2366            0 :     }
    2367            0 :     if (new_seq != seq) {
    2368            0 :         snprintf(buf, sizeof(buf), "%lld", new_seq);
    2369            0 :         dbutils_settings_set_key_value(db, context, CLOUDSYNC_KEY_SEND_SEQ, buf);
    2370            0 :     }
    2371              :     
    2372              :     // returns blob size
    2373            0 :     sqlite3_result_int64(context, (sqlite3_int64)blob_size);
    2374            0 : }
    2375              : 
    2376            0 : void cloudsync_payload_load (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2377              :     DEBUG_FUNCTION("cloudsync_payload_load");
    2378              :     
    2379              :     // sanity check argument
    2380            0 :     if (sqlite3_value_type(argv[0]) != SQLITE_TEXT) {
    2381            0 :         sqlite3_result_error(context, "Unable to retrieve file path.", -1);
    2382            0 :         return;
    2383              :     }
    2384              :     
    2385              :     // retrieve full path to file
    2386            0 :     const char *path = (const char *)sqlite3_value_text(argv[0]);
    2387              :     
    2388            0 :     sqlite3_int64 payload_size = 0;
    2389            0 :     char *payload = cloudsync_file_read(path, &payload_size);
    2390            0 :     if (!payload) {
    2391            0 :         if (payload_size == -1) sqlite3_result_error(context, "Unable to read payload from file path.", -1);
    2392            0 :         if (payload) cloudsync_memory_free(payload);
    2393            0 :         return;
    2394              :     }
    2395              :     
    2396            0 :     int nrows = (payload_size) ? cloudsync_payload_apply (context, payload, (int)payload_size) : 0;
    2397            0 :     if (payload) cloudsync_memory_free(payload);
    2398              :     
    2399              :     // returns number of applied rows
    2400            0 :     if (nrows != -1) sqlite3_result_int(context, nrows);
    2401            0 : }
    2402              : 
    2403              : #endif
    2404              : 
    2405              : // MARK: - Public -
    2406              : 
    2407            7 : void cloudsync_version (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2408              :     DEBUG_FUNCTION("cloudsync_version");
    2409            7 :     UNUSED_PARAMETER(argc);
    2410            7 :     UNUSED_PARAMETER(argv);
    2411            7 :     sqlite3_result_text(context, CLOUDSYNC_VERSION, -1, SQLITE_STATIC);
    2412            7 : }
    2413              : 
    2414          471 : void cloudsync_siteid (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2415              :     DEBUG_FUNCTION("cloudsync_siteid");
    2416          471 :     UNUSED_PARAMETER(argc);
    2417          471 :     UNUSED_PARAMETER(argv);
    2418              :     
    2419          471 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2420          471 :     sqlite3_result_blob(context, data->site_id, UUID_LEN, SQLITE_STATIC);
    2421          471 : }
    2422              : 
    2423            3 : void cloudsync_db_version (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2424              :     DEBUG_FUNCTION("cloudsync_db_version");
    2425            3 :     UNUSED_PARAMETER(argc);
    2426            3 :     UNUSED_PARAMETER(argv);
    2427              :     
    2428              :     // retrieve context
    2429            3 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2430            3 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2431              :     
    2432            3 :     int rc = db_version_check_uptodate(db, data);
    2433            3 :     if (rc != SQLITE_OK) {
    2434            0 :         dbutils_context_result_error(context, "Unable to retrieve db_version (%s).", sqlite3_errmsg(db));
    2435            0 :         return;
    2436              :     }
    2437              :     
    2438            3 :     sqlite3_result_int64(context, data->db_version);
    2439            3 : }
    2440              : 
    2441        24633 : void cloudsync_db_version_next (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2442              :     DEBUG_FUNCTION("cloudsync_db_version_next");
    2443              :     
    2444              :     // retrieve context
    2445        24633 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2446        24633 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2447              :     
    2448        24633 :     sqlite3_int64 merging_version = (argc == 1) ? sqlite3_value_int64(argv[0]) : CLOUDSYNC_VALUE_NOTSET;
    2449        24633 :     sqlite3_int64 value = db_version_next(db, data, merging_version);
    2450        24633 :     if (value == -1) {
    2451            0 :         dbutils_context_result_error(context, "Unable to retrieve next_db_version (%s).", sqlite3_errmsg(db));
    2452            0 :         return;
    2453              :     }
    2454              :     
    2455        24633 :     sqlite3_result_int64(context, value);
    2456        24633 : }
    2457              : 
    2458           22 : void cloudsync_seq (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2459              :     DEBUG_FUNCTION("cloudsync_seq");
    2460              :     
    2461              :     // retrieve context
    2462           22 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2463           22 :     sqlite3_result_int(context, BUMP_SEQ(data));
    2464           22 : }
    2465              : 
    2466            1 : void cloudsync_uuid (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2467              :     DEBUG_FUNCTION("cloudsync_uuid");
    2468              :     
    2469              :     char value[UUID_STR_MAXLEN];
    2470            1 :     char *uuid = cloudsync_uuid_v7_string(value, true);
    2471            1 :     sqlite3_result_text(context, uuid, -1, SQLITE_TRANSIENT);
    2472            1 : }
    2473              : 
    2474              : // MARK: -
    2475              : 
    2476            1 : void cloudsync_set (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2477              :     DEBUG_FUNCTION("cloudsync_set");
    2478              :     
    2479              :     // sanity check parameters
    2480            1 :     const char *key = (const char *)sqlite3_value_text(argv[0]);
    2481            1 :     const char *value = (const char *)sqlite3_value_text(argv[1]);
    2482              :     
    2483              :     // silently fails
    2484            1 :     if (key == NULL) return;
    2485              :     
    2486            1 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2487            1 :     dbutils_settings_set_key_value(db, context, key, value);
    2488            1 : }
    2489              : 
    2490            1 : void cloudsync_set_column (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2491              :     DEBUG_FUNCTION("cloudsync_set_column");
    2492              :     
    2493            1 :     const char *tbl = (const char *)sqlite3_value_text(argv[0]);
    2494            1 :     const char *col = (const char *)sqlite3_value_text(argv[1]);
    2495            1 :     const char *key = (const char *)sqlite3_value_text(argv[2]);
    2496            1 :     const char *value = (const char *)sqlite3_value_text(argv[3]);
    2497            1 :     dbutils_table_settings_set_key_value(NULL, context, tbl, col, key, value);
    2498            1 : }
    2499              : 
    2500            1 : void cloudsync_set_table (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2501              :     DEBUG_FUNCTION("cloudsync_set_table");
    2502              :     
    2503            1 :     const char *tbl = (const char *)sqlite3_value_text(argv[0]);
    2504            1 :     const char *key = (const char *)sqlite3_value_text(argv[1]);
    2505            1 :     const char *value = (const char *)sqlite3_value_text(argv[2]);
    2506            1 :     dbutils_table_settings_set_key_value(NULL, context, tbl, "*", key, value);
    2507            1 : }
    2508              : 
    2509        25559 : void cloudsync_is_sync (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2510              :     DEBUG_FUNCTION("cloudsync_is_sync");
    2511              :     
    2512        25559 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2513        25559 :     if (data->insync) {
    2514        21756 :         sqlite3_result_int(context, 1);
    2515        21756 :         return;
    2516              :     }
    2517              :     
    2518         3803 :     const char *table_name = (const char *)sqlite3_value_text(argv[0]);
    2519         3803 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2520         3803 :     sqlite3_result_int(context, (table) ? (table->enabled == 0) : 0);
    2521        25559 : }
    2522              : 
    2523       125592 : void cloudsync_col_value (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2524              :     // DEBUG_FUNCTION("cloudsync_col_value");
    2525              :     
    2526              :     // argv[0] -> table name
    2527              :     // argv[1] -> column name
    2528              :     // argv[2] -> encoded pk
    2529              :     
    2530              :     // lookup table
    2531       125592 :     const char *table_name = (const char *)sqlite3_value_text(argv[0]);
    2532       125592 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2533       125592 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2534       125592 :     if (!table) {
    2535            0 :         dbutils_context_result_error(context, "Unable to retrieve table name %s in clousdsync_colvalue.", table_name);
    2536            0 :         return;
    2537              :     }
    2538              :     
    2539              :     // retrieve column name
    2540       125592 :     const char *col_name = (const char *)sqlite3_value_text(argv[1]);
    2541              :     
    2542              :     // check for special tombstone value
    2543       125592 :     if (strcmp(col_name, CLOUDSYNC_TOMBSTONE_VALUE) == 0) {
    2544         4034 :         sqlite3_result_null(context);
    2545         4034 :         return;
    2546              :     }
    2547              :     
    2548              :     // extract the right col_value vm associated to the column name
    2549       121558 :     sqlite3_stmt *vm = table_column_lookup(table, col_name, false, NULL);
    2550       121558 :     if (!vm) {
    2551            0 :         sqlite3_result_error(context, "Unable to retrieve column value precompiled statement in clousdsync_colvalue.", -1);
    2552            0 :         return;
    2553              :     }
    2554              :     
    2555              :     // bind primary key values
    2556       121558 :     int rc = pk_decode_prikey((char *)sqlite3_value_blob(argv[2]), (size_t)sqlite3_value_bytes(argv[2]), pk_decode_bind_callback, (void *)vm);
    2557       121558 :     if (rc < 0) goto cleanup;
    2558              :     
    2559              :     // execute vm
    2560       121558 :     rc = sqlite3_step(vm);
    2561       243116 :     if (rc == SQLITE_DONE) {
    2562            0 :         rc = SQLITE_OK;
    2563            0 :         sqlite3_result_text(context, CLOUDSYNC_RLS_RESTRICTED_VALUE, -1, SQLITE_STATIC);
    2564       121558 :     } else if (rc == SQLITE_ROW) {
    2565              :         // store value result
    2566       121558 :         rc = SQLITE_OK;
    2567       121558 :         sqlite3_result_value(context, sqlite3_column_value(vm, 0));
    2568       121558 :     }
    2569              :     
    2570              : cleanup:
    2571       121558 :     if (rc != SQLITE_OK) {
    2572            0 :         sqlite3 *db = sqlite3_context_db_handle(context);
    2573            0 :         sqlite3_result_error(context, sqlite3_errmsg(db), -1);
    2574            0 :     }
    2575       121558 :     sqlite3_reset(vm);
    2576       125592 : }
    2577              : 
    2578        10365 : void cloudsync_pk_encode (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2579        10365 :     size_t bsize = 0;
    2580        10365 :     char *buffer = pk_encode_prikey(argv, argc, NULL, &bsize);
    2581        10365 :     if (!buffer) {
    2582            0 :         sqlite3_result_null(context);
    2583            0 :         return;
    2584              :     }
    2585        10365 :     sqlite3_result_blob(context, (const void *)buffer, (int)bsize, SQLITE_TRANSIENT);
    2586        10365 :     cloudsync_memory_free(buffer);
    2587        10365 : }
    2588              : 
    2589          380 : int cloudsync_pk_decode_set_result_callback (void *xdata, int index, int type, int64_t ival, double dval, char *pval) {
    2590          380 :     cloudsync_pk_decode_context *decode_context = (cloudsync_pk_decode_context *)xdata;
    2591              :     // decode_context->index is 1 based
    2592              :     // index is 0 based
    2593          380 :     if (decode_context->index != index+1) return SQLITE_OK;
    2594              :     
    2595          190 :     int rc = 0;
    2596          190 :     sqlite3_context *context = decode_context->context;
    2597          190 :     switch (type) {
    2598              :         case SQLITE_INTEGER:
    2599            0 :             sqlite3_result_int64(context, ival);
    2600            0 :             break;
    2601              : 
    2602              :         case SQLITE_FLOAT:
    2603            0 :             sqlite3_result_double(context, dval);
    2604            0 :             break;
    2605              : 
    2606              :         case SQLITE_NULL:
    2607            0 :             sqlite3_result_null(context);
    2608            0 :             break;
    2609              : 
    2610              :         case SQLITE_TEXT:
    2611          190 :             sqlite3_result_text(context, pval, (int)ival, SQLITE_TRANSIENT);
    2612          190 :             break;
    2613              : 
    2614              :         case SQLITE_BLOB:
    2615            0 :             sqlite3_result_blob(context, pval, (int)ival, SQLITE_TRANSIENT);
    2616            0 :             break;
    2617              :     }
    2618              :     
    2619          190 :     return rc;
    2620          380 : }
    2621              : 
    2622              : 
    2623          190 : void cloudsync_pk_decode (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2624          190 :     const char *pk = (const char *)sqlite3_value_text(argv[0]);
    2625          190 :     int i = sqlite3_value_int(argv[1]);
    2626              :     
    2627          190 :     cloudsync_pk_decode_context xdata = {.context = context, .index = i};
    2628          190 :     pk_decode_prikey((char *)pk, strlen(pk), cloudsync_pk_decode_set_result_callback, &xdata);
    2629          190 : }
    2630              : 
    2631              : // MARK: -
    2632              : 
    2633         3468 : void cloudsync_insert (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2634              :     DEBUG_FUNCTION("cloudsync_insert %s", sqlite3_value_text(argv[0]));
    2635              :     // debug_values(argc-1, &argv[1]);
    2636              :     
    2637              :     // argv[0] is table name
    2638              :     // argv[1]..[N] is primary key(s)
    2639              :     
    2640              :     // table_cloudsync
    2641              :     // pk               -> encode(argc-1, &argv[1])
    2642              :     // col_name         -> name
    2643              :     // col_version      -> 0/1 +1
    2644              :     // db_version       -> check
    2645              :     // site_id          0
    2646              :     // seq              -> sqlite_master
    2647              :     
    2648              :     // retrieve context
    2649         3468 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2650         3468 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2651              :     
    2652              :     // lookup table
    2653         3468 :     const char *table_name = (const char *)sqlite3_value_text(argv[0]);
    2654         3468 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2655         3468 :     if (!table) {
    2656            0 :         dbutils_context_result_error(context, "Unable to retrieve table name %s in cloudsync_insert.", table_name);
    2657            0 :         return;
    2658              :     }
    2659              :     
    2660              :     // encode the primary key values into a buffer
    2661              :     char buffer[1024];
    2662         3468 :     size_t pklen = sizeof(buffer);
    2663         3468 :     char *pk = pk_encode_prikey(&argv[1], table->npks, buffer, &pklen);
    2664         3468 :     if (!pk) {
    2665            0 :         sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
    2666            0 :         return;
    2667              :     }
    2668              :     
    2669              :     // compute the next database version for tracking changes
    2670         3468 :     sqlite3_int64 db_version = db_version_next(db, data, CLOUDSYNC_VALUE_NOTSET);
    2671              :     
    2672              :     // check if a row with the same primary key already exists
    2673              :     // if so, this means the row might have been previously deleted (sentinel)
    2674         3468 :     bool pk_exists = (bool)stmt_count(table->meta_pkexists_stmt, pk, pklen, SQLITE_BLOB);
    2675         3468 :     int rc = SQLITE_OK;
    2676              :     
    2677         3468 :     if (table->ncols == 0) {
    2678              :         // if there are no columns other than primary keys, insert a sentinel record
    2679           91 :         rc = local_mark_insert_sentinel_meta(db, table, pk, pklen, db_version, BUMP_SEQ(data));
    2680           91 :         if (rc != SQLITE_OK) goto cleanup;
    2681         3468 :     } else if (pk_exists){
    2682              :         // if a row with the same primary key already exists, update the sentinel record
    2683            1 :         rc = local_update_sentinel(db, table, pk, pklen, db_version, BUMP_SEQ(data));
    2684            1 :         if (rc != SQLITE_OK) goto cleanup;
    2685            1 :     }
    2686              :     
    2687              :     // process each non-primary key column for insert or update
    2688        14049 :     for (int i=0; i<table->ncols; ++i) {
    2689              :         // mark the column as inserted or updated in the metadata
    2690        10581 :         rc = local_mark_insert_or_update_meta(db, table, pk, pklen, table->col_name[i], db_version, BUMP_SEQ(data));
    2691        10581 :         if (rc != SQLITE_OK) goto cleanup;
    2692        14049 :     }
    2693              :     
    2694              : cleanup:
    2695         3468 :     if (rc != SQLITE_OK) sqlite3_result_error(context, sqlite3_errmsg(db), -1);
    2696              :     // free memory if the primary key was dynamically allocated
    2697         3468 :     if (pk != buffer) cloudsync_memory_free(pk);
    2698         3468 : }
    2699              : 
    2700           28 : void cloudsync_delete (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2701              :     DEBUG_FUNCTION("cloudsync_delete %s", sqlite3_value_text(argv[0]));
    2702              :     // debug_values(argc-1, &argv[1]);
    2703              :     
    2704              :     // retrieve context
    2705           28 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2706           28 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2707              :     
    2708              :     // lookup table
    2709           28 :     const char *table_name = (const char *)sqlite3_value_text(argv[0]);
    2710           28 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2711           28 :     if (!table) {
    2712            0 :         dbutils_context_result_error(context, "Unable to retrieve table name %s in cloudsync_delete.", table_name);
    2713            0 :         return;
    2714              :     }
    2715              :     
    2716              :     // compute the next database version for tracking changes
    2717           28 :     sqlite3_int64 db_version = db_version_next(db, data, CLOUDSYNC_VALUE_NOTSET);
    2718           28 :     int rc = SQLITE_OK;
    2719              :     
    2720              :     // encode the primary key values into a buffer
    2721              :     char buffer[1024];
    2722           28 :     size_t pklen = sizeof(buffer);
    2723           28 :     char *pk = pk_encode_prikey(&argv[1], table->npks, buffer, &pklen);
    2724           28 :     if (!pk) {
    2725            0 :         sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
    2726            0 :         return;
    2727              :     }
    2728              :     
    2729              :     // mark the row as deleted by inserting a delete sentinel into the metadata
    2730           28 :     rc = local_mark_delete_meta(db, table, pk, pklen, db_version, BUMP_SEQ(data));
    2731           28 :     if (rc != SQLITE_OK) goto cleanup;
    2732              :     
    2733              :     // remove any metadata related to the old rows associated with this primary key
    2734           28 :     rc = local_drop_meta(db, table, pk, pklen);
    2735           28 :     if (rc != SQLITE_OK) goto cleanup;
    2736              :     
    2737              : cleanup:
    2738           28 :     if (rc != SQLITE_OK) sqlite3_result_error(context, sqlite3_errmsg(db), -1);
    2739              :     // free memory if the primary key was dynamically allocated
    2740           28 :     if (pk != buffer) cloudsync_memory_free(pk);
    2741           28 : }
    2742              : 
    2743              : // MARK: -
    2744              : 
    2745          337 : void cloudsync_update_payload_free (cloudsync_update_payload *payload) {
    2746         2303 :     for (int i=0; i<payload->count; i++) {
    2747         1966 :         sqlite3_value_free(payload->new_values[i]);
    2748         1966 :         sqlite3_value_free(payload->old_values[i]);
    2749         1966 :     }
    2750          337 :     cloudsync_memory_free(payload->new_values);
    2751          337 :     cloudsync_memory_free(payload->old_values);
    2752          337 :     sqlite3_value_free(payload->table_name);
    2753          337 :     payload->new_values = NULL;
    2754          337 :     payload->old_values = NULL;
    2755          337 :     payload->table_name = NULL;
    2756          337 :     payload->count = 0;
    2757          337 :     payload->capacity = 0;
    2758          337 : }
    2759              : 
    2760         1966 : int cloudsync_update_payload_append (cloudsync_update_payload *payload, sqlite3_value *v1, sqlite3_value *v2, sqlite3_value *v3) {
    2761         1966 :     if (payload->count >= payload->capacity) {
    2762          340 :         int newcap = payload->capacity ? payload->capacity * 2 : 128;
    2763              :         
    2764          340 :         sqlite3_value **new_values_2 = (sqlite3_value **)cloudsync_memory_realloc(payload->new_values, newcap * sizeof(*new_values_2));
    2765          340 :         if (!new_values_2) return SQLITE_NOMEM;
    2766          340 :         payload->new_values = new_values_2;
    2767              :         
    2768          340 :         sqlite3_value **old_values_2 = (sqlite3_value **)cloudsync_memory_realloc(payload->old_values, newcap * sizeof(*old_values_2));
    2769          340 :         if (!old_values_2) return SQLITE_NOMEM;
    2770          340 :         payload->old_values = old_values_2;
    2771              :         
    2772          340 :         payload->capacity = newcap;
    2773          340 :     }
    2774              :     
    2775         1966 :     int index = payload->count;
    2776         1966 :     if (payload->table_name == NULL) payload->table_name = sqlite3_value_dup(v1);
    2777         1629 :     else if (dbutils_value_compare(payload->table_name, v1) != 0) return SQLITE_NOMEM;
    2778         1966 :     payload->new_values[index] = sqlite3_value_dup(v2);
    2779         1966 :     payload->old_values[index] = sqlite3_value_dup(v3);
    2780         1966 :     payload->count++;
    2781              :     
    2782              :     // sanity check memory allocations
    2783         1966 :     bool v1_can_be_null = (sqlite3_value_type(v1) == SQLITE_NULL);
    2784         1966 :     bool v2_can_be_null = (sqlite3_value_type(v2) == SQLITE_NULL);
    2785         1966 :     bool v3_can_be_null = (sqlite3_value_type(v3) == SQLITE_NULL);
    2786              :     
    2787         1966 :     if ((payload->table_name == NULL) && (!v1_can_be_null)) return SQLITE_NOMEM;
    2788         1966 :     if ((payload->old_values[index] == NULL) && (!v2_can_be_null)) return SQLITE_NOMEM;
    2789         1966 :     if ((payload->new_values[index] == NULL) && (!v3_can_be_null)) return SQLITE_NOMEM;
    2790              :     
    2791         1966 :     return SQLITE_OK;
    2792         1966 : }
    2793              : 
    2794         1966 : void cloudsync_update_step (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2795              :     // argv[0] => table_name
    2796              :     // argv[1] => new_column_value
    2797              :     // argv[2] => old_column_value
    2798              :     
    2799              :     // allocate/get the update payload
    2800         1966 :     cloudsync_update_payload *payload = (cloudsync_update_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_update_payload));
    2801         1966 :     if (!payload) {sqlite3_result_error_nomem(context); return;}
    2802              :     
    2803         1966 :     if (cloudsync_update_payload_append(payload, argv[0], argv[1], argv[2]) != SQLITE_OK) {
    2804            0 :         sqlite3_result_error_nomem(context);
    2805            0 :     }
    2806         1966 : }
    2807              : 
    2808          337 : void cloudsync_update_final (sqlite3_context *context) {
    2809          337 :     cloudsync_update_payload *payload = (cloudsync_update_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_update_payload));
    2810          337 :     if (!payload || payload->count == 0) return;
    2811              :     
    2812              :     // retrieve context
    2813          337 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2814          337 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2815              :     
    2816              :     // lookup table
    2817          337 :     const char *table_name = (const char *)sqlite3_value_text(payload->table_name);
    2818          337 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2819          337 :     if (!table) {
    2820            0 :         dbutils_context_result_error(context, "Unable to retrieve table name %s in cloudsync_update.", table_name);
    2821            0 :         return;
    2822              :     }
    2823              : 
    2824              :     // compute the next database version for tracking changes
    2825          337 :     sqlite3_int64 db_version = db_version_next(db, data, CLOUDSYNC_VALUE_NOTSET);
    2826          337 :     int rc = SQLITE_OK;
    2827              :     
    2828              :     // Check if the primary key(s) have changed
    2829          337 :     bool prikey_changed = false;
    2830          947 :     for (int i=0; i<table->npks; ++i) {
    2831          640 :         if (dbutils_value_compare(payload->old_values[i], payload->new_values[i]) != 0) {
    2832           30 :             prikey_changed = true;
    2833           30 :             break;
    2834              :         }
    2835          610 :     }
    2836              : 
    2837              :     // encode the NEW primary key values into a buffer (used later for indexing)
    2838              :     char buffer[1024];
    2839              :     char buffer2[1024];
    2840          337 :     size_t pklen = sizeof(buffer);
    2841          337 :     size_t oldpklen = sizeof(buffer2);
    2842          337 :     char *oldpk = NULL;
    2843              :     
    2844          337 :     char *pk = pk_encode_prikey(payload->new_values, table->npks, buffer, &pklen);
    2845          337 :     if (!pk) {
    2846            0 :         sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
    2847            0 :         return;
    2848              :     }
    2849              :     
    2850          337 :     if (prikey_changed) {
    2851              :         // if the primary key has changed, we need to handle the row differently:
    2852              :         // 1. mark the old row (OLD primary key) as deleted
    2853              :         // 2. create a new row (NEW primary key)
    2854              :         
    2855              :         // encode the OLD primary key into a buffer
    2856           30 :         oldpk = pk_encode_prikey(payload->old_values, table->npks, buffer2, &oldpklen);
    2857           30 :         if (!oldpk) {
    2858            0 :             if (pk != buffer) cloudsync_memory_free(pk);
    2859            0 :             sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
    2860            0 :             return;
    2861              :         }
    2862              :         
    2863              :         // mark the rows with the old primary key as deleted in the metadata (old row handling)
    2864           30 :         rc = local_mark_delete_meta(db, table, oldpk, oldpklen, db_version, BUMP_SEQ(data));
    2865           30 :         if (rc != SQLITE_OK) goto cleanup;
    2866              :         
    2867              :         // move non-sentinel metadata entries from OLD primary key to NEW primary key
    2868              :         // handles the case where some metadata is retained across primary key change
    2869              :         // see https://github.com/sqliteai/sqlite-sync/blob/main/docs/PriKey.md for more details
    2870           30 :         rc = local_update_move_meta(db, table, pk, pklen, oldpk, oldpklen, db_version);
    2871           30 :         if (rc != SQLITE_OK) goto cleanup;
    2872              :         
    2873              :         // mark a new sentinel row with the new primary key in the metadata
    2874           30 :         rc = local_mark_insert_sentinel_meta(db, table, pk, pklen, db_version, BUMP_SEQ(data));
    2875           30 :         if (rc != SQLITE_OK) goto cleanup;
    2876              :         
    2877              :         // free memory if the OLD primary key was dynamically allocated
    2878           30 :         if (oldpk != buffer2) cloudsync_memory_free(oldpk);
    2879           30 :         oldpk = NULL;
    2880           30 :     }
    2881              :     
    2882              :     // compare NEW and OLD values (excluding primary keys) to handle column updates
    2883         1634 :     for (int i=0; i<table->ncols; i++) {
    2884         1297 :         int col_index = table->npks + i;  // Regular columns start after primary keys
    2885              : 
    2886         1297 :         if (dbutils_value_compare(payload->old_values[col_index], payload->new_values[col_index]) != 0) {
    2887              :             // if a column value has changed, mark it as updated in the metadata
    2888              :             // columns are in cid order
    2889          609 :             rc = local_mark_insert_or_update_meta(db, table, pk, pklen, table->col_name[i], db_version, BUMP_SEQ(data));
    2890          609 :             if (rc != SQLITE_OK) goto cleanup;
    2891          609 :         }
    2892         1634 :     }
    2893              :     
    2894              : cleanup:
    2895          337 :     if (rc != SQLITE_OK) sqlite3_result_error(context, sqlite3_errmsg(db), -1);
    2896          337 :     if (pk != buffer) cloudsync_memory_free(pk);
    2897          337 :     if (oldpk && (oldpk != buffer2)) cloudsync_memory_free(oldpk);
    2898              :     
    2899          337 :     cloudsync_update_payload_free(payload);
    2900          337 : }
    2901              : 
    2902              : // MARK: -
    2903              : 
    2904            6 : int cloudsync_cleanup_internal (sqlite3_context *context, const char *table_name) {
    2905            6 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2906              :     
    2907              :     // get database reference
    2908            6 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2909              :     
    2910              :     // init cloudsync_settings
    2911            6 :     if (cloudsync_context_init(db, data, context) == NULL) return SQLITE_MISUSE;
    2912              :     
    2913            6 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2914            6 :     if (!table) return SQLITE_OK;
    2915              :     
    2916            6 :     table_remove_from_context(data, table);
    2917            6 :     table_free(table);
    2918              :         
    2919              :     // drop meta-table
    2920            6 :     char *sql = cloudsync_memory_mprintf("DROP TABLE IF EXISTS \"%w_cloudsync\";", table_name);
    2921            6 :     int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
    2922            6 :     cloudsync_memory_free(sql);
    2923            6 :     if (rc != SQLITE_OK) {
    2924            0 :         dbutils_context_result_error(context, "Unable to drop cloudsync table %s_cloudsync in cloudsync_cleanup.", table_name);
    2925            0 :         sqlite3_result_error_code(context, rc);
    2926            0 :         return rc;
    2927              :     }
    2928              :     
    2929              :     // drop original triggers
    2930            6 :     dbutils_delete_triggers(db, table_name);
    2931            6 :     if (rc != SQLITE_OK) {
    2932            0 :         dbutils_context_result_error(context, "Unable to drop cloudsync table %s_cloudsync in cloudsync_cleanup.", table_name);
    2933            0 :         sqlite3_result_error_code(context, rc);
    2934            0 :         return rc;
    2935              :     }
    2936              :     
    2937              :     // remove all table related settings
    2938            6 :     dbutils_table_settings_set_key_value(db, context, table_name, NULL, NULL, NULL);
    2939              :     
    2940            6 :     return SQLITE_OK;
    2941            6 : }
    2942              : 
    2943            1 : void cloudsync_cleanup_all (sqlite3_context *context) {
    2944            1 :     char *sql = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'cloudsync_%' AND name NOT LIKE '%_cloudsync';";
    2945              :     
    2946            1 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2947            1 :     char **result = NULL;
    2948              :     int nrows, ncols;
    2949              :     char *errmsg;
    2950            1 :     int rc = sqlite3_get_table(db, sql, &result, &nrows, &ncols, &errmsg);
    2951            1 :     if (errmsg || ncols != 1) {
    2952            0 :         printf("cloudsync_cleanup_all error: %s\n", errmsg ? errmsg : "invalid table");
    2953            0 :         goto cleanup;
    2954              :     }
    2955              :     
    2956            1 :     rc = SQLITE_OK;
    2957            6 :     for (int i = ncols; i < nrows+ncols; i+=ncols) {
    2958            5 :         int rc2 = cloudsync_cleanup_internal(context, result[i]);
    2959            5 :         if (rc2 != SQLITE_OK) rc = rc2;
    2960            5 :     }
    2961              :     
    2962            2 :     if (rc == SQLITE_OK) {
    2963            1 :         cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2964            1 :         data->site_id[0] = 0;
    2965            1 :         dbutils_settings_cleanup(db);
    2966            1 :     }
    2967              :     
    2968              : cleanup:
    2969            1 :     sqlite3_free_table(result);
    2970            1 :     sqlite3_free(errmsg);
    2971            1 : }
    2972              : 
    2973            2 : void cloudsync_cleanup (sqlite3_context *context, int argc, sqlite3_value **argv) {
    2974              :     DEBUG_FUNCTION("cloudsync_cleanup");
    2975              :     
    2976            2 :     const char *table = (const char *)sqlite3_value_text(argv[0]);
    2977            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2978            2 :     sqlite3 *db = sqlite3_context_db_handle(context);
    2979              :     
    2980            2 :     if (dbutils_is_star_table(table)) cloudsync_cleanup_all(context);
    2981            1 :     else cloudsync_cleanup_internal(context, table);
    2982              :     
    2983            2 :     if (dbutils_table_exists(db, CLOUDSYNC_TABLE_SETTINGS_NAME) == true) dbutils_update_schema_hash(db, &data->schema_hash);
    2984            2 : }
    2985              : 
    2986            2 : void cloudsync_enable_disable (sqlite3_context *context, const char *table_name, bool value) {
    2987              :     DEBUG_FUNCTION("cloudsync_enable_disable");
    2988              :     
    2989            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2990            2 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2991            2 :     if (!table) return;
    2992              :     
    2993            2 :     table->enabled = value;
    2994            2 : }
    2995              : 
    2996           28 : int cloudsync_enable_disable_all_callback (void *xdata, int ncols, char **values, char **names) {
    2997           28 :     sqlite3_context *context = (sqlite3_context *)xdata;
    2998           28 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    2999           28 :     bool value = data->temp_bool;
    3000              :     
    3001           56 :     for (int i=0; i<ncols; i++) {
    3002           28 :         const char *table_name = values[i];
    3003           28 :         cloudsync_table_context *table = table_lookup(data, table_name);
    3004           28 :         if (!table) continue;
    3005           10 :         table->enabled = value;
    3006           10 :     }
    3007              :     
    3008           28 :     return SQLITE_OK;
    3009              : }
    3010              : 
    3011            2 : void cloudsync_enable_disable_all (sqlite3_context *context, bool value) {
    3012              :     DEBUG_FUNCTION("cloudsync_enable_disable_all");
    3013              :     
    3014            2 :     char *sql = "SELECT name FROM sqlite_master WHERE type='table';";
    3015              :     
    3016            2 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    3017            2 :     data->temp_bool = value;
    3018            2 :     sqlite3 *db = sqlite3_context_db_handle(context);
    3019            2 :     sqlite3_exec(db, sql, cloudsync_enable_disable_all_callback, context, NULL);
    3020            2 : }
    3021              : 
    3022            2 : void cloudsync_enable (sqlite3_context *context, int argc, sqlite3_value **argv) {
    3023              :     DEBUG_FUNCTION("cloudsync_enable");
    3024              :     
    3025            2 :     const char *table = (const char *)sqlite3_value_text(argv[0]);
    3026            2 :     if (dbutils_is_star_table(table)) cloudsync_enable_disable_all(context, true);
    3027            1 :     else cloudsync_enable_disable(context, table, true);
    3028            2 : }
    3029              : 
    3030            2 : void cloudsync_disable (sqlite3_context *context, int argc, sqlite3_value **argv) {
    3031              :     DEBUG_FUNCTION("cloudsync_disable");
    3032              :     
    3033            2 :     const char *table = (const char *)sqlite3_value_text(argv[0]);
    3034            2 :     if (dbutils_is_star_table(table)) cloudsync_enable_disable_all(context, false);
    3035            1 :     else cloudsync_enable_disable(context, table, false);
    3036            2 : }
    3037              : 
    3038         2854 : void cloudsync_is_enabled (sqlite3_context *context, int argc, sqlite3_value **argv) {
    3039              :     DEBUG_FUNCTION("cloudsync_is_enabled");
    3040              :     
    3041         2854 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    3042         2854 :     const char *table_name = (const char *)sqlite3_value_text(argv[0]);
    3043         2854 :     cloudsync_table_context *table = table_lookup(data, table_name);
    3044              :     
    3045         2854 :     int result = (table && table->enabled) ? 1 : 0;
    3046         2854 :     sqlite3_result_int(context, result);
    3047         2854 : }
    3048              : 
    3049          111 : void cloudsync_terminate (sqlite3_context *context, int argc, sqlite3_value **argv) {
    3050              :     DEBUG_FUNCTION("cloudsync_terminate");
    3051              :     
    3052          111 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    3053              :     
    3054          251 :     for (int i=0; i<data->tables_count; ++i) {
    3055          140 :         if (data->tables[i]) table_free(data->tables[i]);
    3056          140 :         data->tables[i] = NULL;
    3057          140 :     }
    3058              :     
    3059          111 :     if (data->schema_version_stmt) sqlite3_finalize(data->schema_version_stmt);
    3060          111 :     if (data->data_version_stmt) sqlite3_finalize(data->data_version_stmt);
    3061          111 :     if (data->db_version_stmt) sqlite3_finalize(data->db_version_stmt);
    3062          111 :     if (data->getset_siteid_stmt) sqlite3_finalize(data->getset_siteid_stmt);
    3063              :     
    3064          111 :     data->schema_version_stmt = NULL;
    3065          111 :     data->data_version_stmt = NULL;
    3066          111 :     data->db_version_stmt = NULL;
    3067          111 :     data->getset_siteid_stmt = NULL;
    3068              :     
    3069              :     // reset the site_id so the cloudsync_context_init will be executed again
    3070              :     // if any other cloudsync function is called after terminate
    3071          111 :     data->site_id[0] = 0;
    3072              :     
    3073          111 :     sqlite3_result_int(context, 1);
    3074          111 : }
    3075              : 
    3076              : // MARK: -
    3077              : 
    3078          111 : int cloudsync_load_siteid (sqlite3 *db, cloudsync_context *data) {
    3079              :     // check if site_id was already loaded
    3080          111 :     if (data->site_id[0] != 0) return SQLITE_OK;
    3081              :     
    3082              :     // load site_id
    3083              :     int size, rc;
    3084          110 :     char *buffer = dbutils_blob_select(db, "SELECT site_id FROM cloudsync_site_id WHERE rowid=0;", &size, data->sqlite_ctx, &rc);
    3085          110 :     if (!buffer) return rc;
    3086          110 :     if (size != UUID_LEN) return SQLITE_MISUSE;
    3087              :     
    3088          110 :     memcpy(data->site_id, buffer, UUID_LEN);
    3089          110 :     cloudsync_memory_free(buffer);
    3090              :     
    3091          110 :     return SQLITE_OK;
    3092          111 : }
    3093              : 
    3094          170 : int cloudsync_init_internal (sqlite3_context *context, const char *table_name, const char *algo_name, bool skip_int_pk_check) {
    3095              :     DEBUG_FUNCTION("cloudsync_init_internal");
    3096              :     
    3097              :     // get database reference
    3098          170 :     sqlite3 *db = sqlite3_context_db_handle(context);
    3099              :     
    3100              :     // retrieve global context
    3101          170 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    3102              :     
    3103              :     // sanity check table and its primary key(s)
    3104          170 :     if (dbutils_table_sanity_check(db, context, table_name, skip_int_pk_check) == false) {
    3105            1 :         return SQLITE_MISUSE;
    3106              :     }
    3107              :     
    3108              :     // init cloudsync_settings
    3109          169 :     if (cloudsync_context_init(db, data, context) == NULL) return SQLITE_MISUSE;
    3110              :     
    3111              :     // sanity check algo name (if exists)
    3112          169 :     table_algo algo_new = table_algo_none;
    3113          169 :     if (!algo_name) {
    3114           17 :         algo_name = CLOUDSYNC_DEFAULT_ALGO;
    3115           17 :     }
    3116              :     
    3117          169 :     algo_new = crdt_algo_from_name(algo_name);
    3118          169 :     if (algo_new == table_algo_none) {
    3119            1 :         dbutils_context_result_error(context, "algo name %s does not exist", crdt_algo_name);
    3120            1 :         return SQLITE_MISUSE;
    3121              :     }
    3122              :     
    3123              :     // check if table name was already augmented
    3124          168 :     table_algo algo_current = dbutils_table_settings_get_algo(db, table_name);
    3125              :     
    3126              :     // sanity check algorithm
    3127          168 :     if ((algo_new == algo_current) && (algo_current != table_algo_none)) {
    3128              :         // if table algorithms and the same and not none, do nothing
    3129          168 :     } else if ((algo_new == table_algo_none) && (algo_current == table_algo_none)) {
    3130              :         // nothing is written into settings because the default table_algo_crdt_cls will be used
    3131            0 :         algo_new = algo_current = table_algo_crdt_cls;
    3132          139 :     } else if ((algo_new == table_algo_none) && (algo_current != table_algo_none)) {
    3133              :         // algo is already written into settins so just use it
    3134            0 :         algo_new = algo_current;
    3135          139 :     } else if ((algo_new != table_algo_none) && (algo_current == table_algo_none)) {
    3136              :         // write table algo name in settings
    3137          139 :         dbutils_table_settings_set_key_value(NULL, context, table_name, "*", "algo", algo_name);
    3138          139 :     } else {
    3139              :         // error condition
    3140            0 :         dbutils_context_result_error(context, "%s", "Before changing a table algorithm you must call cloudsync_cleanup(table_name)");
    3141            0 :         return SQLITE_MISUSE;
    3142              :     }
    3143              :     
    3144              :     // Run the following function even if table was already augmented.
    3145              :     // It is safe to call the following function multiple times, if there is nothing to update nothing will be changed.
    3146              :     // After an alter table, in contrast, all the cloudsync triggers, tables and stmts must be recreated.
    3147              :     
    3148              :     // sync algo with table (unused in this version)
    3149              :     // cloudsync_sync_table_key(data, table_name, "*", CLOUDSYNC_KEY_ALGO, crdt_algo_name(algo_new));
    3150              :     
    3151              :     // check triggers
    3152          168 :     int rc = dbutils_check_triggers(db, table_name, algo_new);
    3153          168 :     if (rc != SQLITE_OK) {
    3154            0 :         dbutils_context_result_error(context, "An error occurred while creating triggers: %s (%d)", sqlite3_errmsg(db), rc);
    3155            0 :         return SQLITE_MISUSE;
    3156              :     }
    3157              :     
    3158              :     // check meta-table
    3159          168 :     rc = dbutils_check_metatable(db, table_name, algo_new);
    3160          168 :     if (rc != SQLITE_OK) {
    3161            0 :         dbutils_context_result_error(context, "An error occurred while creating metatable: %s (%d)", sqlite3_errmsg(db), rc);
    3162            0 :         return SQLITE_MISUSE;
    3163              :     }
    3164              :     
    3165              :     // add prepared statements
    3166          168 :     if (stmts_add_tocontext(db, data) != SQLITE_OK) {
    3167            0 :         dbutils_context_result_error(context, "%s", "An error occurred while trying to compile prepared SQL statements.");
    3168            0 :         return SQLITE_MISUSE;
    3169              :     }
    3170              :     
    3171              :     // add table to in-memory data context
    3172          168 :     if (table_add_to_context(db, data, algo_new, table_name) == false) {
    3173            0 :         dbutils_context_result_error(context, "An error occurred while adding %s table information to global context", table_name);
    3174            0 :         return SQLITE_MISUSE;
    3175              :     }
    3176              :     
    3177          168 :     if (cloudsync_refill_metatable(db, data, table_name) != SQLITE_OK) {
    3178            0 :         dbutils_context_result_error(context, "%s", "An error occurred while trying to fill the augmented table.");
    3179            0 :         return SQLITE_MISUSE;
    3180              :     }
    3181              :         
    3182          168 :     return SQLITE_OK;
    3183          170 : }
    3184              : 
    3185            1 : int cloudsync_init_all (sqlite3_context *context, const char *algo_name, bool skip_int_pk_check) {
    3186              :     char sql[1024];
    3187            1 :     snprintf(sql, sizeof(sql), "SELECT name, '%s' FROM sqlite_master WHERE type='table' and name NOT LIKE 'sqlite_%%' AND name NOT LIKE 'cloudsync_%%' AND name NOT LIKE '%%_cloudsync';", (algo_name) ? algo_name : CLOUDSYNC_DEFAULT_ALGO);
    3188              :     
    3189            1 :     sqlite3 *db = sqlite3_context_db_handle(context);
    3190            1 :     sqlite3_stmt *vm = NULL;
    3191            1 :     int rc = sqlite3_prepare_v2(db, sql, -1, &vm, NULL);
    3192            1 :     if (rc != SQLITE_OK) goto abort_init_all;
    3193              :     
    3194            6 :     while (1) {
    3195            6 :         rc = sqlite3_step(vm);
    3196            6 :         if (rc == SQLITE_DONE) break;
    3197            5 :         else if (rc != SQLITE_ROW) goto abort_init_all;
    3198              :         
    3199            5 :         const char *table = (const char *)sqlite3_column_text(vm, 0);
    3200            5 :         const char *algo = (const char *)sqlite3_column_text(vm, 1);
    3201            5 :         rc = cloudsync_init_internal(context, table, algo, skip_int_pk_check);
    3202            5 :         if (rc != SQLITE_OK) {cloudsync_cleanup_internal(context, table); goto abort_init_all;}
    3203              :     }
    3204            1 :     rc = SQLITE_OK;
    3205              :      
    3206              : abort_init_all:
    3207            1 :     if (vm) sqlite3_finalize(vm);
    3208            1 :     return rc;
    3209              : }
    3210              : 
    3211          143 : void cloudsync_init (sqlite3_context *context, const char *table, const char *algo, bool skip_int_pk_check) {
    3212          143 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    3213          143 :     data->sqlite_ctx = context;
    3214              :     
    3215          143 :     sqlite3 *db = sqlite3_context_db_handle(context);
    3216          143 :     int rc = sqlite3_exec(db, "SAVEPOINT cloudsync_init;", NULL, NULL, NULL);
    3217          143 :     if (rc != SQLITE_OK) {
    3218            0 :         dbutils_context_result_error(context, "Unable to create cloudsync_init savepoint. %s", sqlite3_errmsg(db));
    3219            0 :         sqlite3_result_error_code(context, rc);
    3220            0 :         return;
    3221              :     }
    3222              :     
    3223          143 :     if (dbutils_is_star_table(table)) rc = cloudsync_init_all(context, algo, skip_int_pk_check);
    3224          142 :     else rc = cloudsync_init_internal(context, table, algo, skip_int_pk_check);
    3225              :     
    3226          143 :     if (rc == SQLITE_OK) {
    3227          141 :         rc = sqlite3_exec(db, "RELEASE cloudsync_init", NULL, NULL, NULL);
    3228          141 :         if (rc != SQLITE_OK) {
    3229            0 :             dbutils_context_result_error(context, "Unable to release cloudsync_init savepoint. %s", sqlite3_errmsg(db));
    3230            0 :             sqlite3_result_error_code(context, rc);
    3231            0 :         }
    3232          141 :     }
    3233              :     
    3234              :     // in case of error, rollback transaction
    3235          143 :     if (rc != SQLITE_OK) {
    3236            2 :         sqlite3_exec(db, "ROLLBACK TO cloudsync_init; RELEASE cloudsync_init", NULL, NULL, NULL);
    3237            2 :         return;
    3238              :     }
    3239              :     
    3240          141 :     dbutils_update_schema_hash(db, &data->schema_hash);
    3241              :     
    3242              :     // returns site_id as TEXT
    3243              :     char buffer[UUID_STR_MAXLEN];
    3244          141 :     cloudsync_uuid_v7_stringify(data->site_id, buffer, false);
    3245          141 :     sqlite3_result_text(context, buffer, -1, NULL);
    3246          143 : }
    3247              : 
    3248           41 : void cloudsync_init3 (sqlite3_context *context, int argc, sqlite3_value **argv) {
    3249              :     DEBUG_FUNCTION("cloudsync_init2");
    3250              :     
    3251           41 :     const char *table = (const char *)sqlite3_value_text(argv[0]);
    3252           41 :     const char *algo = (const char *)sqlite3_value_text(argv[1]);
    3253           41 :     bool skip_int_pk_check = (bool)sqlite3_value_int(argv[2]);
    3254              : 
    3255           41 :     cloudsync_init(context, table, algo, skip_int_pk_check);
    3256           41 : }
    3257              : 
    3258           83 : void cloudsync_init2 (sqlite3_context *context, int argc, sqlite3_value **argv) {
    3259              :     DEBUG_FUNCTION("cloudsync_init2");
    3260              :     
    3261           83 :     const char *table = (const char *)sqlite3_value_text(argv[0]);
    3262           83 :     const char *algo = (const char *)sqlite3_value_text(argv[1]);
    3263              :     
    3264           83 :     cloudsync_init(context, table, algo, false);
    3265           83 : }
    3266              : 
    3267           19 : void cloudsync_init1 (sqlite3_context *context, int argc, sqlite3_value **argv) {
    3268              :     DEBUG_FUNCTION("cloudsync_init1");
    3269              :     
    3270           19 :     const char *table = (const char *)sqlite3_value_text(argv[0]);
    3271              :     
    3272           19 :     cloudsync_init(context, table, NULL, false);
    3273           19 : }
    3274              : 
    3275              : // MARK: -
    3276              : 
    3277           24 : void cloudsync_begin_alter (sqlite3_context *context, int argc, sqlite3_value **argv) {
    3278              :     DEBUG_FUNCTION("cloudsync_begin_alter");
    3279           24 :     char *errmsg = NULL;
    3280           24 :     char **result = NULL;
    3281              :     
    3282           24 :     const char *table_name = (const char *)sqlite3_value_text(argv[0]);
    3283              :     
    3284              :     // get database reference
    3285           24 :     sqlite3 *db = sqlite3_context_db_handle(context);
    3286              :     
    3287              :     // retrieve global context
    3288           24 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    3289              :     
    3290              :     // init cloudsync_settings
    3291           24 :     if (cloudsync_context_init(db, data, context) == NULL) {
    3292            0 :         sqlite3_result_error(context, "Unable to init the cloudsync context.", -1);
    3293            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    3294            0 :         return;
    3295              :     }
    3296              :     
    3297           24 :     cloudsync_table_context *table = table_lookup(data, table_name);
    3298           24 :     if (!table) {
    3299            1 :         dbutils_context_result_error(context, "Unable to find table %s", table_name);
    3300            1 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    3301            1 :         return;
    3302              :     }
    3303              : 
    3304           23 :     if (table->is_altering) return;
    3305              : 
    3306              :     // create a savepoint to manage the alter operations as a transaction
    3307           23 :     int rc = sqlite3_exec(db, "SAVEPOINT cloudsync_alter", NULL, NULL, NULL);
    3308           23 :     if (rc != SQLITE_OK) {
    3309            0 :         sqlite3_result_error(context, "Unable to create cloudsync_alter savepoint.", -1);
    3310            0 :         sqlite3_result_error_code(context, rc);
    3311            0 :         goto rollback_begin_alter;
    3312              :     }
    3313              : 
    3314              :     int nrows, ncols;
    3315           23 :     char *sql = cloudsync_memory_mprintf("SELECT name FROM pragma_table_info('%q') WHERE pk>0 ORDER BY pk;", table_name);
    3316           23 :     rc = sqlite3_get_table(db, sql, &result, &nrows, &ncols, &errmsg);
    3317           23 :     cloudsync_memory_free(sql);
    3318           23 :     if (errmsg || ncols != 1 || nrows != table->npks) {
    3319            0 :         dbutils_context_result_error(context, "Unable to get primary keys for table %s (%s)", table_name, errmsg);
    3320            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    3321            0 :         goto rollback_begin_alter;
    3322              :     }
    3323              :     
    3324              :     // drop original triggers
    3325           23 :     dbutils_delete_triggers(db, table_name);
    3326           23 :     if (rc != SQLITE_OK) {
    3327            0 :         dbutils_context_result_error(context, "Unable to delete triggers for table %s in cloudsync_begin_alter.", table_name);
    3328            0 :         sqlite3_result_error_code(context, rc);
    3329            0 :         goto rollback_begin_alter;
    3330              :     }
    3331              :     
    3332           23 :     if (table->pk_name) sqlite3_free_table(table->pk_name);
    3333           23 :     table->pk_name = result;
    3334           23 :     table->is_altering = true;
    3335           23 :     return;
    3336              :     
    3337              : rollback_begin_alter:
    3338            0 :     sqlite3_exec(db, "ROLLBACK TO cloudsync_alter; RELEASE cloudsync_alter;", NULL, NULL, NULL);
    3339              : 
    3340              : cleanup_begin_alter:
    3341            0 :     sqlite3_free_table(result);
    3342            0 :     sqlite3_free(errmsg);
    3343           24 : }
    3344              : 
    3345           24 : void cloudsync_commit_alter (sqlite3_context *context, int argc, sqlite3_value **argv) {
    3346              :     DEBUG_FUNCTION("cloudsync_commit_alter");
    3347              :     
    3348           24 :     const char *table_name = (const char *)sqlite3_value_text(argv[0]);
    3349           24 :     cloudsync_table_context *table = NULL;
    3350              :     
    3351              :     // get database reference
    3352           24 :     sqlite3 *db = sqlite3_context_db_handle(context);
    3353              :     
    3354              :     // retrieve global context
    3355           24 :     cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
    3356              :     
    3357              :     // init cloudsync_settings
    3358           24 :     if (cloudsync_context_init(db, data, context) == NULL) {
    3359            0 :         dbutils_context_result_error(context, "Unable to init the cloudsync context.");
    3360            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    3361            0 :         goto rollback_finalize_alter;
    3362              :     }
    3363              :     
    3364           24 :     table = table_lookup(data, table_name);
    3365           24 :     if (!table) {
    3366            1 :         dbutils_context_result_error(context, "Unable to find table context.");
    3367            1 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    3368            1 :         goto rollback_finalize_alter;
    3369              :     }
    3370              : 
    3371           23 :     if (!table->is_altering) return;
    3372              : 
    3373           23 :     if (!table->pk_name) {
    3374            0 :         dbutils_context_result_error(context, "Unable to find table context.");
    3375            0 :         sqlite3_result_error_code(context, SQLITE_MISUSE);
    3376            0 :         goto rollback_finalize_alter;
    3377              :     }
    3378              : 
    3379           23 :     int rc = cloudsync_finalize_alter(context, data, table);
    3380           23 :     if (rc != SQLITE_OK) goto rollback_finalize_alter;
    3381              :     
    3382              :     // the table is outdated, delete it and it will be reloaded in the cloudsync_init_internal
    3383           23 :     table_remove(data, table_name);
    3384           23 :     table_free(table);
    3385           23 :     table = NULL;
    3386              :         
    3387              :     // init again cloudsync for the table
    3388           23 :     table_algo algo_current = dbutils_table_settings_get_algo(db, table_name);
    3389           23 :     if (algo_current == table_algo_none) algo_current = dbutils_table_settings_get_algo(db, "*");
    3390           23 :     rc = cloudsync_init_internal(context, table_name, crdt_algo_name(algo_current), true);
    3391           23 :     if (rc != SQLITE_OK) goto rollback_finalize_alter;
    3392              : 
    3393              :     // release savepoint
    3394           23 :     rc = sqlite3_exec(db, "RELEASE cloudsync_alter", NULL, NULL, NULL);
    3395           23 :     if (rc != SQLITE_OK) {
    3396            0 :         dbutils_context_result_error(context, sqlite3_errmsg(db));
    3397            0 :         sqlite3_result_error_code(context, rc);
    3398            0 :         goto rollback_finalize_alter;
    3399              :     }
    3400              :     
    3401           23 :     dbutils_update_schema_hash(db, &data->schema_hash);
    3402              : 
    3403              :     // flag is reset here; note that table_remove + table_free destroyed the old table,
    3404              :     // and cloudsync_init_internal created a fresh one with is_altering = false (zero-alloc).
    3405              : 
    3406           23 :     return;
    3407              : 
    3408              : rollback_finalize_alter:
    3409            1 :     sqlite3_exec(db, "ROLLBACK TO cloudsync_alter; RELEASE cloudsync_alter;", NULL, NULL, NULL);
    3410            1 :     if (table) {
    3411            0 :         sqlite3_free_table(table->pk_name);
    3412            0 :         table->pk_name = NULL;
    3413            0 :         table->is_altering = false;
    3414            0 :     }
    3415           24 : }
    3416              :     
    3417              : // MARK: - Main Entrypoint -
    3418              : 
    3419          111 : int cloudsync_register (sqlite3 *db, char **pzErrMsg) {
    3420          111 :     int rc = SQLITE_OK;
    3421              :     
    3422              :     // there's no built-in way to verify if sqlite3_cloudsync_init has already been called
    3423              :     // for this specific database connection, we use a workaround: we attempt to retrieve the
    3424              :     // cloudsync_version and check for an error, an error indicates that initialization has not been performed
    3425          111 :     if (sqlite3_exec(db, "SELECT cloudsync_version();", NULL, NULL, NULL) == SQLITE_OK) return SQLITE_OK;
    3426              :     
    3427              :     // init memory debugger (NOOP in production)
    3428              :     cloudsync_memory_init(1);
    3429              :     
    3430              :     // init context
    3431          109 :     void *ctx = cloudsync_context_create();
    3432          109 :     if (!ctx) {
    3433            0 :         if (pzErrMsg) *pzErrMsg = "Not enought memory to create a database context";
    3434            0 :         return SQLITE_NOMEM;
    3435              :     }
    3436              :     
    3437              :     // register functions
    3438              :     
    3439              :     // PUBLIC functions
    3440          109 :     rc = dbutils_register_function(db, "cloudsync_version", cloudsync_version, 0, pzErrMsg, ctx, cloudsync_context_free);
    3441          109 :     if (rc != SQLITE_OK) return rc;
    3442              :     
    3443          109 :     rc = dbutils_register_function(db, "cloudsync_init", cloudsync_init1, 1, pzErrMsg, ctx, NULL);
    3444          109 :     if (rc != SQLITE_OK) return rc;
    3445              :     
    3446          109 :     rc = dbutils_register_function(db, "cloudsync_init", cloudsync_init2, 2, pzErrMsg, ctx, NULL);
    3447          109 :     if (rc != SQLITE_OK) return rc;
    3448              :     
    3449          109 :     rc = dbutils_register_function(db, "cloudsync_init", cloudsync_init3, 3, pzErrMsg, ctx, NULL);
    3450          109 :     if (rc != SQLITE_OK) return rc;
    3451              : 
    3452              :     
    3453          109 :     rc = dbutils_register_function(db, "cloudsync_enable", cloudsync_enable, 1, pzErrMsg, ctx, NULL);
    3454          109 :     if (rc != SQLITE_OK) return rc;
    3455              :     
    3456          109 :     rc = dbutils_register_function(db, "cloudsync_disable", cloudsync_disable, 1, pzErrMsg, ctx, NULL);
    3457          109 :     if (rc != SQLITE_OK) return rc;
    3458              :     
    3459          109 :     rc = dbutils_register_function(db, "cloudsync_is_enabled", cloudsync_is_enabled, 1, pzErrMsg, ctx, NULL);
    3460          109 :     if (rc != SQLITE_OK) return rc;
    3461              :     
    3462          109 :     rc = dbutils_register_function(db, "cloudsync_cleanup", cloudsync_cleanup, 1, pzErrMsg, ctx, NULL);
    3463          109 :     if (rc != SQLITE_OK) return rc;
    3464              :     
    3465          109 :     rc = dbutils_register_function(db, "cloudsync_terminate", cloudsync_terminate, 0, pzErrMsg, ctx, NULL);
    3466          109 :     if (rc != SQLITE_OK) return rc;
    3467              :     
    3468          109 :     rc = dbutils_register_function(db, "cloudsync_set", cloudsync_set, 2, pzErrMsg, ctx, NULL);
    3469          109 :     if (rc != SQLITE_OK) return rc;
    3470              :     
    3471          109 :     rc = dbutils_register_function(db, "cloudsync_set_table", cloudsync_set_table, 3, pzErrMsg, ctx, NULL);
    3472          109 :     if (rc != SQLITE_OK) return rc;
    3473              :     
    3474          109 :     rc = dbutils_register_function(db, "cloudsync_set_column", cloudsync_set_column, 4, pzErrMsg, ctx, NULL);
    3475          109 :     if (rc != SQLITE_OK) return rc;
    3476              :     
    3477          109 :     rc = dbutils_register_function(db, "cloudsync_siteid", cloudsync_siteid, 0, pzErrMsg, ctx, NULL);
    3478          109 :     if (rc != SQLITE_OK) return rc;
    3479              :     
    3480          109 :     rc = dbutils_register_function(db, "cloudsync_db_version", cloudsync_db_version, 0, pzErrMsg, ctx, NULL);
    3481          109 :     if (rc != SQLITE_OK) return rc;
    3482              :     
    3483          109 :     rc = dbutils_register_function(db, "cloudsync_db_version_next", cloudsync_db_version_next, 0, pzErrMsg, ctx, NULL);
    3484          109 :     if (rc != SQLITE_OK) return rc;
    3485              :     
    3486          109 :     rc = dbutils_register_function(db, "cloudsync_db_version_next", cloudsync_db_version_next, 1, pzErrMsg, ctx, NULL);
    3487          109 :     if (rc != SQLITE_OK) return rc;
    3488              :     
    3489          109 :     rc = dbutils_register_function(db, "cloudsync_begin_alter", cloudsync_begin_alter, 1, pzErrMsg, ctx, NULL);
    3490          109 :     if (rc != SQLITE_OK) return rc;
    3491              :     
    3492          109 :     rc = dbutils_register_function(db, "cloudsync_commit_alter", cloudsync_commit_alter, 1, pzErrMsg, ctx, NULL);
    3493          109 :     if (rc != SQLITE_OK) return rc;
    3494              :     
    3495          109 :     rc = dbutils_register_function(db, "cloudsync_uuid", cloudsync_uuid, 0, pzErrMsg, ctx, NULL);
    3496          109 :     if (rc != SQLITE_OK) return rc;
    3497              :     
    3498              :     // PAYLOAD
    3499          109 :     rc = dbutils_register_aggregate(db, "cloudsync_payload_encode", cloudsync_payload_encode_step, cloudsync_payload_encode_final, -1, pzErrMsg, ctx, NULL);
    3500          109 :     if (rc != SQLITE_OK) return rc;
    3501              :     
    3502          109 :     rc = dbutils_register_function(db, "cloudsync_payload_decode", cloudsync_payload_decode, -1, pzErrMsg, ctx, NULL);
    3503          109 :     if (rc != SQLITE_OK) return rc;
    3504              :     
    3505              :     #ifdef CLOUDSYNC_DESKTOP_OS
    3506          109 :     rc = dbutils_register_function(db, "cloudsync_payload_save", cloudsync_payload_save, 1, pzErrMsg, ctx, NULL);
    3507          109 :     if (rc != SQLITE_OK) return rc;
    3508              :     
    3509          109 :     rc = dbutils_register_function(db, "cloudsync_payload_load", cloudsync_payload_load, 1, pzErrMsg, ctx, NULL);
    3510          109 :     if (rc != SQLITE_OK) return rc;
    3511              :     #endif
    3512              :     
    3513              :     // PRIVATE functions
    3514          109 :     rc = dbutils_register_function(db, "cloudsync_is_sync", cloudsync_is_sync, 1, pzErrMsg, ctx, NULL);
    3515          109 :     if (rc != SQLITE_OK) return rc;
    3516              :     
    3517          109 :     rc = dbutils_register_function(db, "cloudsync_insert", cloudsync_insert, -1, pzErrMsg, ctx, NULL);
    3518          109 :     if (rc != SQLITE_OK) return rc;
    3519              :     
    3520          109 :     rc = dbutils_register_aggregate(db, "cloudsync_update", cloudsync_update_step, cloudsync_update_final, 3, pzErrMsg, ctx, NULL);
    3521          109 :     if (rc != SQLITE_OK) return rc;
    3522              :     
    3523          109 :     rc = dbutils_register_function(db, "cloudsync_delete", cloudsync_delete, -1, pzErrMsg, ctx, NULL);
    3524          109 :     if (rc != SQLITE_OK) return rc;
    3525              :     
    3526          109 :     rc = dbutils_register_function(db, "cloudsync_col_value", cloudsync_col_value, 3, pzErrMsg, ctx, NULL);
    3527          109 :     if (rc != SQLITE_OK) return rc;
    3528              :     
    3529          109 :     rc = dbutils_register_function(db, "cloudsync_pk_encode", cloudsync_pk_encode, -1, pzErrMsg, ctx, NULL);
    3530          109 :     if (rc != SQLITE_OK) return rc;
    3531              :     
    3532          109 :     rc = dbutils_register_function(db, "cloudsync_pk_decode", cloudsync_pk_decode, 2, pzErrMsg, ctx, NULL);
    3533          109 :     if (rc != SQLITE_OK) return rc;
    3534              :     
    3535          109 :     rc = dbutils_register_function(db, "cloudsync_seq", cloudsync_seq, 0, pzErrMsg, ctx, NULL);
    3536          109 :     if (rc != SQLITE_OK) return rc;
    3537              : 
    3538              :     // NETWORK LAYER
    3539              :     #ifndef CLOUDSYNC_OMIT_NETWORK
    3540              :     rc = cloudsync_network_register(db, pzErrMsg, ctx);
    3541              :     if (rc != SQLITE_OK) return rc;
    3542              :     #endif
    3543              :     
    3544          109 :     cloudsync_context *data = (cloudsync_context *)ctx;
    3545          109 :     sqlite3_commit_hook(db, cloudsync_commit_hook, ctx);
    3546          109 :     sqlite3_rollback_hook(db, cloudsync_rollback_hook, ctx);
    3547              :     
    3548              :     // register eponymous only changes virtual table
    3549          109 :     rc = cloudsync_vtab_register_changes (db, data);
    3550          109 :     if (rc != SQLITE_OK) return rc;
    3551              :     
    3552              :     // load config, if exists
    3553          109 :     if (cloudsync_config_exists(db)) {
    3554            0 :         cloudsync_context_init(db, ctx, NULL);
    3555              :         
    3556              :         // make sure to update internal version to current version
    3557            0 :         dbutils_settings_set_key_value(db, NULL, CLOUDSYNC_KEY_LIBVERSION, CLOUDSYNC_VERSION);
    3558            0 :     }
    3559              :     
    3560          109 :     return SQLITE_OK;
    3561          111 : }
    3562              : 
    3563          111 : APIEXPORT int sqlite3_cloudsync_init (sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
    3564              :     DEBUG_FUNCTION("sqlite3_cloudsync_init");
    3565              :     
    3566              :     #ifndef SQLITE_CORE
    3567              :     SQLITE_EXTENSION_INIT2(pApi);
    3568              :     #endif
    3569              :     
    3570          111 :     return cloudsync_register(db, pzErrMsg);
    3571              : }
        

Generated by: LCOV version 2.4-0