LCOV - code coverage report
Current view: top level - src - cloudsync.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 88.1 % 2745 2417
Test Date: 2026-07-17 12:57:54 Functions: 91.6 % 154 141

            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              : #include <time.h>
      19              : 
      20              : #include "cloudsync.h"
      21              : #include "lz4.h"
      22              : #include "pk.h"
      23              : #include "sql.h"
      24              : #include "utils.h"
      25              : #include "dbutils.h"
      26              : #include "block.h"
      27              : 
      28              : #ifdef _WIN32
      29              : #include <winsock2.h>
      30              : #include <ws2tcpip.h>
      31              : #else
      32              : #include <arpa/inet.h>                          // for htonl, htons, ntohl, ntohs
      33              : #include <netinet/in.h>                         // for struct sockaddr_in, INADDR_ANY, etc. (if needed)
      34              : #endif
      35              : 
      36              : #ifndef htonll
      37              : #if __BIG_ENDIAN__
      38              : #define htonll(x)                               (x)
      39              : #define ntohll(x)                               (x)
      40              : #else
      41              : #ifndef htobe64
      42              : #define htonll(x)                               ((uint64_t)htonl((x) & 0xFFFFFFFF) << 32 | (uint64_t)htonl((x) >> 32))
      43              : #define ntohll(x)                               ((uint64_t)ntohl((x) & 0xFFFFFFFF) << 32 | (uint64_t)ntohl((x) >> 32))
      44              : #else
      45              : #define htonll(x)                               htobe64(x)
      46              : #define ntohll(x)                               be64toh(x)
      47              : #endif
      48              : #endif
      49              : #endif
      50              : 
      51              : #define CLOUDSYNC_INIT_NTABLES                  64
      52              : #define CLOUDSYNC_MIN_DB_VERSION                0
      53              : 
      54              : #define CLOUDSYNC_PAYLOAD_MINBUF_SIZE                   (512*1024)
      55              : #define CLOUDSYNC_PAYLOAD_SIGNATURE                     0x434C5359  /* 'C','L','S','Y' */
      56              : #define CLOUDSYNC_PAYLOAD_VERSION_ORIGNAL               1
      57              : #define CLOUDSYNC_PAYLOAD_VERSION_1                     CLOUDSYNC_PAYLOAD_VERSION_ORIGNAL
      58              : #define CLOUDSYNC_PAYLOAD_VERSION_2                     2
      59              : #define CLOUDSYNC_PAYLOAD_VERSION_3                     3
      60              : #define CLOUDSYNC_PAYLOAD_VERSION_LATEST                CLOUDSYNC_PAYLOAD_VERSION_2
      61              : #define CLOUDSYNC_PAYLOAD_MIN_VERSION_WITH_CHECKSUM     CLOUDSYNC_PAYLOAD_VERSION_2
      62              : #define CLOUDSYNC_PAYLOAD_FRAGMENT_PREFIX               "__cloudsync_frag_v1__:"
      63              : #define CLOUDSYNC_PAYLOAD_FRAGMENT_STALE_SECONDS        (24*60*60)
      64              : #define CLOUDSYNC_PAYLOAD_FRAGMENT_CLEANUP_MIN_INTERVAL (60)
      65              : 
      66              : #ifndef MAX
      67              : #define MAX(a, b)                               (((a)>(b))?(a):(b))
      68              : #endif
      69              : 
      70              : #define DEBUG_DBERROR(_rc, _fn, _data)   do {if (_rc != DBRES_OK) printf("Error in %s: %s\n", _fn, database_errmsg(_data));} while (0)
      71              : 
      72              : typedef enum {
      73              :     CLOUDSYNC_PK_INDEX_TBL          = 0,
      74              :     CLOUDSYNC_PK_INDEX_PK           = 1,
      75              :     CLOUDSYNC_PK_INDEX_COLNAME      = 2,
      76              :     CLOUDSYNC_PK_INDEX_COLVALUE     = 3,
      77              :     CLOUDSYNC_PK_INDEX_COLVERSION   = 4,
      78              :     CLOUDSYNC_PK_INDEX_DBVERSION    = 5,
      79              :     CLOUDSYNC_PK_INDEX_SITEID       = 6,
      80              :     CLOUDSYNC_PK_INDEX_CL           = 7,
      81              :     CLOUDSYNC_PK_INDEX_SEQ          = 8
      82              : } CLOUDSYNC_PK_INDEX;
      83              : 
      84              : typedef enum {
      85              :     DBVM_VALUE_ERROR      = -1,
      86              :     DBVM_VALUE_UNCHANGED  = 0,
      87              :     DBVM_VALUE_CHANGED    = 1,
      88              : } DBVM_VALUE;
      89              : 
      90              : #define SYNCBIT_SET(_data)                  _data->insync = 1
      91              : #define SYNCBIT_RESET(_data)                _data->insync = 0
      92              : 
      93              : // MARK: - Deferred column-batch merge -
      94              : 
      95              : typedef struct {
      96              :     const char  *col_name;       // pointer into table_context->col_name[idx] (stable)
      97              :     dbvalue_t   *col_value;      // duplicated via database_value_dup (owned)
      98              :     int64_t     col_version;
      99              :     int64_t     db_version;
     100              :     uint8_t     site_id[UUID_LEN];
     101              :     int         site_id_len;
     102              :     int64_t     seq;
     103              : } merge_pending_entry;
     104              : 
     105              : typedef struct {
     106              :     cloudsync_table_context *table;
     107              :     char        *pk;             // malloc'd copy, freed on flush
     108              :     int         pk_len;
     109              :     int64_t     cl;
     110              :     bool        sentinel_pending;
     111              :     bool        row_exists;       // true when the PK already exists locally
     112              :     int         count;
     113              :     int         capacity;
     114              :     merge_pending_entry *entries;
     115              : 
     116              :     // Statement cache — reuse the prepared statement when the column
     117              :     // combination and row_exists flag match between consecutive PK flushes.
     118              :     dbvm_t      *cached_vm;
     119              :     bool        cached_row_exists;
     120              :     int         cached_col_count;
     121              :     const char  **cached_col_names; // array of pointers into table_context (not owned)
     122              : } merge_pending_batch;
     123              : 
     124              : // MARK: -
     125              : 
     126              : struct cloudsync_pk_decode_bind_context {
     127              :     dbvm_t      *vm;
     128              :     char        *tbl;
     129              :     int64_t     tbl_len;
     130              :     const void  *pk;
     131              :     int64_t     pk_len;
     132              :     char        *col_name;
     133              :     int64_t     col_name_len;
     134              :     int64_t     col_version;
     135              :     int64_t     db_version;
     136              :     const void  *site_id;
     137              :     int64_t     site_id_len;
     138              :     int64_t     cl;
     139              :     int64_t     seq;
     140              : };
     141              : 
     142              : struct cloudsync_context {
     143              :     void        *db;
     144              :     char        errmsg[1024];
     145              :     int         errcode;
     146              :     
     147              :     char        *libversion;
     148              :     uint8_t     site_id[UUID_LEN];
     149              :     int         insync;
     150              :     int         debug;
     151              :     bool        merge_equal_values;
     152              :     void        *aux_data;
     153              :     
     154              :     // stmts and context values
     155              :     dbvm_t      *schema_version_stmt;
     156              :     dbvm_t      *data_version_stmt;
     157              :     dbvm_t      *db_version_stmt;
     158              :     dbvm_t      *getset_siteid_stmt;
     159              :     int         data_version;
     160              :     int         schema_version;
     161              :     uint64_t    schema_hash;
     162              :     
     163              :     // set at transaction start and reset on commit/rollback
     164              :     int64_t    db_version;
     165              :     // version the DB would have if the transaction committed now
     166              :     int64_t    pending_db_version;
     167              :     // used to set an order inside each transaction
     168              :     int        seq;
     169              : 
     170              :     // wall-clock (time()) of the last stale v3-fragment GC; throttles the GC so
     171              :     // it does not run a full table scan on every applied fragment (0 = never run)
     172              :     int64_t    last_fragment_cleanup;
     173              : 
     174              :     // optional schema_name to be set in the cloudsync_table_context
     175              :     char       *current_schema;
     176              :     
     177              :     // augmented tables are stored in-memory so we do not need to retrieve information about
     178              :     // col_names and cid from the disk each time a write statement is performed
     179              :     // we do also not need to use an hash map here because for few tables the direct
     180              :     // in-memory comparison with table name is faster
     181              :     cloudsync_table_context **tables;   // dense vector: [0..tables_count-1] are valid
     182              :     int tables_count;                   // size
     183              :     int tables_cap;                     // capacity
     184              : 
     185              :     int skip_decode_idx;                // -1 in sqlite, col_value index in postgresql
     186              : 
     187              :     // deferred column-batch merge (active during payload_apply)
     188              :     merge_pending_batch *pending_batch;
     189              : 
     190              :     // last (db_version, seq) successfully applied during the current
     191              :     // cloudsync_payload_apply call; used to resolve the
     192              :     // CLOUDSYNC_CHECKPOINT_LAST_APPLIED receive-checkpoint mode (-1 = none yet).
     193              :     int64_t    apply_last_db_version;
     194              :     int64_t    apply_last_seq;
     195              : };
     196              : 
     197              : struct cloudsync_table_context {
     198              :     table_algo  algo;                           // CRDT algoritm associated to the table
     199              :     char        *name;                          // table name
     200              :     char        *schema;                        // table schema
     201              :     char        *meta_ref;                      // schema-qualified meta table name (e.g. "schema"."name_cloudsync")
     202              :     char        *base_ref;                      // schema-qualified base table name (e.g. "schema"."name")
     203              :     char        **col_name;                     // array of column names
     204              :     dbvm_t      **col_merge_stmt;               // array of merge insert stmt (indexed by col_name)
     205              :     dbvm_t      **col_value_stmt;               // array of column value stmt (indexed by col_name)
     206              :     int         *col_id;                        // array of column id
     207              :     col_algo_t  *col_algo;                      // per-column algorithm (normal or block)
     208              :     char        **col_delimiter;                // per-column delimiter for block splitting (NULL = default "\n")
     209              :     bool        has_block_cols;                 // quick check: does this table have any block columns?
     210              :     dbvm_t      *block_value_read_stmt;         // SELECT col_value FROM blocks table
     211              :     dbvm_t      *block_value_write_stmt;        // INSERT OR REPLACE into blocks table
     212              :     dbvm_t      *block_value_delete_stmt;       // DELETE from blocks table
     213              :     dbvm_t      *block_list_stmt;               // SELECT block entries for materialization
     214              :     char        *blocks_ref;                    // schema-qualified blocks table name
     215              :     int         ncols;                          // number of non primary key cols
     216              :     int         npks;                           // number of primary key cols
     217              :     bool        enabled;                        // flag to check if a table is enabled or disabled
     218              :     #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     219              :     bool        rowid_only;                     // a table with no primary keys other than the implicit rowid
     220              :     #endif
     221              :     
     222              :     char        **pk_name;                      // array of primary key names
     223              : 
     224              :     // precompiled statements
     225              :     dbvm_t      *meta_pkexists_stmt;            // check if a primary key already exist in the augmented table
     226              :     dbvm_t      *meta_sentinel_update_stmt;     // update a local sentinel row
     227              :     dbvm_t      *meta_sentinel_insert_stmt;     // insert a local sentinel row
     228              :     dbvm_t      *meta_row_insert_update_stmt;   // insert/update a local row
     229              :     dbvm_t      *meta_row_drop_stmt;            // delete rows from meta
     230              :     dbvm_t      *meta_update_move_stmt;         // update rows in meta when pk changes
     231              :     dbvm_t      *meta_local_cl_stmt;            // compute local cl value
     232              :     dbvm_t      *meta_winner_clock_stmt;        // get the rowid of the last inserted/updated row in the meta table
     233              :     dbvm_t      *meta_merge_delete_drop;
     234              :     dbvm_t      *meta_zero_clock_stmt;
     235              :     dbvm_t      *meta_col_version_stmt;
     236              :     dbvm_t      *meta_site_id_stmt;
     237              :     
     238              :     dbvm_t      *real_col_values_stmt;          // retrieve all column values based on pk
     239              :     dbvm_t      *real_merge_delete_stmt;
     240              :     dbvm_t      *real_merge_sentinel_stmt;
     241              :     
     242              :     bool        is_altering;                    // flag to track if a table alteration is in progress
     243              : 
     244              :     // context
     245              :     cloudsync_context *context;
     246              : };
     247              : 
     248              : struct cloudsync_payload_context {
     249              :     char        *buffer;
     250              :     size_t      bsize;
     251              :     size_t      balloc;
     252              :     size_t      bused;
     253              :     uint64_t    nrows;
     254              :     uint16_t    ncols;
     255              :     uint8_t     version;
     256              : };
     257              : 
     258              : #ifdef _MSC_VER
     259              :     #pragma pack(push, 1) // For MSVC: pack struct with 1-byte alignment
     260              :     #define PACKED
     261              : #else
     262              :     #define PACKED __attribute__((__packed__))
     263              : #endif
     264              : 
     265              : typedef struct PACKED {
     266              :     uint32_t    signature;          // 'CLSY'
     267              :     uint8_t     version;            // protocol version
     268              :     uint8_t     libversion[3];      // major.minor.patch
     269              :     uint32_t    expanded_size;
     270              :     uint16_t    ncols;
     271              :     uint32_t    nrows;
     272              :     uint64_t    schema_hash;
     273              :     uint8_t     checksum[6];        // 48 bits checksum (to ensure struct is 32 bytes)
     274              : } cloudsync_payload_header;
     275              : 
     276              : #ifdef _MSC_VER
     277              :     #pragma pack(pop)
     278              : #endif
     279              : 
     280              : #if CLOUDSYNC_UNITTEST
     281              : bool force_uncompressed_blob = false;
     282              : #define CHECK_FORCE_UNCOMPRESSED_BUFFER()   if (force_uncompressed_blob) use_uncompressed_buffer = true
     283              : #else
     284              : #define CHECK_FORCE_UNCOMPRESSED_BUFFER()
     285              : #endif
     286              : 
     287              : // Internal prototypes
     288              : int local_mark_insert_or_update_meta (cloudsync_table_context *table, const void *pk, size_t pklen, const char *col_name, int64_t db_version, int seq);
     289              : 
     290              : // MARK: - CRDT algos -
     291              : 
     292          660 : table_algo cloudsync_algo_from_name (const char *algo_name) {
     293          660 :     if (algo_name == NULL) return table_algo_none;
     294              :     
     295          660 :     if ((strcasecmp(algo_name, "CausalLengthSet") == 0) || (strcasecmp(algo_name, "cls") == 0)) return table_algo_crdt_cls;
     296          284 :     if ((strcasecmp(algo_name, "GrowOnlySet") == 0) || (strcasecmp(algo_name, "gos") == 0)) return table_algo_crdt_gos;
     297          277 :     if ((strcasecmp(algo_name, "DeleteWinsSet") == 0) || (strcasecmp(algo_name, "dws") == 0)) return table_algo_crdt_dws;
     298          276 :     if ((strcasecmp(algo_name, "AddWinsSet") == 0) || (strcasecmp(algo_name, "aws") == 0)) return table_algo_crdt_aws;
     299              :     
     300              :     // if nothing is found
     301          275 :     return table_algo_none;
     302          660 : }
     303              : 
     304          110 : const char *cloudsync_algo_name (table_algo algo) {
     305          110 :     switch (algo) {
     306          105 :         case table_algo_crdt_cls: return "cls";
     307            1 :         case table_algo_crdt_gos: return "gos";
     308            1 :         case table_algo_crdt_dws: return "dws";
     309            1 :         case table_algo_crdt_aws: return "aws";
     310            1 :         case table_algo_none: return NULL;
     311              :     }
     312            1 :     return NULL;
     313          110 : }
     314              : 
     315              : // MARK: - DBVM Utils -
     316              : 
     317        35484 : DBVM_VALUE dbvm_execute (dbvm_t *stmt, cloudsync_context *data) {
     318        35484 :     if (!stmt) return DBVM_VALUE_ERROR;
     319              : 
     320        35482 :     int rc = databasevm_step(stmt);
     321        35482 :     if (rc != DBRES_ROW && rc != DBRES_DONE) {
     322            2 :         if (data) DEBUG_DBERROR(rc, "stmt_execute", data);
     323            2 :         databasevm_reset(stmt);
     324            2 :         return DBVM_VALUE_ERROR;
     325              :     }
     326              : 
     327        35480 :     DBVM_VALUE result = DBVM_VALUE_CHANGED;
     328        35480 :     if (stmt == data->data_version_stmt) {
     329        33000 :         int version = (int)database_column_int(stmt, 0);
     330        33000 :         if (version != data->data_version) {
     331          243 :             data->data_version = version;
     332          243 :         } else {
     333        32757 :             result = DBVM_VALUE_UNCHANGED;
     334              :         }
     335        35480 :     } else if (stmt == data->schema_version_stmt) {
     336         1240 :         int version = (int)database_column_int(stmt, 0);
     337         1240 :         if (version > data->schema_version) {
     338          398 :             data->schema_version = version;
     339          398 :         } else {
     340          842 :             result = DBVM_VALUE_UNCHANGED;
     341              :         }
     342              :         
     343         2480 :     } else if (stmt == data->db_version_stmt) {
     344         1240 :         data->db_version = (rc == DBRES_DONE) ? CLOUDSYNC_MIN_DB_VERSION : database_column_int(stmt, 0);
     345         1240 :     }
     346              :     
     347        35480 :     databasevm_reset(stmt);
     348        35480 :     return result;
     349        35484 : }
     350              : 
     351         4857 : int dbvm_count (dbvm_t *stmt, const char *value, size_t len, int type) {
     352         4857 :     int result = -1;
     353         4857 :     int rc = DBRES_OK;
     354              :     
     355         4857 :     if (value) {
     356         4856 :         rc = (type == DBTYPE_TEXT) ? databasevm_bind_text(stmt, 1, value, (int)len) : databasevm_bind_blob(stmt, 1, value, len);
     357         4856 :         if (rc != DBRES_OK) goto cleanup;
     358         4856 :     }
     359              : 
     360         4857 :     rc = databasevm_step(stmt);
     361         9714 :     if (rc == DBRES_DONE) {
     362            1 :         result = 0;
     363            1 :         rc = DBRES_OK;
     364         4857 :     } else if (rc == DBRES_ROW) {
     365         4856 :         result = (int)database_column_int(stmt, 0);
     366         4856 :         rc = DBRES_OK;
     367         4856 :     }
     368              :     
     369              : cleanup:
     370         4857 :     databasevm_reset(stmt);
     371         4857 :     return result;
     372              : }
     373              : 
     374       269278 : void dbvm_reset (dbvm_t *stmt) {
     375       269278 :     if (!stmt) return;
     376       246147 :     databasevm_clear_bindings(stmt);
     377       246147 :     databasevm_reset(stmt);
     378       269278 : }
     379              : 
     380              : // MARK: - Database Version -
     381              : 
     382          709 : int cloudsync_dbversion_build_query (cloudsync_context *data, char **sql_out) {
     383              :     // this function must be manually called each time tables changes
     384              :     // because the query plan changes too and it must be re-prepared
     385              :     // unfortunately there is no other way
     386              : 
     387              :     // we need to execute a query like:
     388              :     /*
     389              :      SELECT max(version) as version FROM (
     390              :          SELECT max(db_version) as version FROM "table1_cloudsync"
     391              :          UNION ALL
     392              :          SELECT max(db_version) as version FROM "table2_cloudsync"
     393              :          UNION ALL
     394              :          SELECT max(db_version) as version FROM "table3_cloudsync"
     395              :          UNION
     396              :          SELECT value as version FROM cloudsync_settings WHERE key = 'pre_alter_dbversion'
     397              :      )
     398              :      */
     399              : 
     400              :     // the good news is that the query can be computed in SQLite without the need to do any extra computation from the host language
     401              : 
     402          709 :     *sql_out = NULL;
     403          709 :     return database_select_text(data, SQL_DBVERSION_BUILD_QUERY, sql_out);
     404              : }
     405              : 
     406          950 : int cloudsync_dbversion_rebuild (cloudsync_context *data) {
     407          950 :     if (data->db_version_stmt) {
     408          461 :         databasevm_finalize(data->db_version_stmt);
     409          461 :         data->db_version_stmt = NULL;
     410          461 :     }
     411              : 
     412          950 :     int64_t count = dbutils_table_settings_count_tables(data);
     413          950 :     if (count == 0) return DBRES_OK;
     414          709 :     else if (count == -1) return cloudsync_set_dberror(data);
     415              : 
     416          709 :     char *sql = NULL;
     417          709 :     int rc = cloudsync_dbversion_build_query(data, &sql);
     418          709 :     if (rc != DBRES_OK) return cloudsync_set_dberror(data);
     419              : 
     420              :     // A NULL SQL with rc == OK means the generator produced a NULL row:
     421              :     // sqlite_master has no *_cloudsync meta-tables (for example, the user
     422              :     // dropped the base table and its meta-table without calling
     423              :     // cloudsync_cleanup, leaving stale cloudsync_table_settings rows).
     424              :     // Treat this the same as count == 0: no prepared statement, db_version
     425              :     // stays at the minimum and will be rebuilt on the next cloudsync_init.
     426              :     // Genuine errors from database_select_text are handled above.
     427          708 :     if (!sql) return DBRES_OK;
     428              :     DEBUG_SQL("db_version_stmt: %s", sql);
     429              : 
     430          707 :     rc = databasevm_prepare(data, sql, (void **)&data->db_version_stmt, DBFLAG_PERSISTENT);
     431              :     DEBUG_STMT("db_version_stmt %p", data->db_version_stmt);
     432          707 :     cloudsync_memory_free(sql);
     433          707 :     return rc;
     434          950 : }
     435              : 
     436         1240 : int cloudsync_dbversion_rerun (cloudsync_context *data) {
     437         1240 :     DBVM_VALUE schema_changed = dbvm_execute(data->schema_version_stmt, data);
     438         1240 :     if (schema_changed == DBVM_VALUE_ERROR) return -1;
     439              : 
     440         1240 :     if (schema_changed == DBVM_VALUE_CHANGED) {
     441          398 :         int rc = cloudsync_dbversion_rebuild(data);
     442          398 :         if (rc != DBRES_OK) return -1;
     443          398 :     }
     444              : 
     445         1240 :     if (!data->db_version_stmt) {
     446            0 :         data->db_version = CLOUDSYNC_MIN_DB_VERSION;
     447            0 :         return 0;
     448              :     }
     449              : 
     450         1240 :     DBVM_VALUE rc = dbvm_execute(data->db_version_stmt, data);
     451         1240 :     if (rc == DBVM_VALUE_ERROR) return -1;
     452         1240 :     return 0;
     453         1240 : }
     454              : 
     455        33002 : int cloudsync_dbversion_check_uptodate (cloudsync_context *data) {
     456              :     // perform a PRAGMA data_version to check if some other process write any data
     457        33002 :     DBVM_VALUE rc = dbvm_execute(data->data_version_stmt, data);
     458        33002 :     if (rc == DBVM_VALUE_ERROR) return -1;
     459              :     
     460              :     // db_version is already set and there is no need to update it
     461        33000 :     if (data->db_version != CLOUDSYNC_VALUE_NOTSET && rc == DBVM_VALUE_UNCHANGED) return 0;
     462              :     
     463         1240 :     return cloudsync_dbversion_rerun(data);
     464        33002 : }
     465              : 
     466        32976 : int64_t cloudsync_dbversion_next (cloudsync_context *data, int64_t merging_version) {
     467        32976 :     int rc = cloudsync_dbversion_check_uptodate(data);
     468        32976 :     if (rc != DBRES_OK) return -1;
     469              :     
     470        32975 :     int64_t result = data->db_version + 1;
     471        32975 :     if (result < data->pending_db_version) result = data->pending_db_version;
     472        32975 :     if (merging_version != CLOUDSYNC_VALUE_NOTSET && result < merging_version) result = merging_version;
     473        32975 :     data->pending_db_version = result;
     474              :     
     475        32975 :     return result;
     476        32976 : }
     477              : 
     478              : // MARK: - PK Context -
     479              : 
     480            0 : char *cloudsync_pk_context_tbl (cloudsync_pk_decode_bind_context *ctx, int64_t *tbl_len) {
     481            0 :     *tbl_len = ctx->tbl_len;
     482            0 :     return ctx->tbl;
     483              : }
     484              : 
     485            0 : void *cloudsync_pk_context_pk (cloudsync_pk_decode_bind_context *ctx, int64_t *pk_len) {
     486            0 :     *pk_len = ctx->pk_len;
     487            0 :     return (void *)ctx->pk;
     488              : }
     489              : 
     490            0 : char *cloudsync_pk_context_colname (cloudsync_pk_decode_bind_context *ctx, int64_t *colname_len) {
     491            0 :     *colname_len = ctx->col_name_len;
     492            0 :     return ctx->col_name;
     493              : }
     494              : 
     495            0 : int64_t cloudsync_pk_context_cl (cloudsync_pk_decode_bind_context *ctx) {
     496            0 :     return ctx->cl;
     497              : }
     498              : 
     499            0 : int64_t cloudsync_pk_context_dbversion (cloudsync_pk_decode_bind_context *ctx) {
     500            0 :     return ctx->db_version;
     501              : }
     502              : 
     503              : // MARK: - CloudSync Context -
     504              : 
     505        15497 : int cloudsync_insync (cloudsync_context *data) {
     506        15497 :     return data->insync;
     507              : }
     508              : 
     509          838 : void *cloudsync_siteid (cloudsync_context *data) {
     510          838 :     return (void *)data->site_id;
     511              : }
     512              : 
     513            2 : void cloudsync_reset_siteid (cloudsync_context *data) {
     514            2 :     memset(data->site_id, 0, sizeof(uint8_t) * UUID_LEN);
     515            2 : }
     516              : 
     517          249 : int cloudsync_load_siteid (cloudsync_context *data) {
     518              :     // check if site_id was already loaded
     519          249 :     if (data->site_id[0] != 0) return DBRES_OK;
     520              :     
     521              :     // load site_id
     522          247 :     char *buffer = NULL;
     523          247 :     int64_t size = 0;
     524          247 :     int rc = database_select_blob(data, SQL_SITEID_SELECT_ROWID0, &buffer, &size);
     525          247 :     if (rc != DBRES_OK) return rc;
     526          247 :     if (!buffer || size != UUID_LEN) {
     527            0 :         if (buffer) cloudsync_memory_free(buffer);
     528            0 :         return cloudsync_set_error(data, "Unable to retrieve siteid", DBRES_MISUSE);
     529              :     }
     530              :     
     531          247 :     memcpy(data->site_id, buffer, UUID_LEN);
     532          247 :     cloudsync_memory_free(buffer);
     533              :     
     534          247 :     return DBRES_OK;
     535          249 : }
     536              : 
     537            2 : int64_t cloudsync_dbversion (cloudsync_context *data) {
     538            2 :     return data->db_version;
     539              : }
     540              : 
     541        13686 : int cloudsync_bumpseq (cloudsync_context *data) {
     542        13686 :     int value = data->seq;
     543        13686 :     data->seq += 1;
     544        13686 :     return value;
     545              : }
     546              : 
     547          304 : void cloudsync_update_schema_hash (cloudsync_context *data) {
     548          304 :     database_update_schema_hash(data, &data->schema_hash);
     549          304 : }
     550              : 
     551        67063 : void *cloudsync_db (cloudsync_context *data) {
     552        67063 :     return data->db;
     553              : }
     554              : 
     555          551 : int cloudsync_add_dbvms (cloudsync_context *data) {
     556              :     DEBUG_DBFUNCTION("cloudsync_add_stmts");
     557              :     
     558          551 :     if (data->data_version_stmt == NULL) {
     559          247 :         int rc = databasevm_prepare(data, SQL_DATA_VERSION, (void **)&data->data_version_stmt, DBFLAG_PERSISTENT);
     560              :         DEBUG_STMT("data_version_stmt %p", data->data_version_stmt);
     561          247 :         if (rc != DBRES_OK) return rc;
     562              :         DEBUG_SQL("data_version_stmt: %s", SQL_DATA_VERSION);
     563          247 :     }
     564              :     
     565          551 :     if (data->schema_version_stmt == NULL) {
     566          247 :         int rc = databasevm_prepare(data, SQL_SCHEMA_VERSION, (void **)&data->schema_version_stmt, DBFLAG_PERSISTENT);
     567              :         DEBUG_STMT("schema_version_stmt %p", data->schema_version_stmt);
     568          247 :         if (rc != DBRES_OK) return rc;
     569              :         DEBUG_SQL("schema_version_stmt: %s", SQL_SCHEMA_VERSION);
     570          247 :     }
     571              :     
     572          551 :     if (data->getset_siteid_stmt == NULL) {
     573              :         // get and set index of the site_id
     574              :         // 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,
     575              :         // however, we can use a workaround by leveraging the INSERT statement with ON CONFLICT DO UPDATE and then combining it with RETURNING rowid
     576          247 :         int rc = databasevm_prepare(data, SQL_SITEID_GETSET_ROWID_BY_SITEID, (void **)&data->getset_siteid_stmt, DBFLAG_PERSISTENT);
     577              :         DEBUG_STMT("getset_siteid_stmt %p", data->getset_siteid_stmt);
     578          247 :         if (rc != DBRES_OK) return rc;
     579              :         DEBUG_SQL("getset_siteid_stmt: %s", SQL_SITEID_GETSET_ROWID_BY_SITEID);
     580          247 :     }
     581              :     
     582          551 :     return cloudsync_dbversion_rebuild(data);
     583          551 : }
     584              : 
     585           23 : int cloudsync_set_error (cloudsync_context *data, const char *err_user, int err_code) {
     586              :     // force err_code to be something different than OK
     587           23 :     if (err_code == DBRES_OK) err_code = database_errcode(data);
     588           23 :     if (err_code == DBRES_OK) err_code = DBRES_ERROR;
     589              :     
     590              :     // compute a meaningful error message
     591           23 :     if (err_user == NULL) {
     592            6 :         snprintf(data->errmsg, sizeof(data->errmsg), "%s", database_errmsg(data));
     593            6 :     } else {
     594           17 :         const char *db_error = database_errmsg(data);
     595              :         char db_error_copy[sizeof(data->errmsg)];
     596           17 :         int rc = database_errcode(data);
     597           17 :         if (rc == DBRES_OK) {
     598           17 :             snprintf(data->errmsg, sizeof(data->errmsg), "%s", err_user);
     599           17 :         } else {
     600            0 :             if (db_error == data->errmsg) {
     601            0 :                 snprintf(db_error_copy, sizeof(db_error_copy), "%s", db_error);
     602            0 :                 db_error = db_error_copy;
     603            0 :             }
     604            0 :             snprintf(data->errmsg, sizeof(data->errmsg), "%s (%s)", err_user, db_error);
     605              :         }
     606              :     }
     607              :     
     608           23 :     data->errcode = err_code;
     609           23 :     return err_code;
     610              : }
     611              : 
     612            6 : int cloudsync_set_dberror (cloudsync_context *data) {
     613            6 :     return cloudsync_set_error(data, NULL, DBRES_OK);
     614              : }
     615              : 
     616           14 : const char *cloudsync_errmsg (cloudsync_context *data) {
     617           14 :     return data->errmsg;
     618              : }
     619              : 
     620            4 : int cloudsync_errcode (cloudsync_context *data) {
     621            4 :     return data->errcode;
     622              : }
     623              : 
     624            2 : void cloudsync_reset_error (cloudsync_context *data) {
     625            2 :     data->errmsg[0] = 0;
     626            2 :     data->errcode = DBRES_OK;
     627            2 : }
     628              : 
     629            3 : void *cloudsync_auxdata (cloudsync_context *data) {
     630            3 :     return data->aux_data;
     631              : }
     632              : 
     633            2 : void cloudsync_set_auxdata (cloudsync_context *data, void *xdata) {
     634            2 :     data->aux_data = xdata;
     635            2 : }
     636              : 
     637            6 : void cloudsync_set_schema (cloudsync_context *data, const char *schema) {
     638            6 :     if (data->current_schema && schema && strcmp(data->current_schema, schema) == 0) return;
     639            5 :     if (data->current_schema) cloudsync_memory_free(data->current_schema);
     640            5 :     data->current_schema = NULL;
     641            5 :     if (schema) data->current_schema = cloudsync_string_dup_lowercase(schema);
     642            6 : }
     643              : 
     644         1383 : const char *cloudsync_schema (cloudsync_context *data) {
     645         1383 :     return data->current_schema;
     646              : }
     647              : 
     648            4 : const char *cloudsync_table_schema (cloudsync_context *data, const char *table_name) {
     649            4 :     cloudsync_table_context *table = table_lookup(data, table_name);
     650            4 :     if (!table) return NULL;
     651              : 
     652            1 :     return table->schema;
     653            4 : }
     654              : 
     655              : // MARK: - Table Utils -
     656              : 
     657           69 : void table_pknames_free (char **names, int nrows) {
     658           69 :     if (!names) return;
     659          132 :     for (int i = 0; i < nrows; ++i) {cloudsync_memory_free(names[i]);}
     660           46 :     cloudsync_memory_free(names);
     661           69 : }
     662              : 
     663          304 : char *table_build_mergedelete_sql (cloudsync_table_context *table) {
     664              :     #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     665              :     if (table->rowid_only) {
     666              :         char *sql = memory_mprintf(SQL_DELETE_ROW_BY_ROWID, table->name);
     667              :         return sql;
     668              :     }
     669              :     #endif
     670              : 
     671          304 :     return sql_build_delete_by_pk(table->context, table->name, table->schema);
     672              : }
     673              : 
     674         1423 : char *table_build_mergeinsert_sql (cloudsync_table_context *table, const char *colname) {
     675         1423 :     char *sql = NULL;
     676              :     
     677              :     #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     678              :     if (table->rowid_only) {
     679              :         if (colname == NULL) {
     680              :             // INSERT OR IGNORE INTO customers (first_name,last_name) VALUES (?,?);
     681              :             sql = memory_mprintf(SQL_INSERT_ROWID_IGNORE, table->name);
     682              :         } else {
     683              :             // INSERT INTO customers (first_name,last_name,age) VALUES (?,?,?) ON CONFLICT DO UPDATE SET age=?;
     684              :             sql = memory_mprintf(SQL_UPSERT_ROWID_AND_COL_BY_ROWID, table->name, colname, colname);
     685              :         }
     686              :         return sql;
     687              :     }
     688              :     #endif
     689              :     
     690         1423 :     if (colname == NULL) {
     691              :         // is sentinel insert
     692          304 :         sql = sql_build_insert_pk_ignore(table->context, table->name, table->schema);
     693          304 :     } else {
     694         1119 :         sql = sql_build_upsert_pk_and_col(table->context, table->name, colname, table->schema);
     695              :     }
     696         1423 :     return sql;
     697              : }
     698              : 
     699         1118 : char *table_build_value_sql (cloudsync_table_context *table, const char *colname) {
     700              :     #if !CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
     701              :     if (table->rowid_only) {
     702              :         char *colnamequote = "\"";
     703              :         char *sql = memory_mprintf(SQL_SELECT_COLS_BY_ROWID_FMT, colnamequote, colname, colnamequote, table->name);
     704              :         return sql;
     705              :     }
     706              :     #endif
     707              :         
     708              :     // SELECT age FROM customers WHERE first_name=? AND last_name=?;
     709         1118 :     return sql_build_select_cols_by_pk(table->context, table->name, colname, table->schema);
     710              : }
     711              :     
     712          304 : cloudsync_table_context *table_create (cloudsync_context *data, const char *name, table_algo algo) {
     713              :     DEBUG_DBFUNCTION("table_create %s", name);
     714              :     
     715          304 :     cloudsync_table_context *table = (cloudsync_table_context *)cloudsync_memory_zeroalloc(sizeof(cloudsync_table_context));
     716          304 :     if (!table) return NULL;
     717              :     
     718          304 :     table->context = data;
     719          304 :     table->algo = algo;
     720          304 :     table->name = cloudsync_string_dup_lowercase(name);
     721              : 
     722              :     // Detect schema from metadata table location. If metadata table doesn't
     723              :     // exist yet (during initialization), fall back to cloudsync_schema() which
     724              :     // returns the explicitly set schema or current_schema().
     725          304 :     table->schema = database_table_schema(name);
     726          304 :     if (!table->schema) {
     727          304 :         const char *fallback_schema = cloudsync_schema(data);
     728          304 :         if (fallback_schema) {
     729            0 :             table->schema = cloudsync_string_dup(fallback_schema);
     730            0 :         }
     731          304 :     }
     732              : 
     733          304 :     if (!table->name) {
     734            0 :         cloudsync_memory_free(table);
     735            0 :         return NULL;
     736              :     }
     737          304 :     table->meta_ref = database_build_meta_ref(table->schema, table->name);
     738          304 :     table->base_ref = database_build_base_ref(table->schema, table->name);
     739          304 :     table->enabled = true;
     740              :         
     741          304 :     return table;
     742          304 : }
     743              : 
     744          304 : void table_free (cloudsync_table_context *table) {
     745              :     DEBUG_DBFUNCTION("table_free %s", (table) ? (table->name) : "NULL");
     746          304 :     if (!table) return;
     747              :     
     748          304 :     if (table->col_name) {
     749         1383 :         for (int i=0; i<table->ncols; ++i) {
     750         1118 :             cloudsync_memory_free(table->col_name[i]);
     751         1118 :         }
     752          265 :         cloudsync_memory_free(table->col_name);
     753          265 :     }
     754          304 :     if (table->col_merge_stmt) {
     755         1383 :         for (int i=0; i<table->ncols; ++i) {
     756         1118 :             databasevm_finalize(table->col_merge_stmt[i]);
     757         1118 :         }
     758          265 :         cloudsync_memory_free(table->col_merge_stmt);
     759          265 :     }
     760          304 :     if (table->col_value_stmt) {
     761         1383 :         for (int i=0; i<table->ncols; ++i) {
     762         1118 :             databasevm_finalize(table->col_value_stmt[i]);
     763         1118 :         }
     764          265 :         cloudsync_memory_free(table->col_value_stmt);
     765          265 :     }
     766          304 :     if (table->col_id) {
     767          265 :         cloudsync_memory_free(table->col_id);
     768          265 :     }
     769          304 :     if (table->col_algo) {
     770          265 :         cloudsync_memory_free(table->col_algo);
     771          265 :     }
     772          304 :     if (table->col_delimiter) {
     773         1383 :         for (int i=0; i<table->ncols; ++i) {
     774         1118 :             if (table->col_delimiter[i]) cloudsync_memory_free(table->col_delimiter[i]);
     775         1118 :         }
     776          265 :         cloudsync_memory_free(table->col_delimiter);
     777          265 :     }
     778              : 
     779          304 :     if (table->block_value_read_stmt) databasevm_finalize(table->block_value_read_stmt);
     780          304 :     if (table->block_value_write_stmt) databasevm_finalize(table->block_value_write_stmt);
     781          304 :     if (table->block_value_delete_stmt) databasevm_finalize(table->block_value_delete_stmt);
     782          304 :     if (table->block_list_stmt) databasevm_finalize(table->block_list_stmt);
     783          304 :     if (table->blocks_ref) cloudsync_memory_free(table->blocks_ref);
     784              : 
     785          304 :     if (table->name) cloudsync_memory_free(table->name);
     786          304 :     if (table->schema) cloudsync_memory_free(table->schema);
     787          304 :     if (table->meta_ref) cloudsync_memory_free(table->meta_ref);
     788          304 :     if (table->base_ref) cloudsync_memory_free(table->base_ref);
     789          304 :     if (table->pk_name) table_pknames_free(table->pk_name, table->npks);
     790          304 :     if (table->meta_pkexists_stmt) databasevm_finalize(table->meta_pkexists_stmt);
     791          304 :     if (table->meta_sentinel_update_stmt) databasevm_finalize(table->meta_sentinel_update_stmt);
     792          304 :     if (table->meta_sentinel_insert_stmt) databasevm_finalize(table->meta_sentinel_insert_stmt);
     793          304 :     if (table->meta_row_insert_update_stmt) databasevm_finalize(table->meta_row_insert_update_stmt);
     794          304 :     if (table->meta_row_drop_stmt) databasevm_finalize(table->meta_row_drop_stmt);
     795          304 :     if (table->meta_update_move_stmt) databasevm_finalize(table->meta_update_move_stmt);
     796          304 :     if (table->meta_local_cl_stmt) databasevm_finalize(table->meta_local_cl_stmt);
     797          304 :     if (table->meta_winner_clock_stmt) databasevm_finalize(table->meta_winner_clock_stmt);
     798          304 :     if (table->meta_merge_delete_drop) databasevm_finalize(table->meta_merge_delete_drop);
     799          304 :     if (table->meta_zero_clock_stmt) databasevm_finalize(table->meta_zero_clock_stmt);
     800          304 :     if (table->meta_col_version_stmt) databasevm_finalize(table->meta_col_version_stmt);
     801          304 :     if (table->meta_site_id_stmt) databasevm_finalize(table->meta_site_id_stmt);
     802              :     
     803          304 :     if (table->real_col_values_stmt) databasevm_finalize(table->real_col_values_stmt);
     804          304 :     if (table->real_merge_delete_stmt) databasevm_finalize(table->real_merge_delete_stmt);
     805          304 :     if (table->real_merge_sentinel_stmt) databasevm_finalize(table->real_merge_sentinel_stmt);
     806              :     
     807          304 :     cloudsync_memory_free(table);
     808          304 : }
     809              : 
     810          304 : int table_add_stmts (cloudsync_table_context *table, int ncols) {
     811          304 :     int rc = DBRES_OK;
     812          304 :     char *sql = NULL;
     813          304 :     cloudsync_context *data = table->context;
     814              :     
     815              :     // META TABLE statements
     816              :     
     817              :     // 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));
     818              :     
     819              :     // precompile the pk exists statement
     820              :     // 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
     821              :     // EXPLAIN QUERY PLAN reports: SEARCH table_name USING PRIMARY KEY (pk=?)
     822          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_ROW_EXISTS_BY_PK, table->meta_ref);
     823          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     824              :     DEBUG_SQL("meta_pkexists_stmt: %s", sql);
     825              : 
     826          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_pkexists_stmt, DBFLAG_PERSISTENT);
     827          304 :     cloudsync_memory_free(sql);
     828          304 :     if (rc != DBRES_OK) goto cleanup;
     829              : 
     830              :     // precompile the update local sentinel statement
     831          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_UPDATE_COL_BUMP_VERSION, table->meta_ref, CLOUDSYNC_TOMBSTONE_VALUE);
     832          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     833              :     DEBUG_SQL("meta_sentinel_update_stmt: %s", sql);
     834              :     
     835          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_sentinel_update_stmt, DBFLAG_PERSISTENT);
     836          304 :     cloudsync_memory_free(sql);
     837          304 :     if (rc != DBRES_OK) goto cleanup;
     838              :     
     839              :     // precompile the insert local sentinel statement
     840          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_UPSERT_COL_INIT_OR_BUMP_VERSION, table->meta_ref, CLOUDSYNC_TOMBSTONE_VALUE, table->meta_ref, table->meta_ref, table->meta_ref);
     841          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     842              :     DEBUG_SQL("meta_sentinel_insert_stmt: %s", sql);
     843              :     
     844          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_sentinel_insert_stmt, DBFLAG_PERSISTENT);
     845          304 :     cloudsync_memory_free(sql);
     846          304 :     if (rc != DBRES_OK) goto cleanup;
     847              : 
     848              :     // precompile the insert/update local row statement
     849          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_UPSERT_RAW_COLVERSION, table->meta_ref, table->meta_ref);
     850          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     851              :     DEBUG_SQL("meta_row_insert_update_stmt: %s", sql);
     852              :     
     853          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_row_insert_update_stmt, DBFLAG_PERSISTENT);
     854          304 :     cloudsync_memory_free(sql);
     855          304 :     if (rc != DBRES_OK) goto cleanup;
     856              :     
     857              :     // precompile the delete rows from meta
     858          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_DELETE_PK_EXCEPT_COL, table->meta_ref, CLOUDSYNC_TOMBSTONE_VALUE);
     859          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     860              :     DEBUG_SQL("meta_row_drop_stmt: %s", sql);
     861              : 
     862          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_row_drop_stmt, DBFLAG_PERSISTENT);
     863          304 :     cloudsync_memory_free(sql);
     864          304 :     if (rc != DBRES_OK) goto cleanup;
     865              :     
     866              :     // precompile the update rows from meta when pk changes
     867              :     // see https://github.com/sqliteai/sqlite-sync/blob/main/docs/PriKey.md for more details
     868          304 :     sql = sql_build_rekey_pk_and_reset_version_except_col(data, table->name, CLOUDSYNC_TOMBSTONE_VALUE);
     869          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     870              :     DEBUG_SQL("meta_update_move_stmt: %s", sql);
     871              :     
     872          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_update_move_stmt, DBFLAG_PERSISTENT);
     873          304 :     cloudsync_memory_free(sql);
     874          304 :     if (rc != DBRES_OK) goto cleanup;
     875              :     
     876              :     // local cl
     877          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_GET_COL_VERSION_OR_ROW_EXISTS, table->meta_ref, CLOUDSYNC_TOMBSTONE_VALUE, table->meta_ref);
     878          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     879              :     DEBUG_SQL("meta_local_cl_stmt: %s", sql);
     880              :     
     881          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_local_cl_stmt, DBFLAG_PERSISTENT);
     882          304 :     cloudsync_memory_free(sql);
     883          304 :     if (rc != DBRES_OK) goto cleanup;
     884              :     
     885              :     // rowid of the last inserted/updated row in the meta table
     886          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_INSERT_RETURN_CHANGE_ID, table->meta_ref);
     887          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     888              :     DEBUG_SQL("meta_winner_clock_stmt: %s", sql);
     889              :     
     890          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_winner_clock_stmt, DBFLAG_PERSISTENT);
     891          304 :     cloudsync_memory_free(sql);
     892          304 :     if (rc != DBRES_OK) goto cleanup;
     893              :     
     894          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_DELETE_PK_EXCEPT_COL, table->meta_ref, CLOUDSYNC_TOMBSTONE_VALUE);
     895          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     896              :     DEBUG_SQL("meta_merge_delete_drop: %s", sql);
     897              : 
     898          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_merge_delete_drop, DBFLAG_PERSISTENT);
     899          304 :     cloudsync_memory_free(sql);
     900          304 :     if (rc != DBRES_OK) goto cleanup;
     901              :     
     902              :     // zero clock
     903          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_TOMBSTONE_PK_EXCEPT_COL, table->meta_ref, CLOUDSYNC_TOMBSTONE_VALUE);
     904          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     905              :     DEBUG_SQL("meta_zero_clock_stmt: %s", sql);
     906              :     
     907          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_zero_clock_stmt, DBFLAG_PERSISTENT);
     908          304 :     cloudsync_memory_free(sql);
     909          304 :     if (rc != DBRES_OK) goto cleanup;
     910              :     
     911              :     // col_version
     912          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_SELECT_COL_VERSION_BY_PK_COL, table->meta_ref);
     913          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     914              :     DEBUG_SQL("meta_col_version_stmt: %s", sql);
     915              :     
     916          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_col_version_stmt, DBFLAG_PERSISTENT);
     917          304 :     cloudsync_memory_free(sql);
     918          304 :     if (rc != DBRES_OK) goto cleanup;
     919              :     
     920              :     // site_id
     921          304 :     sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_SELECT_SITE_ID_BY_PK_COL, table->meta_ref);
     922          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     923              :     DEBUG_SQL("meta_site_id_stmt: %s", sql);
     924              :     
     925          304 :     rc = databasevm_prepare(data, sql, (void **)&table->meta_site_id_stmt, DBFLAG_PERSISTENT);
     926          304 :     cloudsync_memory_free(sql);
     927          304 :     if (rc != DBRES_OK) goto cleanup;
     928              :     
     929              :     // REAL TABLE statements
     930              : 
     931              :     // precompile the get column value statement
     932          304 :     if (ncols > 0) {
     933          265 :         sql = sql_build_select_nonpk_by_pk(data, table->name, table->schema);
     934          265 :         if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     935              :         DEBUG_SQL("real_col_values_stmt: %s", sql);
     936              :         
     937          265 :         rc = databasevm_prepare(data, sql, (void **)&table->real_col_values_stmt, DBFLAG_PERSISTENT);
     938          265 :         cloudsync_memory_free(sql);
     939          265 :         if (rc != DBRES_OK) goto cleanup;
     940          265 :     }
     941              :     
     942          304 :     sql = table_build_mergedelete_sql(table);
     943          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     944              :     DEBUG_SQL("real_merge_delete: %s", sql);
     945              :     
     946          304 :     rc = databasevm_prepare(data, sql, (void **)&table->real_merge_delete_stmt, DBFLAG_PERSISTENT);
     947          304 :     cloudsync_memory_free(sql);
     948          304 :     if (rc != DBRES_OK) goto cleanup;
     949              :     
     950          304 :     sql = table_build_mergeinsert_sql(table, NULL);
     951          304 :     if (!sql) {rc = DBRES_NOMEM; goto cleanup;}
     952              :     DEBUG_SQL("real_merge_sentinel: %s", sql);
     953              :     
     954          304 :     rc = databasevm_prepare(data, sql, (void **)&table->real_merge_sentinel_stmt, DBFLAG_PERSISTENT);
     955          304 :     cloudsync_memory_free(sql);
     956          304 :     if (rc != DBRES_OK) goto cleanup;
     957              :     
     958              : cleanup:
     959          304 :     if (rc != DBRES_OK) DEBUG_ALWAYS("table_add_stmts error: %d %s\n", rc, database_errmsg(data));
     960          304 :     return rc;
     961              : }
     962              : 
     963       206281 : cloudsync_table_context *table_lookup (cloudsync_context *data, const char *table_name) {
     964              :     DEBUG_DBFUNCTION("table_lookup %s", table_name);
     965              :     
     966       206281 :     if (table_name) {
     967       207907 :         for (int i=0; i<data->tables_count; ++i) {
     968       207290 :             if ((strcasecmp(data->tables[i]->name, table_name) == 0)) return data->tables[i];
     969         1626 :         }
     970          617 :     }
     971              :     
     972          617 :     return NULL;
     973       206281 : }
     974              : 
     975       188872 : void *table_column_lookup (cloudsync_table_context *table, const char *col_name, bool is_merge, int *index) {
     976              :     DEBUG_DBFUNCTION("table_column_lookup %s", col_name);
     977              :     
     978       338218 :     for (int i=0; i<table->ncols; ++i) {
     979       338218 :         if (strcasecmp(table->col_name[i], col_name) == 0) {
     980       188872 :             if (index) *index = i;
     981       188872 :             return (is_merge) ? table->col_merge_stmt[i] : table->col_value_stmt[i];
     982              :         }
     983       149346 :     }
     984              :     
     985            0 :     if (index) *index = -1;
     986            0 :     return NULL;
     987       188872 : }
     988              : 
     989          303 : int table_remove (cloudsync_context *data, cloudsync_table_context *table) {
     990          303 :     const char *table_name = table->name;
     991              :     DEBUG_DBFUNCTION("table_remove %s", table_name);
     992              :     
     993          353 :     for (int i = 0; i < data->tables_count; ++i) {
     994          353 :         cloudsync_table_context *t = data->tables[i];
     995              :         
     996              :         // pointer compare is fastest but fallback to strcasecmp if not same pointer
     997          353 :         if ((t == table) || ((strcasecmp(t->name, table_name) == 0))) {
     998          303 :             int last = data->tables_count - 1;
     999          303 :             data->tables[i] = data->tables[last];   // move last into the hole (keeps array dense)
    1000          303 :             data->tables[last] = NULL;              // NULLify tail (as an extra security measure)
    1001          303 :             data->tables_count--;
    1002          303 :             return data->tables_count;
    1003              :         }
    1004           50 :     }
    1005              :     
    1006            0 :     return -1;
    1007          303 : }
    1008              : 
    1009         1119 : int table_add_to_context_cb (void *xdata, int ncols, char **values, char **names) {
    1010         1119 :     cloudsync_table_context *table = (cloudsync_table_context *)xdata;
    1011         1119 :     cloudsync_context *data = table->context;
    1012              : 
    1013         1119 :     int index = table->ncols;
    1014         2237 :     for (int i=0; i<ncols; i+=2) {
    1015         1119 :         const char *name = values[i];
    1016         1119 :         int cid = (int)strtol(values[i+1], NULL, 0);
    1017              : 
    1018         1119 :         table->col_id[index] = cid;
    1019         1119 :         table->col_name[index] = cloudsync_string_dup_lowercase(name);
    1020         1119 :         if (!table->col_name[index]) goto error;
    1021              : 
    1022         1119 :         char *sql = table_build_mergeinsert_sql(table, name);
    1023         1119 :         if (!sql) goto error;
    1024              :         DEBUG_SQL("col_merge_stmt[%d]: %s", index, sql);
    1025              : 
    1026         1119 :         int rc = databasevm_prepare(data, sql, (void **)&table->col_merge_stmt[index], DBFLAG_PERSISTENT);
    1027         1119 :         cloudsync_memory_free(sql);
    1028         1119 :         if (rc != DBRES_OK) goto error;
    1029         1118 :         if (!table->col_merge_stmt[index]) goto error;
    1030              : 
    1031         1118 :         sql = table_build_value_sql(table, name);
    1032         1118 :         if (!sql) goto error;
    1033              :         DEBUG_SQL("col_value_stmt[%d]: %s", index, sql);
    1034              : 
    1035         1118 :         rc = databasevm_prepare(data, sql, (void **)&table->col_value_stmt[index], DBFLAG_PERSISTENT);
    1036         1118 :         cloudsync_memory_free(sql);
    1037         1118 :         if (rc != DBRES_OK) goto error;
    1038         1118 :         if (!table->col_value_stmt[index]) goto error;
    1039         1118 :     }
    1040         1118 :     table->ncols += 1;
    1041              : 
    1042         1118 :     return 0;
    1043              : 
    1044              : error:
    1045              :     // clean up partially-initialized entry at index
    1046            1 :     if (table->col_name[index]) {cloudsync_memory_free(table->col_name[index]); table->col_name[index] = NULL;}
    1047            1 :     if (table->col_merge_stmt[index]) {databasevm_finalize(table->col_merge_stmt[index]); table->col_merge_stmt[index] = NULL;}
    1048            1 :     if (table->col_value_stmt[index]) {databasevm_finalize(table->col_value_stmt[index]); table->col_value_stmt[index] = NULL;}
    1049            1 :     return 1;
    1050         1119 : }
    1051              : 
    1052          304 : bool table_ensure_capacity (cloudsync_context *data) {
    1053          304 :     if (data->tables_count < data->tables_cap) return true;
    1054              :     
    1055            0 :     int new_cap = data->tables_cap ? data->tables_cap * 2 : CLOUDSYNC_INIT_NTABLES;
    1056            0 :     size_t bytes = (size_t)new_cap * sizeof(*data->tables);
    1057            0 :     void *p = cloudsync_memory_realloc(data->tables, bytes);
    1058            0 :     if (!p) return false;
    1059              :     
    1060            0 :     data->tables = (cloudsync_table_context **)p;
    1061            0 :     data->tables_cap = new_cap;
    1062            0 :     return true;
    1063          304 : }
    1064              : 
    1065          309 : bool table_add_to_context (cloudsync_context *data, table_algo algo, const char *table_name) {
    1066              :     DEBUG_DBFUNCTION("cloudsync_context_add_table %s", table_name);
    1067              : 
    1068              :     // Check if table already initialized in this connection's context.
    1069              :     // Note: This prevents same-connection duplicate initialization.
    1070              :     // SQLite clients cannot distinguish schemas, so having 'public.users'
    1071              :     // and 'auth.users' would cause sync ambiguity. Users should avoid
    1072              :     // initializing tables with the same name in different schemas.
    1073              :     // If two concurrent connections initialize tables with the same name
    1074              :     // in different schemas, the behavior is undefined.
    1075          309 :     cloudsync_table_context *table = table_lookup(data, table_name);
    1076          309 :     if (table) return true;
    1077              :     
    1078              :     // check for space availability
    1079          304 :     if (!table_ensure_capacity(data)) return false;
    1080              :     
    1081              :     // setup a new table
    1082          304 :     table = table_create(data, table_name, algo);
    1083          304 :     if (!table) return false;
    1084              :     
    1085              :     // fill remaining metadata in the table
    1086          304 :     int count = database_count_pk(data, table_name, false, table->schema);
    1087          304 :     if (count < 0) {cloudsync_set_dberror(data); goto abort_add_table;}
    1088          304 :     table->npks = count;
    1089          304 :     if (table->npks == 0) {
    1090              :         #if CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
    1091            0 :         goto abort_add_table;
    1092              :         #else
    1093              :         table->rowid_only = true;
    1094              :         table->npks = 1; // rowid
    1095              :         #endif
    1096              :     }
    1097              :     
    1098          304 :     int ncols = database_count_nonpk(data, table_name, table->schema);
    1099          304 :     if (ncols < 0) {cloudsync_set_dberror(data); goto abort_add_table;}
    1100          304 :     int rc = table_add_stmts(table, ncols);
    1101          304 :     if (rc != DBRES_OK) goto abort_add_table;
    1102              :     
    1103              :     // a table with only pk(s) is totally legal
    1104          304 :     if (ncols > 0) {
    1105          265 :         table->col_name = (char **)cloudsync_memory_zeroalloc((uint64_t)(sizeof(char *) * ncols));
    1106          265 :         if (!table->col_name) goto abort_add_table;
    1107              : 
    1108          265 :         table->col_id = (int *)cloudsync_memory_zeroalloc((uint64_t)(sizeof(int) * ncols));
    1109          265 :         if (!table->col_id) goto abort_add_table;
    1110              : 
    1111          265 :         table->col_merge_stmt = (dbvm_t **)cloudsync_memory_zeroalloc((uint64_t)(sizeof(void *) * ncols));
    1112          265 :         if (!table->col_merge_stmt) goto abort_add_table;
    1113              : 
    1114          265 :         table->col_value_stmt = (dbvm_t **)cloudsync_memory_zeroalloc((uint64_t)(sizeof(void *) * ncols));
    1115          265 :         if (!table->col_value_stmt) goto abort_add_table;
    1116              : 
    1117          265 :         table->col_algo = (col_algo_t *)cloudsync_memory_zeroalloc((uint64_t)(sizeof(col_algo_t) * ncols));
    1118          265 :         if (!table->col_algo) goto abort_add_table;
    1119              : 
    1120          265 :         table->col_delimiter = (char **)cloudsync_memory_zeroalloc((uint64_t)(sizeof(char *) * ncols));
    1121          265 :         if (!table->col_delimiter) goto abort_add_table;
    1122              : 
    1123              :         // Pass empty string when schema is NULL; SQL will fall back to current_schema()
    1124          265 :         const char *schema = table->schema ? table->schema : "";
    1125          530 :         char *sql = cloudsync_memory_mprintf(SQL_PRAGMA_TABLEINFO_LIST_NONPK_NAME_CID,
    1126          265 :                                               table_name, schema, table_name, schema);
    1127          265 :         if (!sql) goto abort_add_table;
    1128          265 :         rc = database_exec_callback(data, sql, table_add_to_context_cb, (void *)table);
    1129          265 :         cloudsync_memory_free(sql);
    1130          265 :         if (rc == DBRES_ABORT) goto abort_add_table;
    1131          264 :     }
    1132              :     
    1133              :     // append newly created table
    1134          303 :     data->tables[data->tables_count++] = table;
    1135          303 :     return true;
    1136              :     
    1137              : abort_add_table:
    1138            1 :     table_free(table);
    1139            1 :     return false;
    1140          309 : }
    1141              : 
    1142            0 : dbvm_t *cloudsync_colvalue_stmt (cloudsync_context *data, const char *tbl_name, bool *persistent) {
    1143            0 :     dbvm_t *vm = NULL;
    1144            0 :     *persistent = false;
    1145              : 
    1146            0 :     cloudsync_table_context *table = table_lookup(data, tbl_name);
    1147            0 :     if (table) {
    1148            0 :         char *col_name = NULL;
    1149            0 :         if (table->ncols > 0) {
    1150            0 :             col_name = table->col_name[0];
    1151              :             // retrieve col_value precompiled statement
    1152            0 :             vm = table_column_lookup(table, col_name, false, NULL);
    1153            0 :             *persistent = true;
    1154            0 :         } else {
    1155            0 :             char *sql = table_build_value_sql(table, "*");
    1156            0 :             databasevm_prepare(data, sql, (void **)&vm, 0);
    1157            0 :             cloudsync_memory_free(sql);
    1158            0 :             *persistent = false;
    1159              :         }
    1160            0 :     }
    1161              :     
    1162            0 :     return vm;
    1163              : }
    1164              : 
    1165         8148 : bool table_enabled (cloudsync_table_context *table) {
    1166         8148 :     return table->enabled;
    1167              : }
    1168              : 
    1169            6 : void table_set_enabled (cloudsync_table_context *table, bool value) {
    1170            6 :     table->enabled = value;
    1171            6 : }
    1172              : 
    1173        23933 : int table_count_cols (cloudsync_table_context *table) {
    1174        23933 :     return table->ncols;
    1175              : }
    1176              : 
    1177         8049 : int table_count_pks (cloudsync_table_context *table) {
    1178         8049 :     return table->npks;
    1179              : }
    1180              : 
    1181        36058 : const char *table_colname (cloudsync_table_context *table, int index) {
    1182        36058 :     return table->col_name[index];
    1183              : }
    1184              : 
    1185         4856 : bool table_pk_exists (cloudsync_table_context *table, const char *value, size_t len) {
    1186              :     // check if a row with the same primary key already exists
    1187              :     // if so, this means the row might have been previously deleted (sentinel)
    1188         4856 :     return (dbvm_count(table->meta_pkexists_stmt, value, len, DBTYPE_BLOB) > 0);
    1189              : }
    1190              : 
    1191            0 : char **table_pknames (cloudsync_table_context *table) {
    1192            0 :     return table->pk_name;
    1193              : }
    1194              : 
    1195           23 : void table_set_pknames (cloudsync_table_context *table, char **pknames) {
    1196           23 :     table_pknames_free(table->pk_name, table->npks);
    1197           23 :     table->pk_name = pknames;
    1198           23 : }
    1199              : 
    1200        51495 : bool table_algo_isgos (cloudsync_table_context *table) {
    1201        51495 :     return (table->algo == table_algo_crdt_gos);
    1202              : }
    1203              : 
    1204            0 : const char *table_schema (cloudsync_table_context *table) {
    1205            0 :     return table->schema;
    1206              : }
    1207              : 
    1208              : // MARK: - Merge Insert -
    1209              : 
    1210        48345 : int64_t merge_get_local_cl (cloudsync_table_context *table, const char *pk, int pklen) {
    1211        48345 :     dbvm_t *vm = table->meta_local_cl_stmt;
    1212        48345 :     int64_t result = -1;
    1213              :     
    1214        48345 :     int rc = databasevm_bind_blob(vm, 1, (const void *)pk, pklen);
    1215        48345 :     if (rc != DBRES_OK) goto cleanup;
    1216              :     
    1217        48345 :     rc = databasevm_bind_blob(vm, 2, (const void *)pk, pklen);
    1218        48345 :     if (rc != DBRES_OK) goto cleanup;
    1219              :     
    1220        48345 :     rc = databasevm_step(vm);
    1221        48345 :     if (rc == DBRES_ROW) result = database_column_int(vm, 0);
    1222            0 :     else if (rc == DBRES_DONE) result = 0;
    1223              :     
    1224              : cleanup:
    1225        48345 :     if (result == -1) cloudsync_set_dberror(table->context);
    1226        48345 :     dbvm_reset(vm);
    1227        48345 :     return result;
    1228              : }
    1229              : 
    1230        47333 : int merge_get_col_version (cloudsync_table_context *table, const char *col_name, const char *pk, int pklen, int64_t *version) {
    1231        47333 :     dbvm_t *vm = table->meta_col_version_stmt;
    1232              :     
    1233        47333 :     int rc = databasevm_bind_blob(vm, 1, (const void *)pk, pklen);
    1234        47333 :     if (rc != DBRES_OK) goto cleanup;
    1235              :     
    1236        47333 :     rc = databasevm_bind_text(vm, 2, col_name, -1);
    1237        47333 :     if (rc != DBRES_OK) goto cleanup;
    1238              :     
    1239        47333 :     rc = databasevm_step(vm);
    1240        72469 :     if (rc == DBRES_ROW) {
    1241        25136 :         *version = database_column_int(vm, 0);
    1242        25136 :         rc = DBRES_OK;
    1243        25136 :     }
    1244              :     
    1245              : cleanup:
    1246        47333 :     if ((rc != DBRES_OK) && (rc != DBRES_DONE)) cloudsync_set_dberror(table->context);
    1247        47333 :     dbvm_reset(vm);
    1248        47333 :     return rc;
    1249              : }
    1250              : 
    1251        27235 : int merge_set_winner_clock (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pk_len, const char *colname, int64_t col_version, int64_t db_version, const char *site_id, int site_len, int64_t seq, int64_t *rowid) {
    1252              :     
    1253              :     // get/set site_id
    1254        27235 :     dbvm_t *vm = data->getset_siteid_stmt;
    1255        27235 :     int rc = databasevm_bind_blob(vm, 1, (const void *)site_id, site_len);
    1256        27235 :     if (rc != DBRES_OK) goto cleanup_merge;
    1257              :     
    1258        27235 :     rc = databasevm_step(vm);
    1259        27235 :     if (rc != DBRES_ROW) goto cleanup_merge;
    1260              :     
    1261        27235 :     int64_t ord = database_column_int(vm, 0);
    1262        27235 :     dbvm_reset(vm);
    1263              :     
    1264        27235 :     vm = table->meta_winner_clock_stmt;
    1265        27235 :     rc = databasevm_bind_blob(vm, 1, (const void *)pk, pk_len);
    1266        27235 :     if (rc != DBRES_OK) goto cleanup_merge;
    1267              :     
    1268        27235 :     rc = databasevm_bind_text(vm, 2, (colname) ? colname : CLOUDSYNC_TOMBSTONE_VALUE, -1);
    1269        27235 :     if (rc != DBRES_OK) goto cleanup_merge;
    1270              :     
    1271        27235 :     rc = databasevm_bind_int(vm, 3, col_version);
    1272        27235 :     if (rc != DBRES_OK) goto cleanup_merge;
    1273              :     
    1274        27235 :     rc = databasevm_bind_int(vm, 4, db_version);
    1275        27235 :     if (rc != DBRES_OK) goto cleanup_merge;
    1276              :     
    1277        27235 :     rc = databasevm_bind_int(vm, 5, seq);
    1278        27235 :     if (rc != DBRES_OK) goto cleanup_merge;
    1279              :     
    1280        27235 :     rc = databasevm_bind_int(vm, 6, ord);
    1281        27235 :     if (rc != DBRES_OK) goto cleanup_merge;
    1282              :     
    1283        27235 :     rc = databasevm_step(vm);
    1284        54470 :     if (rc == DBRES_ROW) {
    1285        27235 :         *rowid = database_column_int(vm, 0);
    1286        27235 :         rc = DBRES_OK;
    1287        27235 :     }
    1288              :     
    1289              : cleanup_merge:
    1290        27235 :     if (rc != DBRES_OK) cloudsync_set_dberror(data);
    1291        27235 :     dbvm_reset(vm);
    1292        27235 :     return rc;
    1293              : }
    1294              : 
    1295              : // MARK: - Deferred column-batch merge functions -
    1296              : 
    1297        22939 : static int merge_pending_add (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pklen, const char *col_name, dbvalue_t *col_value, int64_t col_version, int64_t db_version, const char *site_id, int site_len, int64_t seq) {
    1298        22939 :     merge_pending_batch *batch = data->pending_batch;
    1299              : 
    1300              :     // Store table and PK on first entry
    1301        22939 :     if (batch->table == NULL) {
    1302         8760 :         batch->table = table;
    1303         8760 :         batch->pk = (char *)cloudsync_memory_alloc(pklen);
    1304         8760 :         if (!batch->pk) return cloudsync_set_error(data, "merge_pending_add: out of memory for pk", DBRES_NOMEM);
    1305         8760 :         memcpy(batch->pk, pk, pklen);
    1306         8760 :         batch->pk_len = pklen;
    1307         8760 :     }
    1308              : 
    1309              :     // Ensure capacity
    1310        22939 :     if (batch->count >= batch->capacity) {
    1311          537 :         int new_cap = batch->capacity ? batch->capacity * 2 : 8;
    1312          537 :         merge_pending_entry *new_entries = (merge_pending_entry *)cloudsync_memory_realloc(batch->entries, new_cap * sizeof(merge_pending_entry));
    1313          537 :         if (!new_entries) return cloudsync_set_error(data, "merge_pending_add: out of memory for entries", DBRES_NOMEM);
    1314          537 :         batch->entries = new_entries;
    1315          537 :         batch->capacity = new_cap;
    1316          537 :     }
    1317              : 
    1318              :     // Resolve col_name to a stable pointer from the table context
    1319              :     // (the incoming col_name may point to VM-owned memory that gets freed on reset)
    1320        22939 :     int col_idx = -1;
    1321        22939 :     table_column_lookup(table, col_name, true, &col_idx);
    1322        22939 :     const char *stable_col_name = (col_idx >= 0) ? table_colname(table, col_idx) : NULL;
    1323        22939 :     if (!stable_col_name) return cloudsync_set_error(data, "merge_pending_add: column not found in table context", DBRES_ERROR);
    1324              : 
    1325        22939 :     merge_pending_entry *e = &batch->entries[batch->count];
    1326        22939 :     e->col_name = stable_col_name;
    1327        22939 :     e->col_value = col_value ? (dbvalue_t *)database_value_dup(col_value) : NULL;
    1328        22939 :     e->col_version = col_version;
    1329        22939 :     e->db_version = db_version;
    1330        22939 :     e->site_id_len = (site_len <= (int)sizeof(e->site_id)) ? site_len : (int)sizeof(e->site_id);
    1331        22939 :     memcpy(e->site_id, site_id, e->site_id_len);
    1332        22939 :     e->seq = seq;
    1333              : 
    1334        22939 :     batch->count++;
    1335        22939 :     return DBRES_OK;
    1336        22939 : }
    1337              : 
    1338        20684 : static void merge_pending_free_entries (merge_pending_batch *batch) {
    1339        20684 :     if (batch->entries) {
    1340        38265 :         for (int i = 0; i < batch->count; i++) {
    1341        22939 :             if (batch->entries[i].col_value) {
    1342        22939 :                 database_value_free(batch->entries[i].col_value);
    1343        22939 :                 batch->entries[i].col_value = NULL;
    1344        22939 :             }
    1345        22939 :         }
    1346        15326 :     }
    1347        20684 :     if (batch->pk) {
    1348         8792 :         cloudsync_memory_free(batch->pk);
    1349         8792 :         batch->pk = NULL;
    1350         8792 :     }
    1351        20684 :     batch->table = NULL;
    1352        20684 :     batch->pk_len = 0;
    1353        20684 :     batch->cl = 0;
    1354        20684 :     batch->sentinel_pending = false;
    1355        20684 :     batch->row_exists = false;
    1356        20684 :     batch->count = 0;
    1357        20684 : }
    1358              : 
    1359        20673 : static int merge_flush_pending (cloudsync_context *data) {
    1360        20673 :     merge_pending_batch *batch = data->pending_batch;
    1361        20673 :     if (!batch) return DBRES_OK;
    1362              : 
    1363        20673 :     int rc = DBRES_OK;
    1364        20673 :     bool flush_savepoint = false;
    1365              : 
    1366              :     // Nothing to write — handle sentinel-only case or skip
    1367        20673 :     if (batch->count == 0 && !(batch->sentinel_pending && batch->table)) {
    1368        11881 :         goto cleanup;
    1369              :     }
    1370              : 
    1371              :     // Wrap database operations in a savepoint so that on failure (e.g. RLS
    1372              :     // denial) the rollback properly releases all executor resources (open
    1373              :     // relations, snapshots, plan cache) acquired during the failed statement.
    1374         8792 :     flush_savepoint = (database_begin_savepoint(data, "merge_flush") == DBRES_OK);
    1375              : 
    1376         8792 :     if (batch->count == 0) {
    1377              :         // Sentinel with no winning columns (PK-only row)
    1378           30 :         dbvm_t *vm = batch->table->real_merge_sentinel_stmt;
    1379           30 :         rc = pk_decode_prikey(batch->pk, (size_t)batch->pk_len, pk_decode_bind_callback, vm);
    1380           30 :         if (rc < 0) {
    1381            0 :             cloudsync_set_dberror(data);
    1382            0 :             dbvm_reset(vm);
    1383            0 :             goto cleanup;
    1384              :         }
    1385           30 :         SYNCBIT_SET(data);
    1386           30 :         rc = databasevm_step(vm);
    1387           30 :         dbvm_reset(vm);
    1388           30 :         SYNCBIT_RESET(data);
    1389           30 :         if (rc == DBRES_DONE) rc = DBRES_OK;
    1390           30 :         if (rc != DBRES_OK) {
    1391            0 :             cloudsync_set_dberror(data);
    1392            0 :             goto cleanup;
    1393              :         }
    1394           30 :         goto cleanup;
    1395              :     }
    1396              : 
    1397              :     // Check if cached prepared statement can be reused
    1398         8762 :     cloudsync_table_context *table = batch->table;
    1399         8762 :     dbvm_t *vm = NULL;
    1400         8762 :     bool cache_hit = false;
    1401              : 
    1402        16680 :     if (batch->cached_vm &&
    1403         8225 :         batch->cached_row_exists == batch->row_exists &&
    1404         7918 :         batch->cached_col_count == batch->count) {
    1405         7889 :         cache_hit = true;
    1406        29116 :         for (int i = 0; i < batch->count; i++) {
    1407        21253 :             if (batch->cached_col_names[i] != batch->entries[i].col_name) {
    1408           26 :                 cache_hit = false;
    1409           26 :                 break;
    1410              :             }
    1411        21227 :         }
    1412         7889 :     }
    1413              : 
    1414         8762 :     if (cache_hit) {
    1415         7863 :         vm = batch->cached_vm;
    1416         7863 :         dbvm_reset(vm);
    1417         7863 :     } else {
    1418              :         // Invalidate old cache
    1419          899 :         if (batch->cached_vm) {
    1420          362 :             databasevm_finalize(batch->cached_vm);
    1421          362 :             batch->cached_vm = NULL;
    1422          362 :         }
    1423              : 
    1424              :         // Build multi-column SQL
    1425          899 :         const char **colnames = (const char **)cloudsync_memory_alloc(batch->count * sizeof(const char *));
    1426          899 :         if (!colnames) {
    1427            0 :             rc = cloudsync_set_error(data, "merge_flush_pending: out of memory", DBRES_NOMEM);
    1428            0 :             goto cleanup;
    1429              :         }
    1430         2611 :         for (int i = 0; i < batch->count; i++) {
    1431         1712 :             colnames[i] = batch->entries[i].col_name;
    1432         1712 :         }
    1433              : 
    1434          899 :         char *sql = batch->row_exists
    1435          440 :             ? sql_build_update_pk_and_multi_cols(data, table->name, colnames, batch->count, table->schema)
    1436          459 :             : sql_build_upsert_pk_and_multi_cols(data, table->name, colnames, batch->count, table->schema);
    1437          899 :         cloudsync_memory_free(colnames);
    1438              : 
    1439          899 :         if (!sql) {
    1440            0 :             rc = cloudsync_set_error(data, "merge_flush_pending: unable to build multi-column upsert SQL", DBRES_ERROR);
    1441            0 :             goto cleanup;
    1442              :         }
    1443              : 
    1444          899 :         rc = databasevm_prepare(data, sql, &vm, 0);
    1445          899 :         cloudsync_memory_free(sql);
    1446          899 :         if (rc != DBRES_OK) {
    1447            0 :             rc = cloudsync_set_error(data, "merge_flush_pending: unable to prepare statement", rc);
    1448            0 :             goto cleanup;
    1449              :         }
    1450              : 
    1451              :         // Update cache
    1452          899 :         batch->cached_vm = vm;
    1453          899 :         batch->cached_row_exists = batch->row_exists;
    1454          899 :         batch->cached_col_count = batch->count;
    1455              :         // Reallocate cached_col_names if needed
    1456          899 :         if (batch->cached_col_count > 0) {
    1457          899 :             const char **new_names = (const char **)cloudsync_memory_realloc(
    1458          899 :                 batch->cached_col_names, batch->count * sizeof(const char *));
    1459          899 :             if (new_names) {
    1460         2611 :                 for (int i = 0; i < batch->count; i++) {
    1461         1712 :                     new_names[i] = batch->entries[i].col_name;
    1462         1712 :                 }
    1463          899 :                 batch->cached_col_names = new_names;
    1464          899 :             }
    1465          899 :         }
    1466              :     }
    1467              : 
    1468              :     // Bind PKs (positions 1..npks)
    1469         8762 :     int npks = pk_decode_prikey(batch->pk, (size_t)batch->pk_len, pk_decode_bind_callback, vm);
    1470         8762 :     if (npks < 0) {
    1471            0 :         cloudsync_set_dberror(data);
    1472            0 :         dbvm_reset(vm);
    1473            0 :         rc = DBRES_ERROR;
    1474            0 :         goto cleanup;
    1475              :     }
    1476              : 
    1477              :     // Bind column values (positions npks+1..npks+count)
    1478        31701 :     for (int i = 0; i < batch->count; i++) {
    1479        22939 :         merge_pending_entry *e = &batch->entries[i];
    1480        22939 :         int bind_idx = npks + 1 + i;
    1481        22939 :         if (e->col_value) {
    1482        22939 :             rc = databasevm_bind_value(vm, bind_idx, e->col_value);
    1483        22939 :         } else {
    1484            0 :             rc = databasevm_bind_null(vm, bind_idx);
    1485              :         }
    1486        22939 :         if (rc != DBRES_OK) {
    1487            0 :             cloudsync_set_dberror(data);
    1488            0 :             dbvm_reset(vm);
    1489            0 :             goto cleanup;
    1490              :         }
    1491        22939 :     }
    1492              : 
    1493              :     // Execute with SYNCBIT and GOS handling
    1494         8762 :     if (table->algo == table_algo_crdt_gos) table->enabled = 0;
    1495         8762 :     SYNCBIT_SET(data);
    1496         8762 :     rc = databasevm_step(vm);
    1497         8762 :     dbvm_reset(vm);
    1498         8762 :     SYNCBIT_RESET(data);
    1499         8762 :     if (table->algo == table_algo_crdt_gos) table->enabled = 1;
    1500              : 
    1501         8762 :     if (rc != DBRES_DONE) {
    1502            3 :         cloudsync_set_dberror(data);
    1503            3 :         goto cleanup;
    1504              :     }
    1505         8759 :     rc = DBRES_OK;
    1506              : 
    1507              :     // Call merge_set_winner_clock for each buffered entry
    1508         8759 :     int64_t rowid = 0;
    1509        31690 :     for (int i = 0; i < batch->count; i++) {
    1510        22931 :         merge_pending_entry *e = &batch->entries[i];
    1511        45862 :         int clock_rc = merge_set_winner_clock(data, table, batch->pk, batch->pk_len,
    1512        22931 :                                                e->col_name, e->col_version, e->db_version,
    1513        22931 :                                                (const char *)e->site_id, e->site_id_len,
    1514        22931 :                                                e->seq, &rowid);
    1515        22931 :         if (clock_rc != DBRES_OK) {
    1516            0 :             rc = clock_rc;
    1517            0 :             goto cleanup;
    1518              :         }
    1519        31690 :     }
    1520              : 
    1521              : cleanup:
    1522        20673 :     merge_pending_free_entries(batch);
    1523        20673 :     if (flush_savepoint) {
    1524         8792 :         if (rc == DBRES_OK) database_commit_savepoint(data, "merge_flush");
    1525            3 :         else database_rollback_savepoint(data, "merge_flush");
    1526         8792 :     }
    1527        20673 :     return rc;
    1528        20673 : }
    1529              : 
    1530         3667 : int merge_insert_col (cloudsync_context *data, cloudsync_table_context *table, const void *pk, int pklen, const char *col_name, dbvalue_t *col_value, int64_t col_version, int64_t db_version, const char *site_id, int site_len, int64_t seq, int64_t *rowid) {
    1531              :     int index;
    1532         3667 :     dbvm_t *vm = table_column_lookup(table, col_name, true, &index);
    1533         3667 :     if (vm == NULL) return cloudsync_set_error(data, "Unable to retrieve column merge precompiled statement in merge_insert_col", DBRES_MISUSE);
    1534              :     
    1535              :     // INSERT INTO table (pk1, pk2, col_name) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET col_name=?;"
    1536              :     
    1537              :     // bind primary key(s)
    1538         3667 :     int rc = pk_decode_prikey((char *)pk, (size_t)pklen, pk_decode_bind_callback, vm);
    1539         3667 :     if (rc < 0) {
    1540            0 :         cloudsync_set_dberror(data);
    1541            0 :         dbvm_reset(vm);
    1542            0 :         return rc;
    1543              :     }
    1544              :     
    1545              :     // bind value (always bind all expected parameters for correct prepared statement handling)
    1546         3667 :     if (col_value) {
    1547         3667 :         rc = databasevm_bind_value(vm, table->npks+1, col_value);
    1548         3667 :         if (rc == DBRES_OK) rc = databasevm_bind_value(vm, table->npks+2, col_value);
    1549         3667 :     } else {
    1550            0 :         rc = databasevm_bind_null(vm, table->npks+1);
    1551            0 :         if (rc == DBRES_OK) rc = databasevm_bind_null(vm, table->npks+2);
    1552              :     }
    1553         3667 :     if (rc != DBRES_OK) {
    1554            0 :         cloudsync_set_dberror(data);
    1555            0 :         dbvm_reset(vm);
    1556            0 :         return rc;
    1557              :     }
    1558              : 
    1559              :     // perform real operation and disable triggers
    1560              :     
    1561              :     // in case of GOS we reused the table->col_merge_stmt statement
    1562              :     // which looks like: INSERT INTO table (pk1, pk2, col_name) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET col_name=?;"
    1563              :     // but the UPDATE in the CONFLICT statement would return SQLITE_CONSTRAINT because the trigger raises the error
    1564              :     // the trick is to disable that trigger before executing the statement
    1565         3667 :     if (table->algo == table_algo_crdt_gos) table->enabled = 0;
    1566         3667 :     SYNCBIT_SET(data);
    1567         3667 :     rc = databasevm_step(vm);
    1568              :     DEBUG_MERGE("merge_insert(%02x%02x): %s (%d)", data->site_id[UUID_LEN-2], data->site_id[UUID_LEN-1], databasevm_sql(vm), rc);
    1569         3667 :     dbvm_reset(vm);
    1570         3667 :     SYNCBIT_RESET(data);
    1571         3667 :     if (table->algo == table_algo_crdt_gos) table->enabled = 1;
    1572              :     
    1573         3667 :     if (rc != DBRES_DONE) {
    1574            0 :         cloudsync_set_dberror(data);
    1575            0 :         return rc;
    1576              :     }
    1577              :     
    1578         3667 :     return merge_set_winner_clock(data, table, pk, pklen, col_name, col_version, db_version, site_id, site_len, seq, rowid);
    1579         3667 : }
    1580              : 
    1581          167 : int merge_delete (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pklen, const char *colname, int64_t cl, int64_t db_version, const char *site_id, int site_len, int64_t seq, int64_t *rowid) {
    1582          167 :     int rc = DBRES_OK;
    1583              :     
    1584              :     // reset return value
    1585          167 :     *rowid = 0;
    1586              :     
    1587              :     // bind pk
    1588          167 :     dbvm_t *vm = table->real_merge_delete_stmt;
    1589          167 :     rc = pk_decode_prikey((char *)pk, (size_t)pklen, pk_decode_bind_callback, vm);
    1590          167 :     if (rc < 0) {
    1591            0 :         rc = cloudsync_set_dberror(data);
    1592            0 :         dbvm_reset(vm);
    1593            0 :         return rc;
    1594              :     }
    1595              :     
    1596              :     // perform real operation and disable triggers
    1597          167 :     SYNCBIT_SET(data);
    1598          167 :     rc = databasevm_step(vm);
    1599              :     DEBUG_MERGE("merge_delete(%02x%02x): %s (%d)", data->site_id[UUID_LEN-2], data->site_id[UUID_LEN-1], databasevm_sql(vm), rc);
    1600          167 :     dbvm_reset(vm);
    1601          167 :     SYNCBIT_RESET(data);
    1602          167 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    1603          167 :     if (rc != DBRES_OK) {
    1604            0 :         cloudsync_set_dberror(data);
    1605            0 :         return rc;
    1606              :     }
    1607              :     
    1608          167 :     rc = merge_set_winner_clock(data, table, pk, pklen, colname, cl, db_version, site_id, site_len, seq, rowid);
    1609          167 :     if (rc != DBRES_OK) return rc;
    1610              :     
    1611              :     // drop clocks _after_ setting the winner clock so we don't lose track of the max db_version!!
    1612              :     // this must never come before `set_winner_clock`
    1613          167 :     vm = table->meta_merge_delete_drop;
    1614          167 :     rc = databasevm_bind_blob(vm, 1, (const void *)pk, pklen);
    1615          167 :     if (rc == DBRES_OK) rc = databasevm_step(vm);
    1616          167 :     dbvm_reset(vm);
    1617              :     
    1618          167 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    1619          167 :     if (rc != DBRES_OK) cloudsync_set_dberror(data);
    1620          167 :     return rc;
    1621          167 : }
    1622              : 
    1623           58 : int merge_zeroclock_on_resurrect(cloudsync_table_context *table, int64_t db_version, const char *pk, int pklen) {
    1624           58 :     dbvm_t *vm = table->meta_zero_clock_stmt;
    1625              :     
    1626           58 :     int rc = databasevm_bind_int(vm, 1, db_version);
    1627           58 :     if (rc != DBRES_OK) goto cleanup;
    1628              :     
    1629           58 :     rc = databasevm_bind_blob(vm, 2, (const void *)pk, pklen);
    1630           58 :     if (rc != DBRES_OK) goto cleanup;
    1631              :     
    1632           58 :     rc = databasevm_step(vm);
    1633           58 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    1634              :     
    1635              : cleanup:
    1636           58 :     if (rc != DBRES_OK) cloudsync_set_dberror(table->context);
    1637           58 :     dbvm_reset(vm);
    1638           58 :     return rc;
    1639              : }
    1640              : 
    1641              : // executed only if insert_cl == local_cl
    1642        47333 : int merge_did_cid_win (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pklen, dbvalue_t *insert_value, const char *site_id, int site_len, const char *col_name, int64_t col_version, bool *didwin_flag) {
    1643              :     
    1644        47333 :     if (col_name == NULL) col_name = CLOUDSYNC_TOMBSTONE_VALUE;
    1645              :     
    1646              :     int64_t local_version;
    1647        47333 :     int rc = merge_get_col_version(table, col_name, pk, pklen, &local_version);
    1648        47333 :     if (rc == DBRES_DONE) {
    1649              :         // no rows returned, the incoming change wins if there's nothing there locally
    1650        22197 :         *didwin_flag = true;
    1651        22197 :         return DBRES_OK;
    1652              :     }
    1653        25136 :     if (rc != DBRES_OK) return rc;
    1654              :     
    1655              :     // rc == DBRES_OK, means that a row with a version exists
    1656        25136 :     if (local_version != col_version) {
    1657         2005 :         if (col_version > local_version) {*didwin_flag = true; return DBRES_OK;}
    1658          746 :         if (col_version < local_version) {*didwin_flag = false; return DBRES_OK;}
    1659            0 :     }
    1660              :     
    1661              :     // rc == DBRES_ROW and col_version == local_version, need to compare values
    1662              : 
    1663              :     // retrieve col_value precompiled statement
    1664        23131 :     bool is_block_col = block_is_block_colname(col_name) && table_has_block_cols(table);
    1665              :     dbvm_t *vm;
    1666        23131 :     if (is_block_col) {
    1667              :         // Block column: read value from blocks table (pk + col_name bindings)
    1668           43 :         vm = table_block_value_read_stmt(table);
    1669           43 :         if (!vm) return cloudsync_set_error(data, "Unable to retrieve block value read statement in merge_did_cid_win", DBRES_ERROR);
    1670           43 :         rc = databasevm_bind_blob(vm, 1, (const void *)pk, pklen);
    1671           43 :         if (rc != DBRES_OK) { dbvm_reset(vm); return cloudsync_set_dberror(data); }
    1672           43 :         rc = databasevm_bind_text(vm, 2, col_name, -1);
    1673           43 :         if (rc != DBRES_OK) { dbvm_reset(vm); return cloudsync_set_dberror(data); }
    1674           43 :     } else {
    1675        23088 :         vm = table_column_lookup(table, col_name, false, NULL);
    1676        23088 :         if (!vm) return cloudsync_set_error(data, "Unable to retrieve column value precompiled statement in merge_did_cid_win", DBRES_ERROR);
    1677              : 
    1678              :         // bind primary key values
    1679        23088 :         rc = pk_decode_prikey((char *)pk, (size_t)pklen, pk_decode_bind_callback, (void *)vm);
    1680        23088 :         if (rc < 0) {
    1681            0 :             rc = cloudsync_set_dberror(data);
    1682            0 :             dbvm_reset(vm);
    1683            0 :             return rc;
    1684              :         }
    1685              :     }
    1686              :         
    1687              :     // execute vm
    1688              :     dbvalue_t *local_value;
    1689        23131 :     rc = databasevm_step(vm);
    1690        23131 :     if (rc == DBRES_DONE) {
    1691              :         // meta entry exists but the actual value is missing
    1692              :         // we should allow the value_compare function to make a decision
    1693              :         // value_compare has been modified to handle the case where lvalue is NULL
    1694            2 :         local_value = NULL;
    1695            2 :         rc = DBRES_OK;
    1696        23131 :     } else if (rc == DBRES_ROW) {
    1697        23129 :         local_value = database_column_value(vm, 0);
    1698        23129 :         rc = DBRES_OK;
    1699        23129 :     } else {
    1700            0 :         goto cleanup;
    1701              :     }
    1702              :     
    1703              :     // compare values
    1704        23131 :     int ret = dbutils_value_compare(insert_value, local_value);
    1705              :     // reset after compare, otherwise local value would be deallocated
    1706        23131 :     dbvm_reset(vm);
    1707        23131 :     vm = NULL;
    1708              :     
    1709        23131 :     bool compare_site_id = (ret == 0 && data->merge_equal_values == true);
    1710        23131 :     if (!compare_site_id) {
    1711        23131 :         *didwin_flag = (ret > 0);
    1712        23131 :         goto cleanup;
    1713              :     }
    1714              :     
    1715              :     // values are the same and merge_equal_values is true
    1716            0 :     vm = table->meta_site_id_stmt;
    1717            0 :     rc = databasevm_bind_blob(vm, 1, (const void *)pk, pklen);
    1718            0 :     if (rc != DBRES_OK) goto cleanup;
    1719              :     
    1720            0 :     rc = databasevm_bind_text(vm, 2, col_name, -1);
    1721            0 :     if (rc != DBRES_OK) goto cleanup;
    1722              :     
    1723            0 :     rc = databasevm_step(vm);
    1724            0 :     if (rc == DBRES_ROW) {
    1725            0 :         const void *local_site_id = database_column_blob(vm, 0, NULL);
    1726            0 :         if (!local_site_id) {
    1727            0 :             dbvm_reset(vm);
    1728            0 :             return cloudsync_set_error(data, "NULL site_id in cloudsync table, table is probably corrupted", DBRES_ERROR);
    1729              :         }
    1730            0 :         ret = memcmp(site_id, local_site_id, site_len);
    1731            0 :         *didwin_flag = (ret > 0);
    1732            0 :         dbvm_reset(vm);
    1733            0 :         return DBRES_OK;
    1734              :     }
    1735              :     
    1736              :     // handle error condition here
    1737            0 :     dbvm_reset(vm);
    1738            0 :     return cloudsync_set_error(data, "Unable to find site_id for previous change, cloudsync table is probably corrupted", DBRES_ERROR);
    1739              :     
    1740              : cleanup:
    1741        23131 :     if (rc != DBRES_OK) cloudsync_set_dberror(data);
    1742        23131 :     dbvm_reset(vm);
    1743        23131 :     return rc;
    1744        47333 : }
    1745              : 
    1746           58 : int merge_sentinel_only_insert (cloudsync_context *data, cloudsync_table_context *table, const char *pk, int pklen, int64_t cl, int64_t db_version, const char *site_id, int site_len, int64_t seq, int64_t *rowid) {
    1747              : 
    1748              :     // reset return value
    1749           58 :     *rowid = 0;
    1750              : 
    1751           58 :     if (data->pending_batch == NULL) {
    1752              :         // Immediate mode: execute base table INSERT
    1753            0 :         dbvm_t *vm = table->real_merge_sentinel_stmt;
    1754            0 :         int rc = pk_decode_prikey((char *)pk, (size_t)pklen, pk_decode_bind_callback, vm);
    1755            0 :         if (rc < 0) {
    1756            0 :             rc = cloudsync_set_dberror(data);
    1757            0 :             dbvm_reset(vm);
    1758            0 :             return rc;
    1759              :         }
    1760              : 
    1761            0 :         SYNCBIT_SET(data);
    1762            0 :         rc = databasevm_step(vm);
    1763            0 :         dbvm_reset(vm);
    1764            0 :         SYNCBIT_RESET(data);
    1765            0 :         if (rc == DBRES_DONE) rc = DBRES_OK;
    1766            0 :         if (rc != DBRES_OK) {
    1767            0 :             cloudsync_set_dberror(data);
    1768            0 :             return rc;
    1769              :         }
    1770            0 :     } else {
    1771              :         // Batch mode: skip base table INSERT, the batch flush will create the row
    1772           58 :         merge_pending_batch *batch = data->pending_batch;
    1773           58 :         batch->sentinel_pending = true;
    1774           58 :         if (batch->table == NULL) {
    1775           32 :             batch->table = table;
    1776           32 :             batch->pk = (char *)cloudsync_memory_alloc(pklen);
    1777           32 :             if (!batch->pk) return cloudsync_set_error(data, "merge_sentinel_only_insert: out of memory for pk", DBRES_NOMEM);
    1778           32 :             memcpy(batch->pk, pk, pklen);
    1779           32 :             batch->pk_len = pklen;
    1780           32 :         }
    1781              :     }
    1782              : 
    1783              :     // Metadata operations always execute regardless of batch mode
    1784           58 :     int rc = merge_zeroclock_on_resurrect(table, db_version, pk, pklen);
    1785           58 :     if (rc != DBRES_OK) return rc;
    1786              : 
    1787           58 :     return merge_set_winner_clock(data, table, pk, pklen, NULL, cl, db_version, site_id, site_len, seq, rowid);
    1788           58 : }
    1789              : 
    1790              : // MARK: - Block-level merge helpers -
    1791              : 
    1792              : // Store a block value in the blocks table
    1793          379 : static int block_store_value (cloudsync_context *data, cloudsync_table_context *table, const void *pk, int pklen, const char *block_colname, dbvalue_t *col_value) {
    1794          379 :     dbvm_t *vm = table->block_value_write_stmt;
    1795          379 :     if (!vm) return cloudsync_set_error(data, "block_store_value: blocks table not initialized", DBRES_MISUSE);
    1796              : 
    1797          379 :     int rc = databasevm_bind_blob(vm, 1, pk, pklen);
    1798          379 :     if (rc != DBRES_OK) goto cleanup;
    1799          379 :     rc = databasevm_bind_text(vm, 2, block_colname, -1);
    1800          379 :     if (rc != DBRES_OK) goto cleanup;
    1801          379 :     if (col_value) {
    1802          379 :         rc = databasevm_bind_value(vm, 3, col_value);
    1803          379 :     } else {
    1804            0 :         rc = databasevm_bind_null(vm, 3);
    1805              :     }
    1806          379 :     if (rc != DBRES_OK) goto cleanup;
    1807              : 
    1808          379 :     rc = databasevm_step(vm);
    1809          379 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    1810              : 
    1811              : cleanup:
    1812          379 :     if (rc != DBRES_OK) cloudsync_set_dberror(data);
    1813          379 :     databasevm_reset(vm);
    1814          379 :     return rc;
    1815          379 : }
    1816              : 
    1817              : // Delete a block value from the blocks table
    1818           74 : static int block_delete_value (cloudsync_context *data, cloudsync_table_context *table, const void *pk, int pklen, const char *block_colname) {
    1819           74 :     dbvm_t *vm = table->block_value_delete_stmt;
    1820           74 :     if (!vm) return cloudsync_set_error(data, "block_delete_value: blocks table not initialized", DBRES_MISUSE);
    1821              : 
    1822           74 :     int rc = databasevm_bind_blob(vm, 1, pk, pklen);
    1823           74 :     if (rc != DBRES_OK) goto cleanup;
    1824           74 :     rc = databasevm_bind_text(vm, 2, block_colname, -1);
    1825           74 :     if (rc != DBRES_OK) goto cleanup;
    1826              : 
    1827           74 :     rc = databasevm_step(vm);
    1828           74 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    1829              : 
    1830              : cleanup:
    1831           74 :     if (rc != DBRES_OK) cloudsync_set_dberror(data);
    1832           74 :     databasevm_reset(vm);
    1833           74 :     return rc;
    1834           74 : }
    1835              : 
    1836              : // Materialize all alive blocks for a base column into the base table
    1837          462 : int block_materialize_column (cloudsync_context *data, cloudsync_table_context *table, const void *pk, int pklen, const char *base_col_name) {
    1838          462 :     if (!table->block_list_stmt) return cloudsync_set_error(data, "block_materialize_column: blocks table not initialized", DBRES_MISUSE);
    1839              : 
    1840              :     // Find column index and delimiter
    1841          462 :     int col_idx = -1;
    1842          468 :     for (int i = 0; i < table->ncols; i++) {
    1843          468 :         if (strcasecmp(table->col_name[i], base_col_name) == 0) {
    1844          462 :             col_idx = i;
    1845          462 :             break;
    1846              :         }
    1847            6 :     }
    1848          462 :     if (col_idx < 0) return cloudsync_set_error(data, "block_materialize_column: column not found", DBRES_ERROR);
    1849          462 :     const char *delimiter = table->col_delimiter[col_idx] ? table->col_delimiter[col_idx] : BLOCK_DEFAULT_DELIMITER;
    1850              : 
    1851              :     // Build the LIKE pattern for block col_names: "base_col\x1F%"
    1852          462 :     char *like_pattern = block_build_colname(base_col_name, "%");
    1853          462 :     if (!like_pattern) return DBRES_NOMEM;
    1854              : 
    1855              :     // Query alive blocks from blocks table joined with metadata
    1856              :     // block_list_stmt: SELECT b.col_value FROM blocks b JOIN meta m
    1857              :     //   ON b.pk = m.pk AND b.col_name = m.col_name
    1858              :     //   WHERE b.pk = ? AND b.col_name LIKE ? AND m.col_version % 2 = 1
    1859              :     //   ORDER BY b.col_name
    1860          462 :     dbvm_t *vm = table->block_list_stmt;
    1861          462 :     int rc = databasevm_bind_blob(vm, 1, pk, pklen);
    1862          462 :     if (rc != DBRES_OK) { cloudsync_memory_free(like_pattern); databasevm_reset(vm); return rc; }
    1863          462 :     rc = databasevm_bind_text(vm, 2, like_pattern, -1);
    1864          462 :     if (rc != DBRES_OK) { cloudsync_memory_free(like_pattern); databasevm_reset(vm); return rc; }
    1865              :     // Bind pk again for the join condition (parameter 3)
    1866          462 :     rc = databasevm_bind_blob(vm, 3, pk, pklen);
    1867          462 :     if (rc != DBRES_OK) { cloudsync_memory_free(like_pattern); databasevm_reset(vm); return rc; }
    1868          462 :     rc = databasevm_bind_text(vm, 4, like_pattern, -1);
    1869          462 :     if (rc != DBRES_OK) { cloudsync_memory_free(like_pattern); databasevm_reset(vm); return rc; }
    1870              : 
    1871              :     // Collect block values
    1872          462 :     const char **block_values = NULL;
    1873          462 :     int block_count = 0;
    1874          462 :     int block_cap = 0;
    1875              : 
    1876        22695 :     while ((rc = databasevm_step(vm)) == DBRES_ROW) {
    1877        22233 :         const char *value = database_column_text(vm, 0);
    1878        22233 :         if (block_count >= block_cap) {
    1879         1076 :             int new_cap = block_cap ? block_cap * 2 : 16;
    1880         1076 :             const char **new_arr = (const char **)cloudsync_memory_realloc((void *)block_values, (uint64_t)(new_cap * sizeof(char *)));
    1881         1076 :             if (!new_arr) { rc = DBRES_NOMEM; break; }
    1882         1076 :             block_values = new_arr;
    1883         1076 :             block_cap = new_cap;
    1884         1076 :         }
    1885        22233 :         block_values[block_count] = value ? cloudsync_string_dup(value) : cloudsync_string_dup("");
    1886        22233 :         block_count++;
    1887              :     }
    1888          462 :     databasevm_reset(vm);
    1889          462 :     cloudsync_memory_free(like_pattern);
    1890              : 
    1891          462 :     if (rc != DBRES_DONE && rc != DBRES_OK && rc != DBRES_ROW) {
    1892              :         // Free collected values
    1893            0 :         for (int i = 0; i < block_count; i++) cloudsync_memory_free((void *)block_values[i]);
    1894            0 :         if (block_values) cloudsync_memory_free((void *)block_values);
    1895            0 :         return cloudsync_set_dberror(data);
    1896              :     }
    1897              : 
    1898              :     // Materialize text (NULL when no alive blocks)
    1899          462 :     char *text = (block_count > 0) ? block_materialize_text(block_values, block_count, delimiter) : NULL;
    1900        22695 :     for (int i = 0; i < block_count; i++) cloudsync_memory_free((void *)block_values[i]);
    1901          462 :     if (block_values) cloudsync_memory_free((void *)block_values);
    1902          462 :     if (block_count > 0 && !text) return DBRES_NOMEM;
    1903              : 
    1904              :     // Update the base table column via the col_merge_stmt (with triggers disabled)
    1905          462 :     dbvm_t *merge_vm = table->col_merge_stmt[col_idx];
    1906          462 :     if (!merge_vm) { cloudsync_memory_free(text); return DBRES_ERROR; }
    1907              : 
    1908              :     // Bind PKs
    1909          462 :     rc = pk_decode_prikey((char *)pk, (size_t)pklen, pk_decode_bind_callback, merge_vm);
    1910          462 :     if (rc < 0) { cloudsync_memory_free(text); databasevm_reset(merge_vm); return DBRES_ERROR; }
    1911              : 
    1912              :     // Bind the text value twice (INSERT value + ON CONFLICT UPDATE value)
    1913          462 :     int npks = table->npks;
    1914          462 :     if (text) {
    1915          458 :         rc = databasevm_bind_text(merge_vm, npks + 1, text, -1);
    1916          458 :         if (rc != DBRES_OK) { cloudsync_memory_free(text); databasevm_reset(merge_vm); return rc; }
    1917          458 :         rc = databasevm_bind_text(merge_vm, npks + 2, text, -1);
    1918          458 :         if (rc != DBRES_OK) { cloudsync_memory_free(text); databasevm_reset(merge_vm); return rc; }
    1919          458 :     } else {
    1920            4 :         rc = databasevm_bind_null(merge_vm, npks + 1);
    1921            4 :         if (rc != DBRES_OK) { databasevm_reset(merge_vm); return rc; }
    1922            4 :         rc = databasevm_bind_null(merge_vm, npks + 2);
    1923            4 :         if (rc != DBRES_OK) { databasevm_reset(merge_vm); return rc; }
    1924              :     }
    1925              : 
    1926              :     // Execute with triggers disabled
    1927          462 :     table->enabled = 0;
    1928          462 :     SYNCBIT_SET(data);
    1929          462 :     rc = databasevm_step(merge_vm);
    1930          462 :     databasevm_reset(merge_vm);
    1931          462 :     SYNCBIT_RESET(data);
    1932          462 :     table->enabled = 1;
    1933              : 
    1934          462 :     cloudsync_memory_free(text);
    1935              : 
    1936          462 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    1937          462 :     if (rc != DBRES_OK) return cloudsync_set_dberror(data);
    1938          462 :     return DBRES_OK;
    1939          462 : }
    1940              : 
    1941              : // Accessor for has_block_cols flag
    1942         1024 : bool table_has_block_cols (cloudsync_table_context *table) {
    1943         1024 :     return table && table->has_block_cols;
    1944              : }
    1945              : 
    1946              : // Get block column algo for a given column index
    1947        13128 : col_algo_t table_col_algo (cloudsync_table_context *table, int index) {
    1948        13128 :     if (!table || !table->col_algo || index < 0 || index >= table->ncols) return col_algo_normal;
    1949        13128 :     return table->col_algo[index];
    1950        13128 : }
    1951              : 
    1952              : // Get block delimiter for a given column index
    1953          137 : const char *table_col_delimiter (cloudsync_table_context *table, int index) {
    1954          137 :     if (!table || !table->col_delimiter || index < 0 || index >= table->ncols) return BLOCK_DEFAULT_DELIMITER;
    1955          137 :     return table->col_delimiter[index] ? table->col_delimiter[index] : BLOCK_DEFAULT_DELIMITER;
    1956          137 : }
    1957              : 
    1958              : // Block column struct accessors (for use outside cloudsync.c where struct is opaque)
    1959         1024 : dbvm_t *table_block_value_read_stmt (cloudsync_table_context *table) { return table ? table->block_value_read_stmt : NULL; }
    1960          506 : dbvm_t *table_block_value_write_stmt (cloudsync_table_context *table) { return table ? table->block_value_write_stmt : NULL; }
    1961           93 : dbvm_t *table_block_list_stmt (cloudsync_table_context *table) { return table ? table->block_list_stmt : NULL; }
    1962           93 : const char *table_blocks_ref (cloudsync_table_context *table) { return table ? table->blocks_ref : NULL; }
    1963              : 
    1964            3 : void table_set_col_delimiter (cloudsync_table_context *table, int col_idx, const char *delimiter) {
    1965            3 :     if (!table || !table->col_delimiter || col_idx < 0 || col_idx >= table->ncols) return;
    1966            3 :     if (table->col_delimiter[col_idx]) cloudsync_memory_free(table->col_delimiter[col_idx]);
    1967            3 :     table->col_delimiter[col_idx] = delimiter ? cloudsync_string_dup(delimiter) : NULL;
    1968            3 : }
    1969              : 
    1970              : // Find column index by name
    1971          127 : int table_col_index (cloudsync_table_context *table, const char *col_name) {
    1972          127 :     if (!table || !col_name) return -1;
    1973          131 :     for (int i = 0; i < table->ncols; i++) {
    1974          131 :         if (strcasecmp(table->col_name[i], col_name) == 0) return i;
    1975            4 :     }
    1976            0 :     return -1;
    1977          127 : }
    1978              : 
    1979        48345 : int merge_insert (cloudsync_context *data, cloudsync_table_context *table, const char *insert_pk, int insert_pk_len, int64_t insert_cl, const char *insert_name, dbvalue_t *insert_value, int64_t insert_col_version, int64_t insert_db_version, const char *insert_site_id, int insert_site_id_len, int64_t insert_seq, int64_t *rowid) {
    1980              :     // Handle DWS and AWS algorithms here
    1981              :     // Delete-Wins Set (DWS): table_algo_crdt_dws
    1982              :     // Add-Wins Set (AWS): table_algo_crdt_aws
    1983              :     
    1984              :     // Causal-Length Set (CLS) Algorithm (default)
    1985              :     
    1986              :     // compute the local causal length for the row based on the primary key
    1987              :     // the causal length is used to determine the order of operations and resolve conflicts.
    1988        48345 :     int64_t local_cl = merge_get_local_cl(table, insert_pk, insert_pk_len);
    1989        48345 :     if (local_cl < 0) return cloudsync_set_error(data, "Unable to compute local causal length", DBRES_ERROR);
    1990              :     
    1991              :     // if the incoming causal length is older than the local causal length, we can safely ignore it
    1992              :     // because the local changes are more recent
    1993        48345 :     if (insert_cl < local_cl) return DBRES_OK;
    1994              :     
    1995              :     // check if the operation is a delete by examining the causal length
    1996              :     // even causal lengths typically signify delete operations
    1997        48128 :     bool is_delete = (insert_cl % 2 == 0);
    1998        48128 :     if (is_delete) {
    1999              :         // if it's a delete, check if the local state is at the same causal length
    2000              :         // if it is, no further action is needed
    2001          610 :         if (local_cl == insert_cl) return DBRES_OK;
    2002              :         
    2003              :         // perform a delete merge if the causal length is newer than the local one
    2004          334 :         int rc = merge_delete(data, table, insert_pk, insert_pk_len, insert_name, insert_col_version,
    2005          167 :                               insert_db_version, insert_site_id, insert_site_id_len, insert_seq, rowid);
    2006          167 :         if (rc != DBRES_OK) cloudsync_set_error(data, "Unable to perform merge_delete", rc);
    2007          167 :         return rc;
    2008              :     }
    2009              :     
    2010              :     // if the operation is a sentinel-only insert (indicating a new row or resurrected row with no column update), handle it separately.
    2011        47518 :     bool is_sentinel_only = (strcmp(insert_name, CLOUDSYNC_TOMBSTONE_VALUE) == 0);
    2012        47518 :     if (is_sentinel_only) {
    2013          185 :         if (local_cl == insert_cl) return DBRES_OK;
    2014              :         
    2015              :         // perform a sentinel-only insert to track the existence of the row
    2016          116 :         int rc = merge_sentinel_only_insert(data, table, insert_pk, insert_pk_len, insert_col_version,
    2017           58 :                                             insert_db_version, insert_site_id, insert_site_id_len, insert_seq, rowid);
    2018           58 :         if (rc != DBRES_OK) cloudsync_set_error(data, "Unable to perform merge_sentinel_only_insert", rc);
    2019           58 :         return rc;
    2020              :     }
    2021              :     
    2022              :     // from this point I can be sure that insert_name is not sentinel
    2023              :     
    2024              :     // handle the case where a row is being resurrected (e.g., after a delete, a new insert for the same row)
    2025              :     // odd causal lengths can "resurrect" rows
    2026        47333 :     bool needs_resurrect = (insert_cl > local_cl && insert_cl % 2 == 1);
    2027        47333 :     bool row_exists_locally = local_cl != 0;
    2028              :    
    2029              :     // if a resurrection is needed, insert a sentinel to mark the row as alive
    2030              :     // this handles out-of-order deliveries where the row was deleted and is now being re-inserted
    2031        47333 :     if (needs_resurrect && (row_exists_locally || (!row_exists_locally && insert_cl > 1))) {
    2032            0 :         int rc = merge_sentinel_only_insert(data, table, insert_pk, insert_pk_len, insert_cl,
    2033            0 :                                             insert_db_version, insert_site_id, insert_site_id_len, insert_seq, rowid);
    2034            0 :         if (rc != DBRES_OK) return cloudsync_set_error(data, "Unable to perform merge_sentinel_only_insert", rc);
    2035            0 :     }
    2036              :     
    2037              :     // at this point, we determine whether the incoming change wins based on causal length
    2038              :     // this can be due to a resurrection, a non-existent local row, or a conflict resolution
    2039        47333 :     bool flag = false;
    2040        47333 :     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);
    2041        47333 :     if (rc != DBRES_OK) return cloudsync_set_error(data, "Unable to perform merge_did_cid_win", rc);
    2042              :     
    2043              :     // check if the incoming change wins and should be applied
    2044        47333 :     bool does_cid_win = ((needs_resurrect) || (!row_exists_locally) || (flag));
    2045        47333 :     if (!does_cid_win) return DBRES_OK;
    2046              : 
    2047              :     // Block-level LWW: if the incoming col_name is a block entry (contains \x1F),
    2048              :     // bypass the normal base-table write and instead store the value in the blocks table.
    2049              :     // The base table column will be materialized from all alive blocks.
    2050        23868 :     if (block_is_block_colname(insert_name) && table->has_block_cols) {
    2051              :         // Store or delete block value in blocks table depending on tombstone status
    2052          412 :         if (insert_col_version % 2 == 0) {
    2053              :             // Tombstone: remove from blocks table
    2054           33 :             rc = block_delete_value(data, table, insert_pk, insert_pk_len, insert_name);
    2055           33 :         } else {
    2056          379 :             rc = block_store_value(data, table, insert_pk, insert_pk_len, insert_name, insert_value);
    2057              :         }
    2058          412 :         if (rc != DBRES_OK) return cloudsync_set_error(data, "Unable to store/delete block value", rc);
    2059              : 
    2060              :         // Set winner clock in metadata
    2061          824 :         rc = merge_set_winner_clock(data, table, insert_pk, insert_pk_len, insert_name,
    2062          412 :                                     insert_col_version, insert_db_version,
    2063          412 :                                     insert_site_id, insert_site_id_len, insert_seq, rowid);
    2064          412 :         if (rc != DBRES_OK) return cloudsync_set_error(data, "Unable to set winner clock for block", rc);
    2065              : 
    2066              :         // Materialize the full column from blocks into the base table
    2067          412 :         char *base_col = block_extract_base_colname(insert_name);
    2068          412 :         if (base_col) {
    2069          412 :             rc = block_materialize_column(data, table, insert_pk, insert_pk_len, base_col);
    2070          412 :             cloudsync_memory_free(base_col);
    2071          412 :             if (rc != DBRES_OK) return cloudsync_set_error(data, "Unable to materialize block column", rc);
    2072          412 :         }
    2073              : 
    2074          412 :         return DBRES_OK;
    2075              :     }
    2076              : 
    2077              :     // perform the final column insert or update if the incoming change wins
    2078        23456 :     if (data->pending_batch) {
    2079              :         // Propagate row_exists_locally to the batch on the first winning column.
    2080              :         // This lets merge_flush_pending choose UPDATE vs INSERT ON CONFLICT,
    2081              :         // which matters when RLS policies reference columns not in the payload.
    2082        22939 :         if (data->pending_batch->table == NULL) {
    2083         8760 :             data->pending_batch->row_exists = row_exists_locally;
    2084         8760 :         }
    2085        22939 :         rc = merge_pending_add(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);
    2086        22939 :         if (rc != DBRES_OK) cloudsync_set_error(data, "Unable to perform merge_pending_add", rc);
    2087        22939 :     } else {
    2088          517 :         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);
    2089          517 :         if (rc != DBRES_OK) cloudsync_set_error(data, "Unable to perform merge_insert_col", rc);
    2090              :     }
    2091              : 
    2092        23456 :     return rc;
    2093        48345 : }
    2094              : 
    2095              : // MARK: - Block column setup -
    2096              : 
    2097              : // Migrate existing tracked rows to block format when block-level LWW is first enabled on a column.
    2098              : // Scans the metadata table for alive rows with the plain col_name entry (not yet block entries),
    2099              : // reads each row's current value from the base table, splits it into blocks, and inserts
    2100              : // the block entries into both the blocks table and the metadata table.
    2101              : // Uses INSERT OR IGNORE semantics so the operation is safe to call multiple times.
    2102           73 : static int block_migrate_existing_rows (cloudsync_context *data, cloudsync_table_context *table, int col_idx) {
    2103           73 :     const char *col_name = table->col_name[col_idx];
    2104           73 :     if (!col_name || !table->meta_ref || !table->blocks_ref) return DBRES_OK;
    2105              : 
    2106           73 :     const char *delim = table->col_delimiter[col_idx] ? table->col_delimiter[col_idx] : BLOCK_DEFAULT_DELIMITER;
    2107           73 :     int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
    2108              : 
    2109              :     // Phase 1: collect all existing PKs that have an alive regular col_name entry
    2110              :     // AND do not yet have any entries in the blocks table for this column.
    2111              :     // The NOT IN filter makes this idempotent: rows that were already migrated
    2112              :     // (or had their blocks created via INSERT) are skipped on subsequent calls.
    2113              :     // We collect PKs before writing so that writes to the metadata table (Phase 2)
    2114              :     // do not perturb the read cursor on the same table.
    2115           73 :     char *like_pattern = block_build_colname(col_name, "%");
    2116           73 :     if (!like_pattern) return DBRES_NOMEM;
    2117              : 
    2118           73 :     char *scan_sql = cloudsync_memory_mprintf(SQL_META_SCAN_COL_FOR_MIGRATION, table->meta_ref, table->blocks_ref);
    2119           73 :     if (!scan_sql) { cloudsync_memory_free(like_pattern); return DBRES_NOMEM; }
    2120           73 :     dbvm_t *scan_vm = NULL;
    2121           73 :     int rc = databasevm_prepare(data, scan_sql, &scan_vm, 0);
    2122           73 :     cloudsync_memory_free(scan_sql);
    2123           73 :     if (rc != DBRES_OK) { cloudsync_memory_free(like_pattern); return rc; }
    2124              : 
    2125           73 :     rc = databasevm_bind_text(scan_vm, 1, col_name, -1);
    2126           73 :     if (rc != DBRES_OK) { cloudsync_memory_free(like_pattern); databasevm_finalize(scan_vm); return rc; }
    2127              :     // Bind like_pattern as ?2 and keep it alive until after all scan steps complete,
    2128              :     // because databasevm_bind_text uses SQLITE_STATIC (no copy).
    2129           73 :     rc = databasevm_bind_text(scan_vm, 2, like_pattern, -1);
    2130           73 :     if (rc != DBRES_OK) { cloudsync_memory_free(like_pattern); databasevm_finalize(scan_vm); return rc; }
    2131              : 
    2132              :     // Collect pk blobs into a dynamically-grown array of owned copies
    2133           73 :     void  **pks     = NULL;
    2134           73 :     size_t *pklens  = NULL;
    2135           73 :     int     pk_count = 0;
    2136           73 :     int     pk_cap   = 0;
    2137              : 
    2138           75 :     while ((rc = databasevm_step(scan_vm)) == DBRES_ROW) {
    2139            2 :         size_t pklen = 0;
    2140            2 :         const void *pk = database_column_blob(scan_vm, 0, &pklen);
    2141            2 :         if (!pk || pklen == 0) continue;
    2142              : 
    2143            2 :         if (pk_count >= pk_cap) {
    2144            1 :             int new_cap = pk_cap ? pk_cap * 2 : 8;
    2145            1 :             void  **new_pks    = (void  **)cloudsync_memory_realloc(pks,    (uint64_t)(new_cap * sizeof(void *)));
    2146            1 :             size_t *new_pklens = (size_t *)cloudsync_memory_realloc(pklens, (uint64_t)(new_cap * sizeof(size_t)));
    2147            1 :             if (!new_pks || !new_pklens) {
    2148            0 :                 cloudsync_memory_free(new_pks ? new_pks : pks);
    2149            0 :                 cloudsync_memory_free(new_pklens ? new_pklens : pklens);
    2150            0 :                 databasevm_finalize(scan_vm);
    2151            0 :                 return DBRES_NOMEM;
    2152              :             }
    2153            1 :             pks    = new_pks;
    2154            1 :             pklens = new_pklens;
    2155            1 :             pk_cap = new_cap;
    2156            1 :         }
    2157              : 
    2158            2 :         pks[pk_count] = cloudsync_memory_alloc((uint64_t)pklen);
    2159            2 :         if (!pks[pk_count]) { rc = DBRES_NOMEM; break; }
    2160            2 :         memcpy(pks[pk_count], pk, pklen);
    2161            2 :         pklens[pk_count] = pklen;
    2162            2 :         pk_count++;
    2163              :     }
    2164              : 
    2165           73 :     databasevm_finalize(scan_vm);
    2166           73 :     cloudsync_memory_free(like_pattern); // safe to free after scan_vm is finalized
    2167           73 :     if (rc != DBRES_DONE && rc != DBRES_OK) {
    2168            0 :         for (int i = 0; i < pk_count; i++) cloudsync_memory_free(pks[i]);
    2169            0 :         cloudsync_memory_free(pks);
    2170            0 :         cloudsync_memory_free(pklens);
    2171            0 :         return rc;
    2172              :     }
    2173              : 
    2174           73 :     if (pk_count == 0) {
    2175           72 :         cloudsync_memory_free(pks);
    2176           72 :         cloudsync_memory_free(pklens);
    2177           72 :         return DBRES_OK;
    2178              :     }
    2179              : 
    2180              :     // Phase 2: for each collected PK, read the column value, split into blocks,
    2181              :     // and insert into the blocks table + metadata using INSERT OR IGNORE.
    2182              : 
    2183            1 :     char *meta_sql = cloudsync_memory_mprintf(SQL_META_INSERT_BLOCK_IGNORE, table->meta_ref);
    2184            1 :     if (!meta_sql) { rc = DBRES_NOMEM; goto cleanup_pks; }
    2185            1 :     dbvm_t *meta_vm = NULL;
    2186            1 :     rc = databasevm_prepare(data, meta_sql, &meta_vm, 0);
    2187            1 :     cloudsync_memory_free(meta_sql);
    2188            1 :     if (rc != DBRES_OK) goto cleanup_pks;
    2189              : 
    2190            1 :     char *blocks_sql = cloudsync_memory_mprintf(SQL_BLOCKS_INSERT_IGNORE, table->blocks_ref);
    2191            1 :     if (!blocks_sql) { databasevm_finalize(meta_vm); rc = DBRES_NOMEM; goto cleanup_pks; }
    2192            1 :     dbvm_t *blocks_vm = NULL;
    2193            1 :     rc = databasevm_prepare(data, blocks_sql, &blocks_vm, 0);
    2194            1 :     cloudsync_memory_free(blocks_sql);
    2195            1 :     if (rc != DBRES_OK) { databasevm_finalize(meta_vm); goto cleanup_pks; }
    2196              : 
    2197            1 :     dbvm_t *val_vm = (dbvm_t *)table_column_lookup(table, col_name, false, NULL);
    2198              : 
    2199            3 :     for (int p = 0; p < pk_count; p++) {
    2200            2 :         const void *pk = pks[p];
    2201            2 :         size_t pklen   = pklens[p];
    2202              : 
    2203            2 :         if (!val_vm) continue;
    2204              : 
    2205              :         // Read current column value from the base table
    2206            2 :         int bind_rc = pk_decode_prikey((char *)pk, pklen, pk_decode_bind_callback, (void *)val_vm);
    2207            2 :         if (bind_rc < 0) { databasevm_reset(val_vm); continue; }
    2208              : 
    2209            2 :         int step_rc = databasevm_step(val_vm);
    2210            2 :         const char *text = (step_rc == DBRES_ROW) ? database_column_text(val_vm, 0) : NULL;
    2211              :         // Make a copy of text before resetting val_vm, as the pointer is only valid until reset
    2212            2 :         char *text_copy = text ? cloudsync_string_dup(text) : NULL;
    2213            2 :         databasevm_reset(val_vm);
    2214              : 
    2215            2 :         if (!text_copy) continue; // NULL column value: nothing to migrate
    2216              : 
    2217              :         // Split text into blocks and store each one
    2218            2 :         block_list_t *blocks = block_split(text_copy, delim);
    2219            2 :         cloudsync_memory_free(text_copy);
    2220            2 :         if (!blocks) continue;
    2221              : 
    2222            2 :         char **positions = block_initial_positions(blocks->count);
    2223            2 :         if (positions) {
    2224            7 :             for (int b = 0; b < blocks->count; b++) {
    2225            5 :                 char *block_cn = block_build_colname(col_name, positions[b]);
    2226            5 :                 if (block_cn) {
    2227              :                     // Metadata entry (skip if this block position already exists)
    2228            5 :                     databasevm_bind_blob(meta_vm, 1, pk, (int)pklen);
    2229            5 :                     databasevm_bind_text(meta_vm, 2, block_cn, -1);
    2230            5 :                     databasevm_bind_int(meta_vm, 3, 1);            // col_version = 1 (alive)
    2231            5 :                     databasevm_bind_int(meta_vm, 4, db_version);
    2232            5 :                     databasevm_bind_int(meta_vm, 5, cloudsync_bumpseq(data));
    2233            5 :                     databasevm_step(meta_vm);
    2234            5 :                     databasevm_reset(meta_vm);
    2235              : 
    2236              :                     // Block value (skip if this block position already exists)
    2237            5 :                     databasevm_bind_blob(blocks_vm, 1, pk, (int)pklen);
    2238            5 :                     databasevm_bind_text(blocks_vm, 2, block_cn, -1);
    2239            5 :                     databasevm_bind_text(blocks_vm, 3, blocks->entries[b].content, -1);
    2240            5 :                     databasevm_step(blocks_vm);
    2241            5 :                     databasevm_reset(blocks_vm);
    2242              : 
    2243            5 :                     cloudsync_memory_free(block_cn);
    2244            5 :                 }
    2245            5 :                 cloudsync_memory_free(positions[b]);
    2246            5 :             }
    2247            2 :             cloudsync_memory_free(positions);
    2248            2 :         }
    2249            2 :         block_list_free(blocks);
    2250            2 :     }
    2251              : 
    2252            1 :     databasevm_finalize(meta_vm);
    2253            1 :     databasevm_finalize(blocks_vm);
    2254            1 :     rc = DBRES_OK;
    2255              : 
    2256              : cleanup_pks:
    2257            3 :     for (int i = 0; i < pk_count; i++) cloudsync_memory_free(pks[i]);
    2258            1 :     cloudsync_memory_free(pks);
    2259            1 :     cloudsync_memory_free(pklens);
    2260            1 :     return rc;
    2261           73 : }
    2262              : 
    2263           74 : int cloudsync_setup_block_column (cloudsync_context *data, const char *table_name, const char *col_name, const char *delimiter, bool persist) {
    2264           74 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2265           74 :     if (!table) return cloudsync_set_error(data, "cloudsync_setup_block_column: table not found", DBRES_ERROR);
    2266              : 
    2267              :     // Find column index
    2268           74 :     int col_idx = table_col_index(table, col_name);
    2269           74 :     if (col_idx < 0) {
    2270              :         char buf[1024];
    2271            0 :         snprintf(buf, sizeof(buf), "cloudsync_setup_block_column: column '%s' not found in table '%s'", col_name, table_name);
    2272            0 :         return cloudsync_set_error(data, buf, DBRES_ERROR);
    2273              :     }
    2274              : 
    2275              :     // Set column algo
    2276           74 :     table->col_algo[col_idx] = col_algo_block;
    2277           74 :     table->has_block_cols = true;
    2278              : 
    2279              :     // Set delimiter (can be NULL for default)
    2280           74 :     if (table->col_delimiter[col_idx]) {
    2281            0 :         cloudsync_memory_free(table->col_delimiter[col_idx]);
    2282            0 :         table->col_delimiter[col_idx] = NULL;
    2283            0 :     }
    2284           74 :     if (delimiter) {
    2285            1 :         table->col_delimiter[col_idx] = cloudsync_string_dup(delimiter);
    2286            1 :     }
    2287              : 
    2288              :     // Create blocks table if not already done
    2289           74 :     if (!table->blocks_ref) {
    2290           71 :         table->blocks_ref = database_build_blocks_ref(table->schema, table->name);
    2291           71 :         if (!table->blocks_ref) return DBRES_NOMEM;
    2292              : 
    2293              :         // CREATE TABLE IF NOT EXISTS
    2294           71 :         char *sql = cloudsync_memory_mprintf(SQL_BLOCKS_CREATE_TABLE, table->blocks_ref);
    2295           71 :         if (!sql) return DBRES_NOMEM;
    2296              : 
    2297           71 :         int rc = database_exec(data, sql);
    2298           71 :         cloudsync_memory_free(sql);
    2299           71 :         if (rc != DBRES_OK) return cloudsync_set_error(data, "Unable to create blocks table", rc);
    2300              : 
    2301              :         // Prepare block statements
    2302              :         // Write: upsert into blocks (pk, col_name, col_value)
    2303           71 :         sql = cloudsync_memory_mprintf(SQL_BLOCKS_UPSERT, table->blocks_ref);
    2304           71 :         if (!sql) return DBRES_NOMEM;
    2305           71 :         rc = databasevm_prepare(data, sql, (void **)&table->block_value_write_stmt, DBFLAG_PERSISTENT);
    2306           71 :         cloudsync_memory_free(sql);
    2307           71 :         if (rc != DBRES_OK) return rc;
    2308              : 
    2309              :         // Read: SELECT col_value FROM blocks WHERE pk = ? AND col_name = ?
    2310           71 :         sql = cloudsync_memory_mprintf(SQL_BLOCKS_SELECT, table->blocks_ref);
    2311           71 :         if (!sql) return DBRES_NOMEM;
    2312           71 :         rc = databasevm_prepare(data, sql, (void **)&table->block_value_read_stmt, DBFLAG_PERSISTENT);
    2313           71 :         cloudsync_memory_free(sql);
    2314           71 :         if (rc != DBRES_OK) return rc;
    2315              : 
    2316              :         // Delete: DELETE FROM blocks WHERE pk = ? AND col_name = ?
    2317           71 :         sql = cloudsync_memory_mprintf(SQL_BLOCKS_DELETE, table->blocks_ref);
    2318           71 :         if (!sql) return DBRES_NOMEM;
    2319           71 :         rc = databasevm_prepare(data, sql, (void **)&table->block_value_delete_stmt, DBFLAG_PERSISTENT);
    2320           71 :         cloudsync_memory_free(sql);
    2321           71 :         if (rc != DBRES_OK) return rc;
    2322              : 
    2323              :         // List alive blocks for materialization
    2324           71 :         sql = cloudsync_memory_mprintf(SQL_BLOCKS_LIST_ALIVE, table->blocks_ref, table->meta_ref);
    2325           71 :         if (!sql) return DBRES_NOMEM;
    2326           71 :         rc = databasevm_prepare(data, sql, (void **)&table->block_list_stmt, DBFLAG_PERSISTENT);
    2327           71 :         cloudsync_memory_free(sql);
    2328           71 :         if (rc != DBRES_OK) return rc;
    2329           71 :     }
    2330              : 
    2331              :     // Persist settings (skipped when called from the settings loader, since
    2332              :     // writing to cloudsync_table_settings while sqlite3_exec is iterating it
    2333              :     // re-feeds the rewritten row to the cursor and causes an infinite loop).
    2334           74 :     if (persist) {
    2335           73 :         int rc = dbutils_table_settings_set_key_value(data, table_name, col_name, "algo", "block");
    2336           73 :         if (rc != DBRES_OK) return rc;
    2337              : 
    2338           73 :         if (delimiter) {
    2339            0 :             rc = dbutils_table_settings_set_key_value(data, table_name, col_name, "delimiter", delimiter);
    2340            0 :             if (rc != DBRES_OK) return rc;
    2341            0 :         }
    2342              : 
    2343              :         // Migrate any existing tracked rows: populate the blocks table and metadata with
    2344              :         // block entries derived from the current column value, so that subsequent UPDATE
    2345              :         // operations can diff against the real existing state instead of treating everything
    2346              :         // as new, and so this node participates correctly in LWW conflict resolution.
    2347           73 :         rc = block_migrate_existing_rows(data, table, col_idx);
    2348           73 :         if (rc != DBRES_OK) return rc;
    2349           73 :     }
    2350              : 
    2351           74 :     return DBRES_OK;
    2352           74 : }
    2353              : 
    2354              : // MARK: - Private -
    2355              : 
    2356          255 : bool cloudsync_config_exists (cloudsync_context *data) {
    2357          255 :     return database_internal_table_exists(data, CLOUDSYNC_SITEID_NAME) == true;
    2358              : }
    2359              : 
    2360          801 : bool cloudsync_context_is_initialized (cloudsync_context *data) {
    2361              :     // A fully initialized context has its persistent "is the DB stale" probe
    2362              :     // prepared. cloudsync_context_init prepares data_version_stmt (via
    2363              :     // cloudsync_add_dbvms) only after the cloudsync_site_id table exists, so
    2364              :     // a non-NULL pointer means cloudsync_init has been called at least once
    2365              :     // on this connection. Used to produce actionable error messages when
    2366              :     // callers hit a function before calling cloudsync_init.
    2367          801 :     return data != NULL && data->data_version_stmt != NULL;
    2368              : }
    2369              : 
    2370          270 : cloudsync_context *cloudsync_context_create (void *db) {
    2371          270 :     cloudsync_context *data = (cloudsync_context *)cloudsync_memory_zeroalloc((uint64_t)(sizeof(cloudsync_context)));
    2372          270 :     if (!data) return NULL;
    2373              :     DEBUG_SETTINGS("cloudsync_context_create %p", data);
    2374              :     
    2375          270 :     data->libversion = CLOUDSYNC_VERSION;
    2376          270 :     data->pending_db_version = CLOUDSYNC_VALUE_NOTSET;
    2377              :     #if CLOUDSYNC_DEBUG
    2378              :     data->debug = 1;
    2379              :     #endif
    2380              :     
    2381              :     // allocate space for 64 tables (it can grow if needed)
    2382          270 :     uint64_t mem_needed = (uint64_t)(CLOUDSYNC_INIT_NTABLES * sizeof(cloudsync_table_context *));
    2383          270 :     data->tables = (cloudsync_table_context **)cloudsync_memory_zeroalloc(mem_needed);
    2384          270 :     if (!data->tables) {cloudsync_memory_free(data); return NULL;}
    2385              :     
    2386          270 :     data->tables_cap = CLOUDSYNC_INIT_NTABLES;
    2387          270 :     data->tables_count = 0;
    2388          270 :     data->db = db;
    2389              :     
    2390              :     // SQLite exposes col_value as ANY, but other databases require a concrete type.
    2391              :     // In PostgreSQL we expose col_value as bytea, which holds the pk-encoded value bytes (type + data).
    2392              :     // Because col_value is already encoded, we skip decoding this field and pass it through as bytea.
    2393              :     // It is decoded to the target column type just before applying changes to the base table.
    2394          270 :     data->skip_decode_idx = (db == NULL) ? CLOUDSYNC_PK_INDEX_COLVALUE : -1;
    2395              : 
    2396          270 :     return data;
    2397          270 : }
    2398              : 
    2399          270 : void cloudsync_context_free (void *ctx) {
    2400          270 :     cloudsync_context *data = (cloudsync_context *)ctx;
    2401              :     DEBUG_SETTINGS("cloudsync_context_free %p", data);
    2402          270 :     if (!data) return;
    2403              : 
    2404              :     // free all table contexts and prepared statements
    2405          270 :     cloudsync_terminate(data);
    2406              : 
    2407          270 :     cloudsync_memory_free(data->tables);
    2408          270 :     cloudsync_memory_free(data);
    2409          270 : }
    2410              : 
    2411          364 : const char *cloudsync_context_init (cloudsync_context *data) {
    2412          364 :     if (!data) return NULL;
    2413              :     
    2414              :     // perform init just the first time, if the site_id field is not set.
    2415              :     // The data->site_id value could exists while settings tables don't exists if the
    2416              :     // cloudsync_context_init was previously called in init transaction that was rolled back
    2417              :     // because of an error during the init process.
    2418          364 :     if (data->site_id[0] == 0 || !database_internal_table_exists(data, CLOUDSYNC_SITEID_NAME)) {
    2419          249 :         if (dbutils_settings_init(data) != DBRES_OK) return NULL;
    2420          249 :         if (cloudsync_add_dbvms(data) != DBRES_OK) return NULL;
    2421          249 :         if (cloudsync_load_siteid(data) != DBRES_OK) return NULL;
    2422          249 :         data->schema_hash = database_schema_hash(data);
    2423          249 :     }
    2424              :     
    2425          364 :     return (const char *)data->site_id;
    2426          364 : }
    2427              : 
    2428         1350 : void cloudsync_sync_key (cloudsync_context *data, const char *key, const char *value) {
    2429              :     DEBUG_SETTINGS("cloudsync_sync_key key: %s value: %s", key, value);
    2430              :     
    2431              :     // sync data
    2432         1350 :     if (strcmp(key, CLOUDSYNC_KEY_SCHEMAVERSION) == 0) {
    2433          249 :         data->schema_version = (int)strtol(value, NULL, 0);
    2434          249 :         return;
    2435              :     }
    2436              :     
    2437         1101 :     if (strcmp(key, CLOUDSYNC_KEY_DEBUG) == 0) {
    2438            0 :         data->debug = 0;
    2439            0 :         if (value && (value[0] != 0) && (value[0] != '0')) data->debug = 1;
    2440            0 :         return;
    2441              :     }
    2442              : 
    2443         1101 :     if (strcmp(key, CLOUDSYNC_KEY_SCHEMA) == 0) {
    2444            0 :         cloudsync_set_schema(data, value);
    2445            0 :         return;
    2446              :     }
    2447         1350 : }
    2448              : 
    2449              : #if 0
    2450              : void cloudsync_sync_table_key(cloudsync_context *data, const char *table, const char *column, const char *key, const char *value) {
    2451              :     DEBUG_SETTINGS("cloudsync_sync_table_key table: %s column: %s key: %s value: %s", table, column, key, value);
    2452              :     // Unused in this version
    2453              :     return;
    2454              : }
    2455              : #endif
    2456              : 
    2457         8430 : int cloudsync_commit_hook (void *ctx) {
    2458         8430 :     cloudsync_context *data = (cloudsync_context *)ctx;
    2459              :     
    2460         8430 :     data->db_version = data->pending_db_version;
    2461         8430 :     data->pending_db_version = CLOUDSYNC_VALUE_NOTSET;
    2462         8430 :     data->seq = 0;
    2463              :     
    2464         8430 :     return DBRES_OK;
    2465              : }
    2466              : 
    2467            3 : void cloudsync_rollback_hook (void *ctx) {
    2468            3 :     cloudsync_context *data = (cloudsync_context *)ctx;
    2469              :     
    2470            3 :     data->pending_db_version = CLOUDSYNC_VALUE_NOTSET;
    2471            3 :     data->seq = 0;
    2472            3 : }
    2473              : 
    2474           24 : int cloudsync_begin_alter (cloudsync_context *data, const char *table_name) {
    2475              :     // init cloudsync_settings
    2476           24 :     if (cloudsync_context_init(data) == NULL) {
    2477            0 :         return DBRES_MISUSE;
    2478              :     }
    2479              : 
    2480              :     // lookup table
    2481           24 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2482           24 :     if (!table) {
    2483              :         char buffer[1024];
    2484            1 :         snprintf(buffer, sizeof(buffer), "Unable to find table %s", table_name);
    2485            1 :         return cloudsync_set_error(data, buffer, DBRES_MISUSE);
    2486              :     }
    2487              : 
    2488              :     // idempotent: if already altering, return OK
    2489           23 :     if (table->is_altering) return DBRES_OK;
    2490              : 
    2491              :     // retrieve primary key(s)
    2492           23 :     char **names = NULL;
    2493           23 :     int nrows = 0;
    2494           23 :     int rc = database_pk_names(data, table_name, &names, &nrows);
    2495           23 :     if (rc != DBRES_OK) {
    2496              :         char buffer[1024];
    2497            0 :         snprintf(buffer, sizeof(buffer), "Unable to get primary keys for table %s", table_name);
    2498            0 :         cloudsync_set_error(data, buffer, DBRES_MISUSE);
    2499            0 :         goto rollback_begin_alter;
    2500              :     }
    2501              : 
    2502              :     // sanity check the number of primary keys
    2503           23 :     if (nrows != table_count_pks(table)) {
    2504              :         char buffer[1024];
    2505            0 :         snprintf(buffer, sizeof(buffer), "Number of primary keys for table %s changed before ALTER", table_name);
    2506            0 :         cloudsync_set_error(data, buffer, DBRES_MISUSE);
    2507            0 :         goto rollback_begin_alter;
    2508              :     }
    2509              : 
    2510              :     // drop original triggers
    2511           23 :     rc = database_delete_triggers(data, table_name);
    2512           23 :     if (rc != DBRES_OK) {
    2513              :         char buffer[1024];
    2514            0 :         snprintf(buffer, sizeof(buffer), "Unable to delete triggers for table %s in cloudsync_begin_alter.", table_name);
    2515            0 :         cloudsync_set_error(data, buffer, DBRES_ERROR);
    2516            0 :         goto rollback_begin_alter;
    2517              :     }
    2518              : 
    2519           23 :     table_set_pknames(table, names);
    2520           23 :     table->is_altering = true;
    2521           23 :     return DBRES_OK;
    2522              : 
    2523              : rollback_begin_alter:
    2524            0 :     if (names) table_pknames_free(names, nrows);
    2525            0 :     return rc;
    2526           24 : }
    2527              : 
    2528           23 : int cloudsync_finalize_alter (cloudsync_context *data, cloudsync_table_context *table) {
    2529              :     // check if dbversion needed to be updated
    2530           23 :     cloudsync_dbversion_check_uptodate(data);
    2531              : 
    2532              :     // if primary-key columns change, all row identities change.
    2533              :     // In that case, the clock table must be dropped, recreated,
    2534              :     // and backfilled. We detect this by comparing the unique index
    2535              :     // in the lookaside table with the source table's PKs.
    2536              :     
    2537              :     // retrieve primary keys (to check is they changed)
    2538           23 :     char **result = NULL;
    2539           23 :     int nrows = 0;
    2540           23 :     int rc = database_pk_names (data, table->name, &result, &nrows);
    2541           23 :     if (rc != DBRES_OK || nrows == 0) {
    2542            0 :         if (nrows == 0) rc = DBRES_MISUSE;
    2543            0 :         goto finalize;
    2544              :     }
    2545              :     
    2546              :     // check if there are differences
    2547           23 :     bool pk_diff = (nrows != table->npks);
    2548           23 :     if (!pk_diff) {
    2549           45 :         for (int i = 0; i < nrows; ++i) {
    2550           34 :             if (strcmp(table->pk_name[i], result[i]) != 0) {
    2551            6 :                 pk_diff = true;
    2552            6 :                 break;
    2553              :             }
    2554           28 :         }
    2555           17 :     }
    2556              :     
    2557           23 :     if (pk_diff) {
    2558              :         // drop meta-table, it will be recreated
    2559           12 :         char *sql = cloudsync_memory_mprintf(SQL_DROP_CLOUDSYNC_TABLE, table->meta_ref);
    2560           12 :         rc = database_exec(data, sql);
    2561           12 :         cloudsync_memory_free(sql);
    2562           12 :         if (rc != DBRES_OK) {
    2563            0 :             DEBUG_DBERROR(rc, "cloudsync_finalize_alter", data);
    2564            0 :             goto finalize;
    2565              :         }
    2566           12 :     } else {
    2567              :         // compact meta-table
    2568              :         // delete entries for removed columns
    2569           11 :         const char *schema = table->schema ? table->schema : "";
    2570           11 :         char *sql = sql_build_delete_cols_not_in_schema_query(schema, table->name, table->meta_ref, CLOUDSYNC_TOMBSTONE_VALUE);
    2571           11 :         rc = database_exec(data, sql);
    2572           11 :         cloudsync_memory_free(sql);
    2573           11 :         if (rc != DBRES_OK) {
    2574            0 :             DEBUG_DBERROR(rc, "cloudsync_finalize_alter", data);
    2575            0 :             goto finalize;
    2576              :         }
    2577              :         
    2578           11 :         sql = sql_build_pk_qualified_collist_query(schema, table->name);
    2579           11 :         if (!sql) {rc = DBRES_NOMEM; goto finalize;}
    2580              :         
    2581           11 :         char *pkclause = NULL;
    2582           11 :         rc = database_select_text(data, sql, &pkclause);
    2583           11 :         cloudsync_memory_free(sql);
    2584           11 :         if (rc != DBRES_OK) goto finalize;
    2585           11 :         char *pkvalues = (pkclause) ? pkclause : "rowid";
    2586              :         
    2587              :         // delete entries related to rows that no longer exist in the original table, but preserve tombstone
    2588           11 :         sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_GC_DELETE_ORPHANED_PK, table->meta_ref, CLOUDSYNC_TOMBSTONE_VALUE, CLOUDSYNC_TOMBSTONE_VALUE, table->base_ref, table->meta_ref, pkvalues);
    2589           11 :         rc = database_exec(data, sql);
    2590           11 :         if (pkclause) cloudsync_memory_free(pkclause);
    2591           11 :         cloudsync_memory_free(sql);
    2592           11 :         if (rc != DBRES_OK) {
    2593            0 :             DEBUG_DBERROR(rc, "cloudsync_finalize_alter", data);
    2594            0 :             goto finalize;
    2595              :         }
    2596              : 
    2597              :     }
    2598              :     
    2599              :     // update key to be later used in cloudsync_dbversion_rebuild
    2600              :     char buf[256];
    2601           23 :     snprintf(buf, sizeof(buf), "%" PRId64, data->db_version);
    2602           23 :     dbutils_settings_set_key_value(data, "pre_alter_dbversion", buf);
    2603              :     
    2604              : finalize:
    2605           23 :     table_pknames_free(result, nrows);
    2606           23 :     return rc;
    2607              : }
    2608              : 
    2609           24 : int cloudsync_commit_alter (cloudsync_context *data, const char *table_name) {
    2610           24 :     int rc = DBRES_MISUSE;
    2611           24 :     cloudsync_table_context *table = NULL;
    2612              : 
    2613              :     // init cloudsync_settings
    2614           24 :     if (cloudsync_context_init(data) == NULL) {
    2615            0 :         cloudsync_set_error(data, "Unable to initialize cloudsync context", DBRES_MISUSE);
    2616            0 :         goto rollback_finalize_alter;
    2617              :     }
    2618              : 
    2619              :     // lookup table
    2620           24 :     table = table_lookup(data, table_name);
    2621           24 :     if (!table) {
    2622              :         char buffer[1024];
    2623            1 :         snprintf(buffer, sizeof(buffer), "Unable to find table %s", table_name);
    2624            1 :         cloudsync_set_error(data, buffer, DBRES_MISUSE);
    2625            1 :         goto rollback_finalize_alter;
    2626              :     }
    2627              : 
    2628              :     // idempotent: if not altering, return OK
    2629           23 :     if (!table->is_altering) return DBRES_OK;
    2630              : 
    2631           23 :     rc = cloudsync_finalize_alter(data, table);
    2632           23 :     if (rc != DBRES_OK) goto rollback_finalize_alter;
    2633              : 
    2634              :     // the table is outdated, delete it and it will be reloaded in the cloudsync_init_internal
    2635              :     // is_altering is reset implicitly because table_free + cloudsync_init_table
    2636              :     // will reallocate the table context with zero-initialized memory
    2637           23 :     table_remove(data, table);
    2638           23 :     table_free(table);
    2639           23 :     table = NULL;
    2640              : 
    2641              :     // init again cloudsync for the table
    2642           23 :     table_algo algo_current = dbutils_table_settings_get_algo(data, table_name);
    2643           23 :     if (algo_current == table_algo_none) algo_current = dbutils_table_settings_get_algo(data, "*");
    2644           23 :     rc = cloudsync_init_table(data, table_name, cloudsync_algo_name(algo_current), CLOUDSYNC_INIT_FLAG_SKIP_INT_PK_CHECK);
    2645           23 :     if (rc != DBRES_OK) goto rollback_finalize_alter;
    2646              : 
    2647           23 :     return DBRES_OK;
    2648              : 
    2649              : rollback_finalize_alter:
    2650            1 :     if (table) {
    2651            0 :         table_set_pknames(table, NULL);
    2652            0 :         table->is_altering = false;
    2653            0 :     }
    2654            1 :     return rc;
    2655           24 : }
    2656              : 
    2657              : // MARK: - Filter Rewrite -
    2658              : 
    2659              : // Replace bare column names in a filter expression with prefix-qualified names.
    2660              : // E.g., filter="user_id = 42", prefix="NEW", columns=["user_id","id"] → "NEW.\"user_id\" = 42"
    2661              : // Columns must be sorted by length descending by the caller to avoid partial matches.
    2662              : // Skips content inside single-quoted string literals.
    2663              : // Returns a newly allocated string (caller must free with cloudsync_memory_free), or NULL on error.
    2664              : // Helper: check if an identifier token matches a column name.
    2665          112 : static bool filter_is_column (const char *token, size_t token_len, char **columns, int ncols) {
    2666          450 :     for (int i = 0; i < ncols; ++i) {
    2667          388 :         if (strlen(columns[i]) == token_len && strncmp(token, columns[i], token_len) == 0)
    2668           50 :             return true;
    2669          338 :     }
    2670           62 :     return false;
    2671          112 : }
    2672              : 
    2673              : // Helper: check if character is part of a SQL identifier.
    2674          796 : static bool filter_is_ident_char (char c) {
    2675         1262 :     return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
    2676          466 :            (c >= '0' && c <= '9') || c == '_';
    2677              : }
    2678              : 
    2679           40 : char *cloudsync_filter_add_row_prefix (const char *filter, const char *prefix, char **columns, int ncols) {
    2680           40 :     if (!filter || !prefix || !columns || ncols <= 0) return NULL;
    2681              : 
    2682           40 :     size_t filter_len = strlen(filter);
    2683           40 :     size_t prefix_len = strlen(prefix);
    2684              : 
    2685              :     // Each identifier match grows by at most (prefix_len + 3) bytes.
    2686              :     // Worst case: the entire filter is one repeated column reference separated by
    2687              :     // single characters, so up to (filter_len / 2) matches.  Use a safe upper bound.
    2688           40 :     size_t max_growth = (filter_len / 2 + 1) * (prefix_len + 3);
    2689           40 :     size_t cap = filter_len + max_growth + 64;
    2690           40 :     char *result = (char *)cloudsync_memory_alloc(cap);
    2691           40 :     if (!result) return NULL;
    2692           40 :     size_t out = 0;
    2693              : 
    2694              :     // Single pass: tokenize into identifiers, quoted strings, and everything else.
    2695           40 :     size_t i = 0;
    2696          336 :     while (i < filter_len) {
    2697              :         // Skip single-quoted string literals verbatim (handle '' escape)
    2698          296 :         if (filter[i] == '\'') {
    2699            6 :             result[out++] = filter[i++];
    2700           38 :             while (i < filter_len) {
    2701           38 :                 if (filter[i] == '\'') {
    2702            6 :                     result[out++] = filter[i++];
    2703              :                     // '' is an escaped quote — keep going
    2704            6 :                     if (i < filter_len && filter[i] == '\'') {
    2705            0 :                         result[out++] = filter[i++];
    2706            0 :                         continue;
    2707              :                     }
    2708            6 :                     break; // single ' ends the literal
    2709              :                 }
    2710           32 :                 result[out++] = filter[i++];
    2711              :             }
    2712            6 :             continue;
    2713              :         }
    2714              : 
    2715              :         // Extract identifier token
    2716          290 :         if (filter_is_ident_char(filter[i])) {
    2717          112 :             size_t start = i;
    2718          540 :             while (i < filter_len && filter_is_ident_char(filter[i])) ++i;
    2719          112 :             size_t token_len = i - start;
    2720              : 
    2721          112 :             if (filter_is_column(&filter[start], token_len, columns, ncols)) {
    2722              :                 // Emit PREFIX."column_name"
    2723           50 :                 memcpy(&result[out], prefix, prefix_len); out += prefix_len;
    2724           50 :                 result[out++] = '.';
    2725           50 :                 result[out++] = '"';
    2726           50 :                 memcpy(&result[out], &filter[start], token_len); out += token_len;
    2727           50 :                 result[out++] = '"';
    2728           50 :             } else {
    2729              :                 // Not a column — copy as-is
    2730           62 :                 memcpy(&result[out], &filter[start], token_len); out += token_len;
    2731              :             }
    2732          112 :             continue;
    2733              :         }
    2734              : 
    2735              :         // Any other character — copy as-is
    2736          178 :         result[out++] = filter[i++];
    2737              :     }
    2738              : 
    2739           40 :     result[out] = '\0';
    2740           40 :     return result;
    2741           40 : }
    2742              : 
    2743           24 : int cloudsync_reset_metatable (cloudsync_context *data, const char *table_name) {
    2744           24 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2745           24 :     if (!table) return DBRES_ERROR;
    2746              : 
    2747           24 :     char *sql = cloudsync_memory_mprintf(SQL_DELETE_ALL_FROM_CLOUDSYNC_TABLE, table->meta_ref);
    2748           24 :     int rc = database_exec(data, sql);
    2749           24 :     cloudsync_memory_free(sql);
    2750           24 :     if (rc != DBRES_OK) return rc;
    2751              : 
    2752           24 :     return cloudsync_refill_metatable(data, table_name);
    2753           24 : }
    2754              : 
    2755          326 : int cloudsync_refill_metatable (cloudsync_context *data, const char *table_name) {
    2756          326 :     cloudsync_table_context *table = table_lookup(data, table_name);
    2757          326 :     if (!table) return DBRES_ERROR;
    2758              : 
    2759          326 :     dbvm_t *vm = NULL;
    2760          326 :     int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
    2761              : 
    2762              :     // Read row-level filter from settings (if any)
    2763              :     char filter_buf[2048];
    2764          326 :     int frc = dbutils_table_settings_get_value(data, table_name, "*", "filter", filter_buf, sizeof(filter_buf));
    2765          326 :     const char *filter = (frc == DBRES_OK && filter_buf[0]) ? filter_buf : NULL;
    2766              : 
    2767          326 :     const char *schema = table->schema ? table->schema : "";
    2768          326 :     char *sql = sql_build_pk_collist_query(schema, table_name);
    2769          326 :     char *pkclause_identifiers = NULL;
    2770          326 :     int rc = database_select_text(data, sql, &pkclause_identifiers);
    2771          326 :     cloudsync_memory_free(sql);
    2772          326 :     if (rc != DBRES_OK) goto finalize;
    2773          326 :     char *pkvalues_identifiers = (pkclause_identifiers) ? pkclause_identifiers : "rowid";
    2774              : 
    2775              :     // Use database-specific query builder to handle type differences in composite PKs
    2776          326 :     sql = sql_build_insert_missing_pks_query(schema, table_name, pkvalues_identifiers, table->base_ref, table->meta_ref, filter);
    2777          326 :     if (!sql) {rc = DBRES_NOMEM; goto finalize;}
    2778          326 :     rc = database_exec(data, sql);
    2779          326 :     cloudsync_memory_free(sql);
    2780          326 :     if (rc != DBRES_OK) goto finalize;
    2781              : 
    2782              :     // fill missing colums
    2783              :     // for each non-pk column:
    2784              :     // The new query does 1 encode per source row and one indexed NOT-EXISTS probe.
    2785              :     // 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.
    2786              : 
    2787          326 :     if (filter) {
    2788           20 :         sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_SELECT_PKS_NOT_IN_SYNC_FOR_COL_FILTERED, pkvalues_identifiers, table->base_ref, filter, table->meta_ref);
    2789           20 :     } else {
    2790          306 :         sql = cloudsync_memory_mprintf(SQL_CLOUDSYNC_SELECT_PKS_NOT_IN_SYNC_FOR_COL, pkvalues_identifiers, table->base_ref, table->meta_ref);
    2791              :     }
    2792          326 :     rc = databasevm_prepare(data, sql, (void **)&vm, DBFLAG_PERSISTENT);
    2793          326 :     cloudsync_memory_free(sql);
    2794          326 :     if (rc != DBRES_OK) goto finalize;
    2795              :      
    2796         1508 :     for (int i=0; i<table->ncols; ++i) {
    2797         1182 :         char *col_name = table->col_name[i];
    2798              : 
    2799         1182 :         rc = databasevm_bind_text(vm, 1, col_name, -1);
    2800         1182 :         if (rc != DBRES_OK) goto finalize;
    2801              : 
    2802         1220 :         while (1) {
    2803         1220 :             rc = databasevm_step(vm);
    2804         1220 :             if (rc == DBRES_ROW) {
    2805           38 :                 size_t pklen = 0;
    2806           38 :                 const void *pk = (const char *)database_column_blob(vm, 0, &pklen);
    2807           38 :                 if (!pk) { rc = DBRES_ERROR; break; }
    2808           38 :                 rc = local_mark_insert_or_update_meta(table, pk, pklen, col_name, db_version, cloudsync_bumpseq(data));
    2809         1220 :             } else if (rc == DBRES_DONE) {
    2810         1182 :                 rc = DBRES_OK;
    2811         1182 :                 break;
    2812              :             } else {
    2813            0 :                 break;
    2814              :             }
    2815              :         }
    2816         1182 :         if (rc != DBRES_OK) goto finalize;
    2817              : 
    2818         1182 :         databasevm_reset(vm);
    2819         1508 :     }
    2820              :     
    2821              : finalize:
    2822          326 :     if (rc != DBRES_OK) {DEBUG_ALWAYS("cloudsync_refill_metatable error: %s", database_errmsg(data));}
    2823          326 :     if (pkclause_identifiers) cloudsync_memory_free(pkclause_identifiers);
    2824          326 :     if (vm) databasevm_finalize(vm);
    2825          326 :     return rc;
    2826          326 : }
    2827              : 
    2828              : // MARK: - Local -
    2829              : 
    2830            4 : int local_update_sentinel (cloudsync_table_context *table, const void *pk, size_t pklen, int64_t db_version, int seq) {
    2831            4 :     dbvm_t *vm = table->meta_sentinel_update_stmt;
    2832            4 :     if (!vm) return -1;
    2833              :     
    2834            4 :     int rc = databasevm_bind_int(vm, 1, db_version);
    2835            4 :     if (rc != DBRES_OK) goto cleanup;
    2836              :     
    2837            4 :     rc = databasevm_bind_int(vm, 2, seq);
    2838            4 :     if (rc != DBRES_OK) goto cleanup;
    2839              :     
    2840            4 :     rc = databasevm_bind_blob(vm, 3, pk, (int)pklen);
    2841            4 :     if (rc != DBRES_OK) goto cleanup;
    2842              :     
    2843            4 :     rc = databasevm_step(vm);
    2844            4 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    2845              :     
    2846              : cleanup:
    2847            4 :     DEBUG_DBERROR(rc, "local_update_sentinel", table->context);
    2848            4 :     databasevm_reset(vm);
    2849            4 :     return rc;
    2850            4 : }
    2851              : 
    2852          126 : int local_mark_insert_sentinel_meta (cloudsync_table_context *table, const void *pk, size_t pklen, int64_t db_version, int seq) {
    2853          126 :     dbvm_t *vm = table->meta_sentinel_insert_stmt;
    2854          126 :     if (!vm) return -1;
    2855              :     
    2856          126 :     int rc = databasevm_bind_blob(vm, 1, pk, (int)pklen);
    2857          126 :     if (rc != DBRES_OK) goto cleanup;
    2858              :     
    2859          126 :     rc = databasevm_bind_int(vm, 2, db_version);
    2860          126 :     if (rc != DBRES_OK) goto cleanup;
    2861              :     
    2862          126 :     rc = databasevm_bind_int(vm, 3, seq);
    2863          126 :     if (rc != DBRES_OK) goto cleanup;
    2864              :     
    2865          126 :     rc = databasevm_bind_int(vm, 4, db_version);
    2866          126 :     if (rc != DBRES_OK) goto cleanup;
    2867              :     
    2868          126 :     rc = databasevm_bind_int(vm, 5, seq);
    2869          126 :     if (rc != DBRES_OK) goto cleanup;
    2870              :     
    2871          126 :     rc = databasevm_step(vm);
    2872          126 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    2873              :     
    2874              : cleanup:
    2875          126 :     DEBUG_DBERROR(rc, "local_insert_sentinel", table->context);
    2876          126 :     databasevm_reset(vm);
    2877          126 :     return rc;
    2878          126 : }
    2879              : 
    2880        13493 : int local_mark_insert_or_update_meta_impl (cloudsync_table_context *table, const void *pk, size_t pklen, const char *col_name, int col_version, int64_t db_version, int seq) {
    2881              :     
    2882        13493 :     dbvm_t *vm = table->meta_row_insert_update_stmt;
    2883        13493 :     if (!vm) return -1;
    2884              :     
    2885        13493 :     int rc = databasevm_bind_blob(vm, 1, pk, pklen);
    2886        13493 :     if (rc != DBRES_OK) goto cleanup;
    2887              :     
    2888        13493 :     rc = databasevm_bind_text(vm, 2, (col_name) ? col_name : CLOUDSYNC_TOMBSTONE_VALUE, -1);
    2889        13493 :     if (rc != DBRES_OK) goto cleanup;
    2890              :     
    2891        13493 :     rc = databasevm_bind_int(vm, 3, col_version);
    2892        13493 :     if (rc != DBRES_OK) goto cleanup;
    2893              :     
    2894        13493 :     rc = databasevm_bind_int(vm, 4, db_version);
    2895        13493 :     if (rc != DBRES_OK) goto cleanup;
    2896              :     
    2897        13493 :     rc = databasevm_bind_int(vm, 5, seq);
    2898        13493 :     if (rc != DBRES_OK) goto cleanup;
    2899              :     
    2900        13493 :     rc = databasevm_bind_int(vm, 6, db_version);
    2901        13493 :     if (rc != DBRES_OK) goto cleanup;
    2902              :     
    2903        13493 :     rc = databasevm_bind_int(vm, 7, seq);
    2904        13493 :     if (rc != DBRES_OK) goto cleanup;
    2905              :     
    2906        13493 :     rc = databasevm_step(vm);
    2907        13493 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    2908              :     
    2909              : cleanup:
    2910        13493 :     DEBUG_DBERROR(rc, "local_insert_or_update", table->context);
    2911        13493 :     databasevm_reset(vm);
    2912        13493 :     return rc;
    2913        13493 : }
    2914              : 
    2915        13387 : int local_mark_insert_or_update_meta (cloudsync_table_context *table, const void *pk, size_t pklen, const char *col_name, int64_t db_version, int seq) {
    2916        13387 :     return local_mark_insert_or_update_meta_impl(table, pk, pklen, col_name, 1, db_version, seq);
    2917              : }
    2918              : 
    2919           41 : int local_mark_delete_block_meta (cloudsync_table_context *table, const void *pk, size_t pklen, const char *block_colname, int64_t db_version, int seq) {
    2920              :     // Mark a block as deleted by setting col_version = 2 (even = deleted)
    2921           41 :     return local_mark_insert_or_update_meta_impl(table, pk, pklen, block_colname, 2, db_version, seq);
    2922              : }
    2923              : 
    2924           41 : int block_delete_value_external (cloudsync_context *data, cloudsync_table_context *table, const void *pk, size_t pklen, const char *block_colname) {
    2925           41 :     return block_delete_value(data, table, pk, (int)pklen, block_colname);
    2926              : }
    2927              : 
    2928           65 : int local_mark_delete_meta (cloudsync_table_context *table, const void *pk, size_t pklen, int64_t db_version, int seq) {
    2929           65 :     return local_mark_insert_or_update_meta_impl(table, pk, pklen, NULL, 2, db_version, seq);
    2930              : }
    2931              : 
    2932           36 : int local_drop_meta (cloudsync_table_context *table, const void *pk, size_t pklen) {
    2933           36 :     dbvm_t *vm = table->meta_row_drop_stmt;
    2934           36 :     if (!vm) return -1;
    2935              :     
    2936           36 :     int rc = databasevm_bind_blob(vm, 1, pk, pklen);
    2937           36 :     if (rc != DBRES_OK) goto cleanup;
    2938              :     
    2939           36 :     rc = databasevm_step(vm);
    2940           36 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    2941              :     
    2942              : cleanup:
    2943           36 :     DEBUG_DBERROR(rc, "local_drop_meta", table->context);
    2944           36 :     databasevm_reset(vm);
    2945           36 :     return rc;
    2946           36 : }
    2947              : 
    2948           29 : int local_update_move_meta (cloudsync_table_context *table, const void *pk, size_t pklen, const void *pk2, size_t pklen2, int64_t db_version) {
    2949              :     /*
    2950              :       * This function moves non-sentinel metadata entries from an old primary key (OLD.pk)
    2951              :       * to a new primary key (NEW.pk) when a primary key change occurs.
    2952              :       *
    2953              :       * To ensure consistency and proper conflict resolution in a CRDT (Conflict-free Replicated Data Type) system,
    2954              :       * each non-sentinel metadata entry involved in the move must have a unique sequence value (seq).
    2955              :       *
    2956              :       * The `seq` is crucial for tracking the order of operations and for detecting and resolving conflicts
    2957              :       * during synchronization between replicas. Without a unique `seq` for each entry, concurrent updates
    2958              :       * may be applied incorrectly, leading to data inconsistency.
    2959              :       *
    2960              :       * When performing the update, a unique `seq` must be assigned to each metadata row. This can be achieved
    2961              :       * by either incrementing the maximum sequence value in the table or using a function (e.g., cloudsync_bumpseq(data))
    2962              :       * that generates a unique sequence for each row. The update query should ensure that each row moved
    2963              :       * from OLD.pk to NEW.pk gets a distinct `seq` to maintain proper versioning and ordering of changes.
    2964              :      */
    2965              :     
    2966              :     // see https://github.com/sqliteai/sqlite-sync/blob/main/docs/PriKey.md for more details
    2967              :     // pk2 is the old pk
    2968              :     
    2969           29 :     dbvm_t *vm = table->meta_update_move_stmt;
    2970           29 :     if (!vm) return -1;
    2971              :     
    2972              :     // new primary key
    2973           29 :     int rc = databasevm_bind_blob(vm, 1, pk, pklen);
    2974           29 :     if (rc != DBRES_OK) goto cleanup;
    2975              :     
    2976              :     // new db_version
    2977           29 :     rc = databasevm_bind_int(vm, 2, db_version);
    2978           29 :     if (rc != DBRES_OK) goto cleanup;
    2979              :     
    2980              :     // old primary key
    2981           29 :     rc = databasevm_bind_blob(vm, 3, pk2, pklen2);
    2982           29 :     if (rc != DBRES_OK) goto cleanup;
    2983              :     
    2984           29 :     rc = databasevm_step(vm);
    2985           29 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    2986              :     
    2987              : cleanup:
    2988           29 :     DEBUG_DBERROR(rc, "local_update_move_meta", table->context);
    2989           29 :     databasevm_reset(vm);
    2990           29 :     return rc;
    2991           29 : }
    2992              : 
    2993              : // MARK: - Payload Encode / Decode -
    2994              : 
    2995          743 : static void cloudsync_payload_checksum_store (cloudsync_payload_header *header, uint64_t checksum) {
    2996          743 :     uint64_t h = checksum & 0xFFFFFFFFFFFFULL; // keep 48 bits
    2997          743 :     header->checksum[0] = (uint8_t)(h >> 40);
    2998          743 :     header->checksum[1] = (uint8_t)(h >> 32);
    2999          743 :     header->checksum[2] = (uint8_t)(h >> 24);
    3000          743 :     header->checksum[3] = (uint8_t)(h >> 16);
    3001          743 :     header->checksum[4] = (uint8_t)(h >>  8);
    3002          743 :     header->checksum[5] = (uint8_t)(h >>  0);
    3003          743 : }
    3004              : 
    3005          748 : static uint64_t cloudsync_payload_checksum_load (cloudsync_payload_header *header) {
    3006         2244 :     return ((uint64_t)header->checksum[0] << 40) |
    3007         1496 :            ((uint64_t)header->checksum[1] << 32) |
    3008         1496 :            ((uint64_t)header->checksum[2] << 24) |
    3009         1496 :            ((uint64_t)header->checksum[3] << 16) |
    3010         1496 :            ((uint64_t)header->checksum[4] <<  8) |
    3011          748 :            ((uint64_t)header->checksum[5] <<  0);
    3012              : }
    3013              : 
    3014          748 : static bool cloudsync_payload_checksum_verify (cloudsync_payload_header *header, uint64_t checksum) {
    3015          748 :     uint64_t checksum1 = cloudsync_payload_checksum_load(header);
    3016          748 :     uint64_t checksum2 = checksum & 0xFFFFFFFFFFFFULL;
    3017          748 :     return (checksum1 == checksum2);
    3018              : }
    3019              : 
    3020        52295 : static bool cloudsync_payload_encode_check (cloudsync_payload_context *payload, size_t needed) {
    3021        52295 :     if (payload->nrows == 0) needed += sizeof(cloudsync_payload_header);
    3022              :     
    3023              :     // alloc/resize buffer
    3024        52295 :     if (payload->bused + needed > payload->balloc) {
    3025          762 :         if (needed < CLOUDSYNC_PAYLOAD_MINBUF_SIZE) needed = CLOUDSYNC_PAYLOAD_MINBUF_SIZE;
    3026          762 :         size_t balloc = payload->balloc + needed;
    3027              :         
    3028          762 :         char *buffer = cloudsync_memory_realloc(payload->buffer, balloc);
    3029          762 :         if (!buffer) {
    3030            0 :             if (payload->buffer) cloudsync_memory_free(payload->buffer);
    3031            0 :             memset(payload, 0, sizeof(cloudsync_payload_context));
    3032            0 :             return false;
    3033              :         }
    3034              :         
    3035          762 :         payload->buffer = buffer;
    3036          762 :         payload->balloc = balloc;
    3037          762 :         if (payload->nrows == 0) payload->bused = sizeof(cloudsync_payload_header);
    3038          762 :     }
    3039              :     
    3040        52295 :     return true;
    3041        52295 : }
    3042              : 
    3043        51241 : size_t cloudsync_payload_context_size (size_t *header_size) {
    3044        51241 :     if (header_size) *header_size = sizeof(cloudsync_payload_header);
    3045        51241 :     return sizeof(cloudsync_payload_context);
    3046              : }
    3047              : 
    3048            0 : void cloudsync_payload_context_free (cloudsync_payload_context *payload) {
    3049            0 :     if (!payload) return;
    3050            0 :     if (payload->buffer) cloudsync_memory_free(payload->buffer);
    3051            0 :     cloudsync_memory_free(payload);
    3052            0 : }
    3053              : 
    3054         4108 : uint64_t cloudsync_payload_context_nrows (cloudsync_payload_context *payload) {
    3055         4108 :     return payload ? payload->nrows : 0;
    3056              : }
    3057              : 
    3058         2021 : size_t cloudsync_payload_context_bused (cloudsync_payload_context *payload) {
    3059         2021 :     return payload ? payload->bused : 0;
    3060              : }
    3061              : 
    3062          743 : void cloudsync_payload_header_init (cloudsync_payload_header *header, uint8_t version, uint32_t expanded_size, uint16_t ncols, uint32_t nrows, uint64_t hash) {
    3063          743 :     memset(header, 0, sizeof(cloudsync_payload_header));
    3064              :     assert(sizeof(cloudsync_payload_header)==32);
    3065              :     
    3066              :     int major, minor, patch;
    3067          743 :     sscanf(CLOUDSYNC_VERSION, "%d.%d.%d", &major, &minor, &patch);
    3068              :     
    3069          743 :     header->signature = htonl(CLOUDSYNC_PAYLOAD_SIGNATURE);
    3070          743 :     header->version = version;
    3071          743 :     header->libversion[0] = (uint8_t)major;
    3072          743 :     header->libversion[1] = (uint8_t)minor;
    3073          743 :     header->libversion[2] = (uint8_t)patch;
    3074          743 :     header->expanded_size = htonl(expanded_size);
    3075          743 :     header->ncols = htons(ncols);
    3076          743 :     header->nrows = htonl(nrows);
    3077          743 :     header->schema_hash = htonll(hash);
    3078          743 : }
    3079              : 
    3080        52263 : int cloudsync_payload_encode_step (cloudsync_payload_context *payload, cloudsync_context *data, int argc, dbvalue_t **argv) {
    3081              :     DEBUG_FUNCTION("cloudsync_payload_encode_step");
    3082              :     // debug_values(argc, argv);
    3083              :     
    3084              :     // check if the step function is called for the first time
    3085        52263 :     if (payload->nrows == 0) payload->ncols = (uint16_t)argc;
    3086              :     
    3087        52263 :     size_t breq = pk_encode_size((dbvalue_t **)argv, argc, 0, data->skip_decode_idx);
    3088        52263 :     if (cloudsync_payload_encode_check(payload, breq) == false) {
    3089            0 :         return cloudsync_set_error(data, "Not enough memory to resize payload internal buffer", DBRES_NOMEM);
    3090              :     }
    3091              :     
    3092        52263 :     char *buffer = payload->buffer + payload->bused;
    3093        52263 :     size_t bsize = payload->balloc - payload->bused;
    3094        52263 :     char *p = pk_encode((dbvalue_t **)argv, argc, buffer, false, &bsize, data->skip_decode_idx);
    3095        52263 :     if (!p) return cloudsync_set_error(data, "An error occurred while encoding payload", DBRES_ERROR);
    3096              :     
    3097              :     // update buffer
    3098        52263 :     payload->bused += breq;
    3099              :     
    3100              :     // increment row counter
    3101        52263 :     ++payload->nrows;
    3102              :     
    3103        52263 :     return DBRES_OK;
    3104        52263 : }
    3105              : 
    3106           32 : static bool cloudsync_payload_append_raw (cloudsync_payload_context *payload, cloudsync_context *data, const char **fields, const size_t *field_sizes, int nfields, uint8_t version) {
    3107           32 :     size_t needed = 0;
    3108          320 :     for (int i = 0; i < nfields; ++i) {
    3109          288 :         if (field_sizes[i] > SIZE_MAX - needed) {
    3110            0 :             cloudsync_set_error(data, CLOUDSYNC_ERRCODE_ROW_TOO_LARGE "cloudsync payload raw row too large", DBRES_NOMEM);
    3111            0 :             return false;
    3112              :         }
    3113          288 :         needed += field_sizes[i];
    3114          288 :     }
    3115           32 :     if (!cloudsync_payload_encode_check(payload, needed)) {
    3116            0 :         cloudsync_set_error(data, "Not enough memory to resize payload internal buffer", DBRES_NOMEM);
    3117            0 :         return false;
    3118              :     }
    3119           32 :     if (payload->nrows == 0) {
    3120           32 :         payload->ncols = (uint16_t)nfields;
    3121           32 :         payload->version = version;
    3122           32 :     }
    3123           32 :     char *dst = payload->buffer + payload->bused;
    3124          320 :     for (int i = 0; i < nfields; ++i) {
    3125          288 :         memcpy(dst, fields[i], field_sizes[i]);
    3126          288 :         dst += field_sizes[i];
    3127          288 :     }
    3128           32 :     payload->bused += needed;
    3129           32 :     ++payload->nrows;
    3130           32 :     return true;
    3131           32 : }
    3132              : 
    3133           56 : int cloudsync_payload_max_chunk_size (cloudsync_context *data) {
    3134           56 :     int64_t value = dbutils_settings_get_int64_value(data, CLOUDSYNC_KEY_PAYLOAD_MAX_CHUNK_SIZE);
    3135           56 :     if (value <= 0) value = CLOUDSYNC_PAYLOAD_CHUNK_DEFAULT_SIZE;
    3136           56 :     if (value < CLOUDSYNC_PAYLOAD_CHUNK_MIN_SIZE) value = CLOUDSYNC_PAYLOAD_CHUNK_MIN_SIZE;
    3137           56 :     if (value > CLOUDSYNC_PAYLOAD_CHUNK_MAX_SIZE) value = CLOUDSYNC_PAYLOAD_CHUNK_MAX_SIZE;
    3138           56 :     return (int)value;
    3139              : }
    3140              : 
    3141            0 : int cloudsync_payload_fragment_target_size (cloudsync_context *data) {
    3142            0 :     int max_size = cloudsync_payload_max_chunk_size(data);
    3143            0 :     int target = max_size - (int)sizeof(cloudsync_payload_header) - CLOUDSYNC_PAYLOAD_CHUNK_SAFETY_MARGIN;
    3144            0 :     if (target < 1024) target = 1024;
    3145            0 :     return target;
    3146              : }
    3147              : 
    3148           84 : static size_t cloudsync_payload_decimal_len_i64 (int64_t value) {
    3149           84 :     size_t len = value < 0 ? 1 : 0;
    3150           84 :     uint64_t v = (value < 0) ? (uint64_t)(-(value + 1)) + 1u : (uint64_t)value;
    3151           84 :     do {
    3152          240 :         len++;
    3153          240 :         v /= 10u;
    3154          240 :     } while (v != 0);
    3155           84 :     return len;
    3156              : }
    3157              : 
    3158          224 : static bool cloudsync_payload_size_add (size_t *acc, size_t value) {
    3159          224 :     if (value > SIZE_MAX - *acc) return false;
    3160          224 :     *acc += value;
    3161          224 :     return true;
    3162          224 : }
    3163              : 
    3164           28 : int cloudsync_payload_fragment_count (int64_t total_size, int target_size) {
    3165           28 :     if (total_size <= 0 || target_size <= 0) return 0;
    3166           28 :     uint64_t total = (uint64_t)total_size;
    3167           28 :     uint64_t target = (uint64_t)target_size;
    3168           28 :     uint64_t count = total / target + ((total % target) != 0);
    3169           28 :     if (count == 0 || count > INT_MAX) return 0;
    3170           28 :     return (int)count;
    3171           28 : }
    3172              : 
    3173           28 : int cloudsync_payload_fragment_data_size (cloudsync_context *data,
    3174              :                                           const char *tbl, int tbl_len,
    3175              :                                           const void *pk, int pk_len,
    3176              :                                           const char *col_name, int col_name_len,
    3177              :                                           int64_t col_version, int64_t db_version,
    3178              :                                           const void *site_id, int site_id_len,
    3179              :                                           int64_t cl, int64_t seq,
    3180              :                                           int64_t total_size,
    3181              :                                           int part_index, int part_count) {
    3182           28 :     UNUSED_PARAMETER(pk);
    3183           28 :     UNUSED_PARAMETER(site_id);
    3184           28 :     if (tbl_len < 0 && tbl) tbl_len = (int)strlen(tbl);
    3185           28 :     if (col_name_len < 0 && col_name) col_name_len = (int)strlen(col_name);
    3186           28 :     if (tbl_len < 0 || pk_len < 0 || col_name_len < 0 || site_id_len < 0 || total_size < 0 || part_index < 0 || part_count <= 0) {
    3187            0 :         return 0;
    3188              :     }
    3189              : 
    3190           28 :     size_t fixed = sizeof(cloudsync_payload_header);
    3191           56 :     size_t frag_col_len = strlen(CLOUDSYNC_PAYLOAD_FRAGMENT_PREFIX) + 32 + 1 + 16 + 1 +
    3192           84 :                           cloudsync_payload_decimal_len_i64(part_index) + 1 +
    3193           84 :                           cloudsync_payload_decimal_len_i64(part_count) + 1 +
    3194           84 :                           cloudsync_payload_decimal_len_i64(total_size) + 1 +
    3195           28 :                           (size_t)col_name_len;
    3196          224 :     size_t sizes[] = {
    3197           28 :         pk_encode_raw_size(DBTYPE_TEXT, tbl_len),
    3198           28 :         pk_encode_raw_size(DBTYPE_BLOB, pk_len),
    3199           28 :         pk_encode_raw_size(DBTYPE_TEXT, (int64_t)frag_col_len),
    3200           28 :         pk_encode_raw_size(DBTYPE_INTEGER, col_version),
    3201           28 :         pk_encode_raw_size(DBTYPE_INTEGER, db_version),
    3202           28 :         pk_encode_raw_size(DBTYPE_BLOB, site_id_len),
    3203           28 :         pk_encode_raw_size(DBTYPE_INTEGER, cl),
    3204           28 :         pk_encode_raw_size(DBTYPE_INTEGER, seq)
    3205              :     };
    3206          252 :     for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); ++i) {
    3207          224 :         if (sizes[i] == SIZE_MAX || !cloudsync_payload_size_add(&fixed, sizes[i])) return 0;
    3208          224 :     }
    3209              : 
    3210           28 :     int max_size = cloudsync_payload_max_chunk_size(data);
    3211           28 :     if (fixed >= (size_t)max_size) return 0;
    3212              : 
    3213           28 :     size_t candidate = (size_t)max_size - fixed;
    3214           28 :     if (candidate > INT_MAX) candidate = INT_MAX;
    3215           56 :     while (candidate > 0) {
    3216           56 :         size_t frag_size = pk_encode_raw_size(DBTYPE_BLOB, (int64_t)candidate);
    3217           56 :         if (frag_size == SIZE_MAX) return 0;
    3218           56 :         if (fixed <= (size_t)max_size && frag_size <= (size_t)max_size - fixed) return (int)candidate;
    3219           28 :         size_t total = fixed + frag_size;
    3220           28 :         size_t over = total > (size_t)max_size ? total - (size_t)max_size : 1;
    3221           28 :         if (candidate <= over) return 0;
    3222           28 :         candidate -= over;
    3223              :     }
    3224            0 :     return 0;
    3225           28 : }
    3226              : 
    3227           28 : int cloudsync_payload_encoded_value_header (dbvalue_t *value, char *header, int header_cap, int64_t *payload_len) {
    3228           28 :     if (!value || !header || header_cap <= 0 || !payload_len) return -1;
    3229           28 :     int type = database_value_type(value);
    3230           28 :     *payload_len = 0;
    3231           28 :     if (type != DBTYPE_TEXT && type != DBTYPE_BLOB) return 0;
    3232           28 :     int64_t len = database_value_bytes(value);
    3233           28 :     if (len < 0) return -1;
    3234           28 :     *payload_len = len;
    3235           28 :     size_t total = pk_encode_raw_size(type, len);
    3236           28 :     if (total == SIZE_MAX || total < (size_t)len || total - (size_t)len > (size_t)header_cap) return -1;
    3237           28 :     if (type == DBTYPE_TEXT) {
    3238           18 :         size_t nbytes = pk_encode_raw_size(type, len) - (size_t)len - 1;
    3239           18 :         uint8_t type_byte = (uint8_t)((nbytes << 3) | DBTYPE_TEXT);
    3240           18 :         header[0] = (char)type_byte;
    3241           72 :         for (size_t i = 0; i < nbytes; i++) header[1 + i] = (uint8_t)(((uint64_t)len >> (8 * (nbytes - 1 - i))) & 0xFFu);
    3242           18 :         return (int)(1 + nbytes);
    3243              :     } else {
    3244           10 :         size_t nbytes = pk_encode_raw_size(type, len) - (size_t)len - 1;
    3245           10 :         uint8_t type_byte = (uint8_t)((nbytes << 3) | DBTYPE_BLOB);
    3246           10 :         header[0] = (char)type_byte;
    3247           40 :         for (size_t i = 0; i < nbytes; i++) header[1 + i] = (uint8_t)(((uint64_t)len >> (8 * (nbytes - 1 - i))) & 0xFFu);
    3248           10 :         return (int)(1 + nbytes);
    3249              :     }
    3250           28 : }
    3251              : 
    3252           14 : uint64_t cloudsync_payload_encoded_value_checksum (dbvalue_t *value) {
    3253           14 :     if (!value) return 0;
    3254           14 :     int type = database_value_type(value);
    3255           14 :     if (type != DBTYPE_TEXT && type != DBTYPE_BLOB) {
    3256            0 :         size_t len = pk_encode_size(&value, 1, 0, -1);
    3257              :         char stack[32];
    3258            0 :         char *buf = stack;
    3259            0 :         if (len > sizeof(stack)) buf = cloudsync_memory_alloc((uint64_t)len);
    3260            0 :         if (!buf) return 0;
    3261            0 :         size_t bsize = len;
    3262            0 :         pk_encode(&value, 1, buf, false, &bsize, -1);
    3263            0 :         uint64_t h = pk_checksum(buf, bsize);
    3264            0 :         if (buf != stack) cloudsync_memory_free(buf);
    3265            0 :         return h;
    3266              :     }
    3267              :     char header[16];
    3268           14 :     int64_t payload_len = 0;
    3269           14 :     int header_len = cloudsync_payload_encoded_value_header(value, header, sizeof(header), &payload_len);
    3270           14 :     if (header_len <= 0) return 0;
    3271           14 :     uint64_t h = pk_checksum(header, (size_t)header_len);
    3272           14 :     const char *p = (const char *)database_value_blob(value);
    3273           14 :     if (p && payload_len > 0) {
    3274           14 :         const uint8_t *bytes = (const uint8_t *)p;
    3275     18720014 :         for (int64_t i = 0; i < payload_len; ++i) {
    3276     18720000 :             h ^= bytes[i];
    3277     18720000 :             h *= 1099511628211ULL;
    3278     18720000 :         }
    3279           14 :     }
    3280           14 :     return h;
    3281           14 : }
    3282              : 
    3283         7634 : static uint64_t cloudsync_checksum_update (uint64_t h, const void *data, size_t len) {
    3284         7634 :     const uint8_t *p = (const uint8_t *)data;
    3285      9020067 :     for (size_t i = 0; i < len; ++i) {
    3286      9012433 :         h ^= p[i];
    3287      9012433 :         h *= 1099511628211ULL;
    3288      9012433 :     }
    3289         7634 :     return h;
    3290              : }
    3291              : 
    3292          852 : static uint64_t cloudsync_checksum_update_i64 (uint64_t h, int64_t value) {
    3293          852 :     uint64_t v = (uint64_t)value;
    3294         7668 :     for (int i = 7; i >= 0; --i) {
    3295         6816 :         uint8_t b = (uint8_t)((v >> (8 * i)) & 0xffu);
    3296         6816 :         h = cloudsync_checksum_update(h, &b, 1);
    3297         6816 :     }
    3298          852 :     return h;
    3299              : }
    3300              : 
    3301           71 : static void cloudsync_payload_fragment_value_id (char out[33],
    3302              :                                                  const char *tbl, int tbl_len,
    3303              :                                                  const void *pk, int pk_len,
    3304              :                                                  const char *col_name, int col_name_len,
    3305              :                                                  int64_t col_version, int64_t db_version,
    3306              :                                                  const void *site_id, int site_id_len,
    3307              :                                                  int64_t cl, int64_t seq,
    3308              :                                                  uint64_t value_checksum,
    3309              :                                                  int64_t total_size) {
    3310           71 :     uint64_t h1 = 14695981039346656037ULL;
    3311           71 :     uint64_t h2 = 1099511628211ULL;
    3312           71 :     const char sep = '\x1f';
    3313              : 
    3314           71 :     h1 = cloudsync_checksum_update(h1, tbl, (size_t)tbl_len);
    3315           71 :     h1 = cloudsync_checksum_update(h1, &sep, 1);
    3316           71 :     h1 = cloudsync_checksum_update(h1, pk, (size_t)pk_len);
    3317           71 :     h1 = cloudsync_checksum_update(h1, &sep, 1);
    3318           71 :     h1 = cloudsync_checksum_update(h1, col_name, (size_t)col_name_len);
    3319           71 :     h1 = cloudsync_checksum_update(h1, &sep, 1);
    3320           71 :     h1 = cloudsync_checksum_update(h1, site_id, (size_t)site_id_len);
    3321           71 :     h1 = cloudsync_checksum_update_i64(h1, col_version);
    3322           71 :     h1 = cloudsync_checksum_update_i64(h1, db_version);
    3323           71 :     h1 = cloudsync_checksum_update_i64(h1, cl);
    3324           71 :     h1 = cloudsync_checksum_update_i64(h1, seq);
    3325           71 :     h1 = cloudsync_checksum_update_i64(h1, (int64_t)value_checksum);
    3326           71 :     h1 = cloudsync_checksum_update_i64(h1, total_size);
    3327              : 
    3328           71 :     h2 = cloudsync_checksum_update_i64(h2, total_size);
    3329           71 :     h2 = cloudsync_checksum_update_i64(h2, (int64_t)value_checksum);
    3330           71 :     h2 = cloudsync_checksum_update(h2, site_id, (size_t)site_id_len);
    3331           71 :     h2 = cloudsync_checksum_update(h2, col_name, (size_t)col_name_len);
    3332           71 :     h2 = cloudsync_checksum_update(h2, pk, (size_t)pk_len);
    3333           71 :     h2 = cloudsync_checksum_update(h2, tbl, (size_t)tbl_len);
    3334           71 :     h2 = cloudsync_checksum_update_i64(h2, seq);
    3335           71 :     h2 = cloudsync_checksum_update_i64(h2, cl);
    3336           71 :     h2 = cloudsync_checksum_update_i64(h2, db_version);
    3337           71 :     h2 = cloudsync_checksum_update_i64(h2, col_version);
    3338              : 
    3339           71 :     snprintf(out, 33, "%016" PRIx64 "%016" PRIx64, h1, h2);
    3340           71 : }
    3341              : 
    3342           32 : int cloudsync_payload_encode_fragment_step (cloudsync_payload_context *payload, cloudsync_context *data,
    3343              :                                             const char *tbl, int tbl_len,
    3344              :                                             const void *pk, int pk_len,
    3345              :                                             const char *col_name, int col_name_len,
    3346              :                                             const void *fragment, int fragment_len,
    3347              :                                             int64_t col_version, int64_t db_version,
    3348              :                                             const void *site_id, int site_id_len,
    3349              :                                             int64_t cl, int64_t seq,
    3350              :                                             uint64_t value_checksum,
    3351              :                                             int64_t total_size,
    3352              :                                             int part_index, int part_count) {
    3353           32 :     if (!payload || !data || !tbl || !pk || !col_name || !fragment || !site_id) return DBRES_MISUSE;
    3354           32 :     if (tbl_len < 0) tbl_len = (int)strlen(tbl);
    3355           32 :     if (col_name_len < 0) col_name_len = (int)strlen(col_name);
    3356           64 :     if (tbl_len < 0 || pk_len < 0 || col_name_len < 0 || fragment_len <= 0 || site_id_len < 0 ||
    3357           32 :         total_size <= 0 || part_index < 0 || part_count <= 0 || part_index >= part_count) {
    3358            0 :         return DBRES_MISUSE;
    3359              :     }
    3360              : 
    3361              :     char value_id[33];
    3362              :     char checksum_hex[17];
    3363           64 :     cloudsync_payload_fragment_value_id(value_id, tbl, tbl_len, pk, pk_len, col_name, col_name_len,
    3364           32 :                                         col_version, db_version, site_id, site_id_len, cl, seq,
    3365           32 :                                         value_checksum, total_size);
    3366           32 :     snprintf(checksum_hex, sizeof(checksum_hex), "%016" PRIx64, value_checksum);
    3367              : 
    3368           32 :     char *frag_col = cloudsync_memory_mprintf("%s%s:%s:%d:%d:%" PRId64 ":%.*s",
    3369              :                                               CLOUDSYNC_PAYLOAD_FRAGMENT_PREFIX,
    3370           32 :                                               value_id, checksum_hex, part_index, part_count, total_size,
    3371           32 :                                               col_name_len, col_name);
    3372           32 :     if (!frag_col) return DBRES_NOMEM;
    3373              : 
    3374           32 :     size_t sizes[9] = {0};
    3375           32 :     sizes[0] = pk_encode_raw_size(DBTYPE_TEXT, tbl_len);
    3376           32 :     sizes[1] = pk_encode_raw_size(DBTYPE_BLOB, pk_len);
    3377           32 :     sizes[2] = pk_encode_raw_size(DBTYPE_TEXT, (int64_t)strlen(frag_col));
    3378           32 :     sizes[3] = pk_encode_raw_size(DBTYPE_BLOB, fragment_len);
    3379           32 :     sizes[4] = pk_encode_raw_size(DBTYPE_INTEGER, col_version);
    3380           32 :     sizes[5] = pk_encode_raw_size(DBTYPE_INTEGER, db_version);
    3381           32 :     sizes[6] = pk_encode_raw_size(DBTYPE_BLOB, site_id_len);
    3382           32 :     sizes[7] = pk_encode_raw_size(DBTYPE_INTEGER, cl);
    3383           32 :     sizes[8] = pk_encode_raw_size(DBTYPE_INTEGER, seq);
    3384          320 :     for (int i = 0; i < 9; ++i) {
    3385          288 :         if (sizes[i] == SIZE_MAX) { cloudsync_memory_free(frag_col); return DBRES_NOMEM; }
    3386          288 :     }
    3387              : 
    3388              :     char stack[9][64];
    3389           32 :     char *fields[9] = {0};
    3390          320 :     for (int i = 0; i < 9; ++i) {
    3391          288 :         fields[i] = sizes[i] <= sizeof(stack[0]) ? stack[i] : cloudsync_memory_alloc((uint64_t)sizes[i]);
    3392          288 :         if (!fields[i]) {
    3393            0 :             for (int j = 0; j < i; ++j) if (fields[j] && (fields[j] < (char *)stack || fields[j] >= (char *)(stack + 9))) cloudsync_memory_free(fields[j]);
    3394            0 :             cloudsync_memory_free(frag_col);
    3395            0 :             return DBRES_NOMEM;
    3396              :         }
    3397          288 :     }
    3398              : 
    3399           32 :     pk_encode_raw_text(fields[0], tbl, (size_t)tbl_len);
    3400           32 :     pk_encode_raw_blob(fields[1], pk, (size_t)pk_len);
    3401           32 :     pk_encode_raw_text(fields[2], frag_col, strlen(frag_col));
    3402           32 :     pk_encode_raw_blob(fields[3], fragment, (size_t)fragment_len);
    3403           32 :     pk_encode_raw_int(fields[4], col_version);
    3404           32 :     pk_encode_raw_int(fields[5], db_version);
    3405           32 :     pk_encode_raw_blob(fields[6], site_id, (size_t)site_id_len);
    3406           32 :     pk_encode_raw_int(fields[7], cl);
    3407           32 :     pk_encode_raw_int(fields[8], seq);
    3408              : 
    3409              :     const char *cfields[9];
    3410          320 :     for (int i = 0; i < 9; ++i) cfields[i] = fields[i];
    3411           32 :     bool ok = cloudsync_payload_append_raw(payload, data, cfields, sizes, 9, CLOUDSYNC_PAYLOAD_VERSION_3);
    3412              : 
    3413          320 :     for (int i = 0; i < 9; ++i) {
    3414          288 :         if (!(fields[i] >= (char *)stack && fields[i] < (char *)(stack + 9))) cloudsync_memory_free(fields[i]);
    3415          288 :     }
    3416           32 :     cloudsync_memory_free(frag_col);
    3417           32 :     return ok ? DBRES_OK : cloudsync_errcode(data);
    3418           32 : }
    3419              : 
    3420          751 : int cloudsync_payload_encode_final (cloudsync_payload_context *payload, cloudsync_context *data) {
    3421              :     DEBUG_FUNCTION("cloudsync_payload_encode_final");
    3422              :     
    3423          751 :     if (payload->nrows == 0) {
    3424            8 :         if (payload->buffer) cloudsync_memory_free(payload->buffer);
    3425            8 :         payload->buffer = NULL;
    3426            8 :         payload->bsize = 0;
    3427            8 :         return DBRES_OK;
    3428              :     }
    3429              :     
    3430          743 :     if (payload->nrows > UINT32_MAX) {
    3431            0 :         if (payload->buffer) cloudsync_memory_free(payload->buffer);
    3432            0 :         payload->buffer = NULL;
    3433            0 :         payload->bsize = 0;
    3434            0 :         cloudsync_set_error(data, "Maximum number of payload rows reached", DBRES_ERROR);
    3435            0 :         return DBRES_ERROR;
    3436              :     }
    3437              :     
    3438              :     // sanity check about buffer size
    3439          743 :     int header_size = (int)sizeof(cloudsync_payload_header);
    3440          743 :     int64_t buffer_size = (int64_t)payload->bused - (int64_t)header_size;
    3441          743 :     if (buffer_size < 0) {
    3442            0 :         if (payload->buffer) cloudsync_memory_free(payload->buffer);
    3443            0 :         payload->buffer = NULL;
    3444            0 :         payload->bsize = 0;
    3445            0 :         cloudsync_set_error(data, "cloudsync_encode: internal size underflow", DBRES_ERROR);
    3446            0 :         return DBRES_ERROR;
    3447              :     }
    3448          743 :     if (buffer_size > INT_MAX) {
    3449            0 :         if (payload->buffer) cloudsync_memory_free(payload->buffer);
    3450            0 :         payload->buffer = NULL;
    3451            0 :         payload->bsize = 0;
    3452            0 :         cloudsync_set_error(data, CLOUDSYNC_ERRCODE_PAYLOAD_TOO_LARGE "cloudsync_encode: payload too large to compress (INT_MAX limit)", DBRES_ERROR);
    3453            0 :         return DBRES_ERROR;
    3454              :     }
    3455              :     // try to allocate buffer used for compressed data
    3456          743 :     int real_buffer_size = (int)buffer_size;
    3457          743 :     int zbound = LZ4_compressBound(real_buffer_size);
    3458          743 :     char *zbuffer = cloudsync_memory_alloc(zbound + header_size); // if for some reasons allocation fails then just skip compression
    3459              :     
    3460              :     // skip the reserved header from the buffer to compress
    3461          743 :     char *src_buffer = payload->buffer + sizeof(cloudsync_payload_header);
    3462          743 :     int zused = (zbuffer) ? LZ4_compress_default(src_buffer, zbuffer+header_size, real_buffer_size, zbound) : 0;
    3463          743 :     bool use_uncompressed_buffer = (!zused || zused > real_buffer_size);
    3464          743 :     CHECK_FORCE_UNCOMPRESSED_BUFFER();
    3465              :     
    3466              :     // setup payload header
    3467          743 :     cloudsync_payload_header header = {0};
    3468          743 :     uint32_t expanded_size = (use_uncompressed_buffer) ? 0 : real_buffer_size;
    3469          743 :     uint8_t version = payload->version ? payload->version : CLOUDSYNC_PAYLOAD_VERSION_LATEST;
    3470          743 :     cloudsync_payload_header_init(&header, version, expanded_size, payload->ncols, (uint32_t)payload->nrows, data->schema_hash);
    3471              :     
    3472              :     // if compression fails or if compressed size is bigger than original buffer, then use the uncompressed buffer
    3473          743 :     if (use_uncompressed_buffer) {
    3474           44 :         if (zbuffer) cloudsync_memory_free(zbuffer);
    3475           44 :         zbuffer = payload->buffer;
    3476           44 :         zused = real_buffer_size;
    3477           44 :     }
    3478              :     
    3479              :     // compute checksum of the buffer
    3480          743 :     uint64_t checksum = pk_checksum(zbuffer + header_size, zused);
    3481          743 :     cloudsync_payload_checksum_store(&header, checksum);
    3482              :     
    3483              :     // copy header and data to SQLite BLOB
    3484          743 :     memcpy(zbuffer, &header, sizeof(cloudsync_payload_header));
    3485          743 :     int blob_size = zused + sizeof(cloudsync_payload_header);
    3486          743 :     payload->bsize = blob_size;
    3487              :     
    3488              :     // cleanup memory
    3489          743 :     if (zbuffer != payload->buffer) {
    3490          699 :         cloudsync_memory_free (payload->buffer);
    3491          699 :         payload->buffer = zbuffer;
    3492          699 :     }
    3493              :     
    3494          743 :     return DBRES_OK;
    3495          751 : }
    3496              : 
    3497          751 : char *cloudsync_payload_blob (cloudsync_payload_context *payload, int64_t *blob_size, int64_t *nrows) {
    3498              :     DEBUG_FUNCTION("cloudsync_payload_blob");
    3499              :     
    3500          751 :     if (blob_size) *blob_size = (int64_t)payload->bsize;
    3501          751 :     if (nrows) *nrows = (int64_t)payload->nrows;
    3502          751 :     return payload->buffer;
    3503              : }
    3504              : 
    3505       462132 : static int cloudsync_payload_decode_callback (void *xdata, int index, int type, int64_t ival, double dval, char *pval) {
    3506       462132 :     cloudsync_pk_decode_bind_context *decode_context = (cloudsync_pk_decode_bind_context*)xdata;
    3507       462132 :     int rc = pk_decode_bind_callback(decode_context->vm, index, type, ival, dval, pval);
    3508              :     
    3509       462132 :     if (rc == DBRES_OK) {
    3510              :         // the dbversion index is smaller than seq index, so it is processed first
    3511              :         // when processing the dbversion column: save the value to the tmp_dbversion field
    3512              :         // when processing the seq column: update the dbversion and seq fields only if the current dbversion is greater than the last max value
    3513       462132 :         switch (index) {
    3514              :             case CLOUDSYNC_PK_INDEX_TBL:
    3515        51348 :                 if (type == DBTYPE_TEXT) {
    3516        51348 :                     decode_context->tbl = pval;
    3517        51348 :                     decode_context->tbl_len = ival;
    3518        51348 :                 }
    3519        51348 :                 break;
    3520              :             case CLOUDSYNC_PK_INDEX_PK:
    3521        51348 :                 if (type == DBTYPE_BLOB) {
    3522        51348 :                     decode_context->pk = pval;
    3523        51348 :                     decode_context->pk_len = ival;
    3524        51348 :                 }
    3525        51348 :                 break;
    3526              :             case CLOUDSYNC_PK_INDEX_COLNAME:
    3527        51348 :                 if (type == DBTYPE_TEXT) {
    3528        51348 :                     decode_context->col_name = pval;
    3529        51348 :                     decode_context->col_name_len = ival;
    3530        51348 :                 }
    3531        51348 :                 break;
    3532              :             case CLOUDSYNC_PK_INDEX_COLVERSION:
    3533        51348 :                 if (type == DBTYPE_INTEGER) decode_context->col_version = ival;
    3534        51348 :                 break;
    3535              :             case CLOUDSYNC_PK_INDEX_DBVERSION:
    3536        51348 :                 if (type == DBTYPE_INTEGER) decode_context->db_version = ival;
    3537        51348 :                 break;
    3538              :             case CLOUDSYNC_PK_INDEX_SITEID:
    3539        51348 :                 if (type == DBTYPE_BLOB) {
    3540        51348 :                     decode_context->site_id = pval;
    3541        51348 :                     decode_context->site_id_len = ival;
    3542        51348 :                 }
    3543        51348 :                 break;
    3544              :             case CLOUDSYNC_PK_INDEX_CL:
    3545        51348 :                 if (type == DBTYPE_INTEGER) decode_context->cl = ival;
    3546        51348 :                 break;
    3547              :             case CLOUDSYNC_PK_INDEX_SEQ:
    3548        51348 :                 if (type == DBTYPE_INTEGER) decode_context->seq = ival;
    3549        51348 :                 break;
    3550              :         }
    3551       462132 :     }
    3552              :         
    3553       462132 :     return rc;
    3554              : }
    3555              : 
    3556              : typedef struct {
    3557              :     const char *tbl;
    3558              :     int64_t tbl_len;
    3559              :     const void *pk;
    3560              :     int64_t pk_len;
    3561              :     const char *col_name;
    3562              :     int64_t col_name_len;
    3563              :     const void *col_value;
    3564              :     int64_t col_value_len;
    3565              :     int64_t col_version;
    3566              :     int64_t db_version;
    3567              :     const void *site_id;
    3568              :     int64_t site_id_len;
    3569              :     int64_t cl;
    3570              :     int64_t seq;
    3571              : } cloudsync_payload_fragment_row;
    3572              : 
    3573          351 : static int cloudsync_payload_fragment_decode_callback (void *xdata, int index, int type, int64_t ival, double dval, char *pval) {
    3574          351 :     UNUSED_PARAMETER(dval);
    3575          351 :     cloudsync_payload_fragment_row *row = (cloudsync_payload_fragment_row *)xdata;
    3576          351 :     switch (index) {
    3577              :         case CLOUDSYNC_PK_INDEX_TBL:
    3578           39 :             if (type != DBTYPE_TEXT) return DBRES_ERROR;
    3579           39 :             row->tbl = pval; row->tbl_len = ival;
    3580           39 :             break;
    3581              :         case CLOUDSYNC_PK_INDEX_PK:
    3582           39 :             if (type != DBTYPE_BLOB) return DBRES_ERROR;
    3583           39 :             row->pk = pval; row->pk_len = ival;
    3584           39 :             break;
    3585              :         case CLOUDSYNC_PK_INDEX_COLNAME:
    3586           39 :             if (type != DBTYPE_TEXT) return DBRES_ERROR;
    3587           39 :             row->col_name = pval; row->col_name_len = ival;
    3588           39 :             break;
    3589              :         case CLOUDSYNC_PK_INDEX_COLVALUE:
    3590           39 :             if (type != DBTYPE_BLOB) return DBRES_ERROR;
    3591           39 :             row->col_value = pval; row->col_value_len = ival;
    3592           39 :             break;
    3593              :         case CLOUDSYNC_PK_INDEX_COLVERSION:
    3594           39 :             if (type != DBTYPE_INTEGER) return DBRES_ERROR;
    3595           39 :             row->col_version = ival;
    3596           39 :             break;
    3597              :         case CLOUDSYNC_PK_INDEX_DBVERSION:
    3598           39 :             if (type != DBTYPE_INTEGER) return DBRES_ERROR;
    3599           39 :             row->db_version = ival;
    3600           39 :             break;
    3601              :         case CLOUDSYNC_PK_INDEX_SITEID:
    3602           39 :             if (type != DBTYPE_BLOB) return DBRES_ERROR;
    3603           39 :             row->site_id = pval; row->site_id_len = ival;
    3604           39 :             break;
    3605              :         case CLOUDSYNC_PK_INDEX_CL:
    3606           39 :             if (type != DBTYPE_INTEGER) return DBRES_ERROR;
    3607           39 :             row->cl = ival;
    3608           39 :             break;
    3609              :         case CLOUDSYNC_PK_INDEX_SEQ:
    3610           39 :             if (type != DBTYPE_INTEGER) return DBRES_ERROR;
    3611           39 :             row->seq = ival;
    3612           39 :             break;
    3613              :     }
    3614          351 :     return DBRES_OK;
    3615          351 : }
    3616              : 
    3617           78 : static bool cloudsync_payload_is_hex (const char *value, size_t len) {
    3618         1950 :     for (size_t i = 0; i < len; ++i) {
    3619         1872 :         char c = value[i];
    3620         1872 :         if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) return false;
    3621         1872 :     }
    3622           78 :     return true;
    3623           78 : }
    3624              : 
    3625          117 : static bool cloudsync_payload_parse_u64_segment (const char *start, const char *end, uint64_t max_value, uint64_t *out, const char **next) {
    3626          117 :     if (!start || !end || start >= end) return false;
    3627          117 :     uint64_t value = 0;
    3628          117 :     const char *p = start;
    3629          436 :     while (p < end && *p >= '0' && *p <= '9') {
    3630          319 :         uint64_t digit = (uint64_t)(*p - '0');
    3631          319 :         if (value > (max_value - digit) / 10u) return false;
    3632          319 :         value = value * 10u + digit;
    3633          319 :         p++;
    3634              :     }
    3635          117 :     if (p == start || p >= end || *p != ':') return false;
    3636          117 :     *out = value;
    3637          117 :     *next = p + 1;
    3638          117 :     return true;
    3639          117 : }
    3640              : 
    3641           39 : static bool cloudsync_payload_fragment_parse_colname (const char *col_name, int64_t col_name_len,
    3642              :                                                       char *value_id, size_t value_id_len,
    3643              :                                                       char *checksum_hex, size_t checksum_hex_len,
    3644              :                                                       int *part_index, int *part_count,
    3645              :                                                       int64_t *total_size,
    3646              :                                                       const char **base_col, int64_t *base_col_len) {
    3647           39 :     size_t prefix_len = strlen(CLOUDSYNC_PAYLOAD_FRAGMENT_PREFIX);
    3648           39 :     if (!col_name || col_name_len <= (int64_t)prefix_len) return false;
    3649           39 :     if (strncmp(col_name, CLOUDSYNC_PAYLOAD_FRAGMENT_PREFIX, prefix_len) != 0) return false;
    3650              : 
    3651           39 :     const char *p = col_name + prefix_len;
    3652           39 :     const char *end = col_name + col_name_len;
    3653           39 :     const char *sep = memchr(p, ':', (size_t)(end - p));
    3654           39 :     if (!sep || (size_t)(sep - p) + 1 > value_id_len) return false;
    3655           39 :     if ((sep - p) != 32 || !cloudsync_payload_is_hex(p, (size_t)(sep - p))) return false;
    3656           39 :     memcpy(value_id, p, (size_t)(sep - p));
    3657           39 :     value_id[sep - p] = 0;
    3658              : 
    3659           39 :     p = sep + 1;
    3660           39 :     sep = memchr(p, ':', (size_t)(end - p));
    3661           39 :     if (!sep || (size_t)(sep - p) + 1 > checksum_hex_len) return false;
    3662           39 :     if ((sep - p) != 16 || !cloudsync_payload_is_hex(p, (size_t)(sep - p))) return false;
    3663           39 :     memcpy(checksum_hex, p, (size_t)(sep - p));
    3664           39 :     checksum_hex[sep - p] = 0;
    3665              : 
    3666           39 :     const char *next = NULL;
    3667           39 :     uint64_t parsed = 0;
    3668           39 :     if (!cloudsync_payload_parse_u64_segment(sep + 1, end, INT_MAX, &parsed, &next)) return false;
    3669           39 :     *part_index = (int)parsed;
    3670              : 
    3671           39 :     if (!cloudsync_payload_parse_u64_segment(next, end, INT_MAX, &parsed, &next)) return false;
    3672           39 :     *part_count = (int)parsed;
    3673              : 
    3674           39 :     if (!cloudsync_payload_parse_u64_segment(next, end, INT64_MAX, &parsed, &next)) return false;
    3675           39 :     *total_size = (int64_t)parsed;
    3676              : 
    3677           39 :     *base_col = next;
    3678           39 :     *base_col_len = end - *base_col;
    3679           39 :     return (*part_count > 0 && *part_index < *part_count && *base_col_len > 0);
    3680           39 : }
    3681              : 
    3682              : typedef struct {
    3683              :     dbvm_t *vm;
    3684              :     int param_index;
    3685              : } cloudsync_payload_bind_param_context;
    3686              : 
    3687           11 : static int cloudsync_payload_bind_param_callback (void *xdata, int index, int type, int64_t ival, double dval, char *pval) {
    3688           11 :     UNUSED_PARAMETER(index);
    3689           11 :     cloudsync_payload_bind_param_context *ctx = (cloudsync_payload_bind_param_context *)xdata;
    3690           11 :     switch (type) {
    3691            0 :         case DBTYPE_INTEGER: return databasevm_bind_int(ctx->vm, ctx->param_index, ival);
    3692            0 :         case DBTYPE_FLOAT: return databasevm_bind_double(ctx->vm, ctx->param_index, dval);
    3693            0 :         case DBTYPE_NULL: return databasevm_bind_null(ctx->vm, ctx->param_index);
    3694            2 :         case DBTYPE_TEXT: return databasevm_bind_text(ctx->vm, ctx->param_index, pval, (int)ival);
    3695            9 :         case DBTYPE_BLOB: return databasevm_bind_blob(ctx->vm, ctx->param_index, pval, (uint64_t)ival);
    3696              :     }
    3697            0 :     return DBRES_MISUSE;
    3698           11 : }
    3699              : 
    3700           39 : static int cloudsync_payload_fragments_cleanup_stale (cloudsync_context *data) {
    3701              :     // Stale-fragment GC is pure maintenance (it removes incomplete fragment groups
    3702              :     // older than CLOUDSYNC_PAYLOAD_FRAGMENT_STALE_SECONDS), so it has no correctness
    3703              :     // deadline. It runs a full GROUP BY scan of the fragments table; calling it on
    3704              :     // every applied fragment would be O(n^2) for a heavily-fragmented value, since
    3705              :     // each fragment arrives as its own apply call. Throttle it to at most once per
    3706              :     // CLOUDSYNC_PAYLOAD_FRAGMENT_CLEANUP_MIN_INTERVAL per connection.
    3707           39 :     int64_t now = (int64_t)time(NULL);
    3708           39 :     if (data->last_fragment_cleanup != 0 &&
    3709           33 :         now - data->last_fragment_cleanup < CLOUDSYNC_PAYLOAD_FRAGMENT_CLEANUP_MIN_INTERVAL) {
    3710           33 :         return DBRES_OK;
    3711              :     }
    3712            6 :     data->last_fragment_cleanup = now;
    3713              : 
    3714            6 :     dbvm_t *vm = NULL;
    3715            6 :     int rc = databasevm_prepare(data, SQL_PAYLOAD_FRAGMENTS_CLEANUP_STALE, &vm, 0);
    3716            6 :     if (rc != DBRES_OK) return rc;
    3717            6 :     int64_t cutoff = now - CLOUDSYNC_PAYLOAD_FRAGMENT_STALE_SECONDS;
    3718            6 :     rc = databasevm_bind_int(vm, 1, cutoff);
    3719            6 :     if (rc == DBRES_OK) rc = databasevm_step(vm);
    3720            6 :     databasevm_finalize(vm);
    3721            6 :     return (rc == DBRES_DONE) ? DBRES_OK : rc;
    3722           39 : }
    3723              : 
    3724           11 : static int cloudsync_payload_apply_single_decoded_row (cloudsync_context *data,
    3725              :                                                        const char *tbl, size_t tbl_len,
    3726              :                                                        const char *pk, size_t pk_len,
    3727              :                                                        const char *col_name, size_t col_name_len,
    3728              :                                                        const char *encoded_value, size_t encoded_value_len,
    3729              :                                                        int64_t col_version, int64_t db_version,
    3730              :                                                        const char *site_id, size_t site_id_len,
    3731              :                                                        int64_t cl, int64_t seq,
    3732              :                                                        int *pnrows) {
    3733           11 :     int rc = DBRES_OK;
    3734           11 :     dbvm_t *vm = NULL;
    3735           11 :     bool in_savepoint = false;
    3736           11 :     merge_pending_batch batch = {0};
    3737              : 
    3738           11 :     rc = databasevm_prepare(data, SQL_CHANGES_INSERT_ROW, &vm, 0);
    3739           11 :     if (rc != DBRES_OK) return cloudsync_set_error(data, "Error on cloudsync_payload_apply: error while compiling SQL statement", rc);
    3740              : 
    3741           11 :     rc = databasevm_bind_text(vm, 1, tbl, (int)tbl_len);
    3742           11 :     if (rc == DBRES_OK) rc = databasevm_bind_blob(vm, 2, pk, (uint64_t)pk_len);
    3743           11 :     if (rc == DBRES_OK) rc = databasevm_bind_text(vm, 3, col_name, (int)col_name_len);
    3744           11 :     if (rc == DBRES_OK) {
    3745           11 :         if (data->skip_decode_idx == CLOUDSYNC_PK_INDEX_COLVALUE) {
    3746            0 :             rc = databasevm_bind_blob(vm, 4, encoded_value, (uint64_t)encoded_value_len);
    3747            0 :         } else {
    3748           11 :             size_t seek = 0;
    3749           11 :             cloudsync_payload_bind_param_context bind_ctx = {.vm = vm, .param_index = 4};
    3750           11 :             int res = pk_decode((char *)encoded_value, encoded_value_len, 1, &seek, -1, cloudsync_payload_bind_param_callback, &bind_ctx);
    3751           11 :             if (res == -1 || seek != encoded_value_len) rc = cloudsync_set_error(data, "Error on cloudsync_payload_apply: invalid v3 encoded value", DBRES_MISUSE);
    3752              :         }
    3753           11 :     }
    3754           11 :     if (rc == DBRES_OK) rc = databasevm_bind_int(vm, 5, col_version);
    3755           11 :     if (rc == DBRES_OK) rc = databasevm_bind_int(vm, 6, db_version);
    3756           11 :     if (rc == DBRES_OK) rc = databasevm_bind_blob(vm, 7, site_id, (uint64_t)site_id_len);
    3757           11 :     if (rc == DBRES_OK) rc = databasevm_bind_int(vm, 8, cl);
    3758           11 :     if (rc == DBRES_OK) rc = databasevm_bind_int(vm, 9, seq);
    3759           11 :     if (rc != DBRES_OK) goto cleanup;
    3760              : 
    3761           11 :     if (!database_in_transaction(data)) {
    3762           11 :         rc = database_begin_savepoint(data, "cloudsync_payload_apply");
    3763           11 :         if (rc != DBRES_OK) goto cleanup;
    3764           11 :         in_savepoint = true;
    3765           11 :     }
    3766              : 
    3767           11 :     data->pending_batch = &batch;
    3768           11 :     rc = databasevm_step(vm);
    3769           11 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    3770           11 :     if (rc != DBRES_OK) {
    3771            0 :         cloudsync_set_dberror(data);
    3772            0 :         goto cleanup;
    3773              :     }
    3774              : 
    3775           11 :     rc = merge_flush_pending(data);
    3776           11 :     if (rc != DBRES_OK) goto cleanup;
    3777           11 :     data->pending_batch = NULL;
    3778              : 
    3779           11 :     if (in_savepoint) {
    3780           11 :         rc = database_commit_savepoint(data, "cloudsync_payload_apply");
    3781           11 :         in_savepoint = false;
    3782           11 :         if (rc != DBRES_OK) goto cleanup;
    3783           11 :     }
    3784              : 
    3785              :     // Do NOT advance the receive cursor here: a v3 value carries a single
    3786              :     // (db_version, seq) that can be in the middle of its source db_version, and a
    3787              :     // db_version's chunks can span multiple /check artifacts. Advancing per value
    3788              :     // would leave the cursor mid-db_version. The durable cursor is advanced once,
    3789              :     // after the whole payload/stream is applied, via cloudsync_payload_apply's
    3790              :     // checkpoint argument. Record the last applied position for the
    3791              :     // CLOUDSYNC_CHECKPOINT_LAST_APPLIED mode.
    3792           11 :     if (db_version > data->apply_last_db_version ||
    3793            0 :         (db_version == data->apply_last_db_version && seq > data->apply_last_seq)) {
    3794           11 :         data->apply_last_db_version = db_version;
    3795           11 :         data->apply_last_seq = seq;
    3796           11 :     }
    3797              : 
    3798           11 :     if (pnrows) *pnrows += 1;
    3799              : 
    3800              : cleanup:
    3801           11 :     if (rc != DBRES_OK && in_savepoint) database_rollback_savepoint(data, "cloudsync_payload_apply");
    3802           11 :     data->pending_batch = NULL;
    3803           11 :     merge_pending_free_entries(&batch);
    3804           11 :     if (batch.cached_vm) databasevm_finalize(batch.cached_vm);
    3805           11 :     if (batch.cached_col_names) cloudsync_memory_free(batch.cached_col_names);
    3806           11 :     if (batch.entries) cloudsync_memory_free(batch.entries);
    3807           11 :     if (vm) databasevm_finalize(vm);
    3808           11 :     return rc;
    3809           11 : }
    3810              : 
    3811           39 : static int cloudsync_payload_apply_reassembled_fragment (cloudsync_context *data, const char *value_id, const char *expected_checksum_hex, int *pnrows) {
    3812           39 :     int rc = DBRES_OK;
    3813           39 :     dbvm_t *vm = NULL;
    3814           39 :     char *value = NULL;
    3815           39 :     char *tbl = NULL, *col_name = NULL;
    3816           39 :     char *pk = NULL, *site_id = NULL;
    3817           39 :     size_t tbl_len = 0, col_name_len = 0, pk_len = 0, site_id_len = 0;
    3818           39 :     int64_t col_version = 0, db_version = 0, cl = 0, seq = 0;
    3819           39 :     int64_t total_size = 0, copied = 0;
    3820              : 
    3821           39 :     rc = databasevm_prepare(data, SQL_PAYLOAD_FRAGMENTS_COUNT, &vm, 0);
    3822           39 :     if (rc != DBRES_OK) return rc;
    3823           39 :     rc = databasevm_bind_text(vm, 1, value_id, -1);
    3824           39 :     if (rc != DBRES_OK) { databasevm_finalize(vm); return rc; }
    3825           39 :     rc = databasevm_step(vm);
    3826           39 :     if (rc != DBRES_ROW) { databasevm_finalize(vm); return DBRES_OK; }
    3827           39 :     int64_t have = database_column_int(vm, 0);
    3828           39 :     int64_t part_count_min = database_column_int(vm, 1);
    3829           39 :     int64_t part_count_max = database_column_int(vm, 2);
    3830           39 :     int64_t total_size_min = database_column_int(vm, 3);
    3831           39 :     int64_t total_size_max = database_column_int(vm, 4);
    3832           39 :     const char *checksum_min = database_column_text(vm, 5);
    3833           39 :     const char *checksum_max = database_column_text(vm, 6);
    3834           39 :     char checksum_min_copy[32] = {0};
    3835           39 :     char checksum_max_copy[32] = {0};
    3836           39 :     if (checksum_min) snprintf(checksum_min_copy, sizeof(checksum_min_copy), "%s", checksum_min);
    3837           39 :     if (checksum_max) snprintf(checksum_max_copy, sizeof(checksum_max_copy), "%s", checksum_max);
    3838           39 :     int64_t part_index_min = database_column_int(vm, 7);
    3839           39 :     int64_t part_index_max = database_column_int(vm, 8);
    3840           39 :     databasevm_finalize(vm);
    3841           39 :     vm = NULL;
    3842           39 :     if (have <= 0 || part_count_min <= 0 || have < part_count_max) return DBRES_OK;
    3843           22 :     if (part_count_min != part_count_max || total_size_min != total_size_max || !checksum_min_copy[0] || !checksum_max_copy[0] ||
    3844           11 :         strcmp(checksum_min_copy, checksum_max_copy) != 0 || strcmp(checksum_min_copy, expected_checksum_hex) != 0 ||
    3845           11 :         part_index_min != 0 || part_index_max != part_count_max - 1 || have != part_count_max) {
    3846            0 :         return cloudsync_set_error(data, "Error on cloudsync_payload_apply: inconsistent v3 fragments", DBRES_MISUSE);
    3847              :     }
    3848           11 :     total_size = total_size_max;
    3849              : 
    3850           11 :     value = cloudsync_memory_alloc((uint64_t)total_size);
    3851           11 :     if (!value) return DBRES_NOMEM;
    3852              : 
    3853           11 :     rc = databasevm_prepare(data, SQL_PAYLOAD_FRAGMENTS_SELECT, &vm, 0);
    3854           11 :     if (rc != DBRES_OK) goto cleanup;
    3855           11 :     rc = databasevm_bind_text(vm, 1, value_id, -1);
    3856           11 :     if (rc != DBRES_OK) goto cleanup;
    3857              : 
    3858           11 :     uint64_t checksum = 14695981039346656037ULL;
    3859           48 :     while ((rc = databasevm_step(vm)) == DBRES_ROW) {
    3860           37 :         size_t frag_len = 0;
    3861           37 :         const char *frag = database_column_blob(vm, 0, &frag_len);
    3862           37 :         if (copied + (int64_t)frag_len > total_size) { rc = DBRES_MISUSE; goto cleanup; }
    3863           37 :         memcpy(value + copied, frag, frag_len);
    3864           37 :         checksum = cloudsync_checksum_update(checksum, frag, frag_len);
    3865           37 :         copied += (int64_t)frag_len;
    3866              : 
    3867           37 :         if (!tbl) {
    3868           11 :             const char *t = database_column_text(vm, 1);
    3869           11 :             const char *c = database_column_text(vm, 3);
    3870           11 :             size_t pkl = 0, sidl = 0;
    3871           11 :             const char *p = database_column_blob(vm, 2, &pkl);
    3872           11 :             const char *sid = database_column_blob(vm, 6, &sidl);
    3873           11 :             tbl_len = (size_t)database_column_bytes(vm, 1);
    3874           11 :             col_name_len = (size_t)database_column_bytes(vm, 3);
    3875           11 :             pk_len = pkl;
    3876           11 :             site_id_len = sidl;
    3877           11 :             tbl = cloudsync_memory_alloc((uint64_t)tbl_len);
    3878           11 :             col_name = cloudsync_memory_alloc((uint64_t)col_name_len);
    3879           11 :             pk = cloudsync_memory_alloc((uint64_t)pk_len);
    3880           11 :             site_id = cloudsync_memory_alloc((uint64_t)site_id_len);
    3881           11 :             if (!tbl || !col_name || !pk || !site_id) { rc = DBRES_NOMEM; goto cleanup; }
    3882           11 :             memcpy(tbl, t, tbl_len);
    3883           11 :             memcpy(col_name, c, col_name_len);
    3884           11 :             memcpy(pk, p, pk_len);
    3885           11 :             memcpy(site_id, sid, site_id_len);
    3886           11 :             col_version = database_column_int(vm, 4);
    3887           11 :             db_version = database_column_int(vm, 5);
    3888           11 :             cl = database_column_int(vm, 7);
    3889           11 :             seq = database_column_int(vm, 8);
    3890           11 :         } else {
    3891           26 :             size_t pkl = 0, sidl = 0;
    3892           26 :             const char *t = database_column_text(vm, 1);
    3893           26 :             const char *c = database_column_text(vm, 3);
    3894           26 :             const char *p = database_column_blob(vm, 2, &pkl);
    3895           26 :             const char *sid = database_column_blob(vm, 6, &sidl);
    3896           52 :             if ((size_t)database_column_bytes(vm, 1) != tbl_len || memcmp(tbl, t, tbl_len) != 0 ||
    3897           26 :                 pkl != pk_len || memcmp(pk, p, pk_len) != 0 ||
    3898           26 :                 (size_t)database_column_bytes(vm, 3) != col_name_len || memcmp(col_name, c, col_name_len) != 0 ||
    3899           26 :                 database_column_int(vm, 4) != col_version ||
    3900           26 :                 database_column_int(vm, 5) != db_version ||
    3901           26 :                 sidl != site_id_len || memcmp(site_id, sid, site_id_len) != 0 ||
    3902           26 :                 database_column_int(vm, 7) != cl ||
    3903           26 :                 database_column_int(vm, 8) != seq) {
    3904            0 :                 rc = DBRES_MISUSE;
    3905            0 :                 goto cleanup;
    3906              :             }
    3907              :         }
    3908              :     }
    3909           11 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    3910           11 :     if (rc != DBRES_OK) goto cleanup;
    3911           11 :     if (copied != total_size) { rc = DBRES_MISUSE; goto cleanup; }
    3912              :     char checksum_hex[17];
    3913           11 :     snprintf(checksum_hex, sizeof(checksum_hex), "%016" PRIx64, checksum);
    3914           11 :     if (strcmp(checksum_hex, expected_checksum_hex) != 0) { rc = DBRES_MISUSE; goto cleanup; }
    3915           11 :     databasevm_finalize(vm);
    3916           11 :     vm = NULL;
    3917              : 
    3918           22 :     rc = cloudsync_payload_apply_single_decoded_row(data, tbl, tbl_len, pk, pk_len, col_name, col_name_len,
    3919           11 :                                                     value, (size_t)total_size, col_version, db_version,
    3920           11 :                                                     site_id, site_id_len, cl, seq, pnrows);
    3921           11 :     if (rc != DBRES_OK) goto cleanup;
    3922              : 
    3923           11 :     rc = databasevm_prepare(data, SQL_PAYLOAD_FRAGMENTS_DELETE, &vm, 0);
    3924           22 :     if (rc == DBRES_OK) {
    3925           11 :         databasevm_bind_text(vm, 1, value_id, -1);
    3926           11 :         int step_rc = databasevm_step(vm);
    3927           11 :         if (step_rc == DBRES_DONE) rc = DBRES_OK;
    3928           11 :     }
    3929              : 
    3930              : cleanup:
    3931           11 :     if (vm) databasevm_finalize(vm);
    3932           11 :     if (value) cloudsync_memory_free(value);
    3933           11 :     if (tbl) cloudsync_memory_free(tbl);
    3934           11 :     if (col_name) cloudsync_memory_free(col_name);
    3935           11 :     if (pk) cloudsync_memory_free(pk);
    3936           11 :     if (site_id) cloudsync_memory_free(site_id);
    3937           11 :     return rc;
    3938           39 : }
    3939              : 
    3940           39 : static int cloudsync_payload_apply_fragment_row (cloudsync_context *data, cloudsync_payload_fragment_row *row, int *pnrows) {
    3941              :     char value_id[64];
    3942              :     char checksum_hex[17];
    3943           39 :     int part_index = 0, part_count = 0;
    3944           39 :     int64_t total_size = 0;
    3945           39 :     const char *base_col = NULL;
    3946           39 :     int64_t base_col_len = 0;
    3947           78 :     if (!row || !row->tbl || row->tbl_len <= 0 || !row->pk || row->pk_len <= 0 ||
    3948           39 :         !row->col_name || row->col_name_len <= 0 || !row->col_value || row->col_value_len <= 0 ||
    3949           39 :         !row->site_id || row->site_id_len <= 0) {
    3950            0 :         return cloudsync_set_error(data, "Error on cloudsync_payload_apply: invalid v3 payload row", DBRES_MISUSE);
    3951              :     }
    3952           78 :     if (!cloudsync_payload_fragment_parse_colname(row->col_name, row->col_name_len, value_id, sizeof(value_id),
    3953           39 :                                                   checksum_hex, sizeof(checksum_hex),
    3954              :                                                   &part_index, &part_count, &total_size, &base_col, &base_col_len)) {
    3955            0 :         return cloudsync_set_error(data, "Error on cloudsync_payload_apply: invalid v3 fragment metadata", DBRES_MISUSE);
    3956              :     }
    3957              : 
    3958           39 :     uint64_t value_checksum = strtoull(checksum_hex, NULL, 16);
    3959              :     char expected_value_id[33];
    3960           78 :     cloudsync_payload_fragment_value_id(expected_value_id, row->tbl, (int)row->tbl_len, row->pk, (int)row->pk_len,
    3961           39 :                                         base_col, (int)base_col_len, row->col_version, row->db_version,
    3962           39 :                                         row->site_id, (int)row->site_id_len, row->cl, row->seq,
    3963           39 :                                         value_checksum, total_size);
    3964           39 :     if (strcmp(value_id, expected_value_id) != 0) {
    3965            0 :         return cloudsync_set_error(data, "Error on cloudsync_payload_apply: invalid v3 fragment identity", DBRES_MISUSE);
    3966              :     }
    3967              : 
    3968              :     // the fragments table is guaranteed by dbutils_settings_init; no DDL here
    3969              :     // because the apply path runs under sync-only credentials on server nodes
    3970           39 :     int rc = cloudsync_payload_fragments_cleanup_stale(data);
    3971           39 :     if (rc != DBRES_OK) return rc;
    3972              : 
    3973           39 :     dbvm_t *vm = NULL;
    3974           39 :     rc = databasevm_prepare(data, SQL_PAYLOAD_FRAGMENTS_UPSERT, &vm, 0);
    3975           39 :     if (rc != DBRES_OK) return rc;
    3976           39 :     databasevm_bind_text(vm, 1, value_id, -1);
    3977           39 :     databasevm_bind_int(vm, 2, part_index);
    3978           39 :     databasevm_bind_int(vm, 3, part_count);
    3979           39 :     databasevm_bind_int(vm, 4, total_size);
    3980           39 :     databasevm_bind_text(vm, 5, checksum_hex, -1);
    3981           39 :     databasevm_bind_int(vm, 6, (int64_t)time(NULL));
    3982           39 :     databasevm_bind_text(vm, 7, row->tbl, (int)row->tbl_len);
    3983           39 :     databasevm_bind_blob(vm, 8, row->pk, (uint64_t)row->pk_len);
    3984           39 :     databasevm_bind_text(vm, 9, base_col, (int)base_col_len);
    3985           39 :     databasevm_bind_int(vm, 10, row->col_version);
    3986           39 :     databasevm_bind_int(vm, 11, row->db_version);
    3987           39 :     databasevm_bind_blob(vm, 12, row->site_id, (uint64_t)row->site_id_len);
    3988           39 :     databasevm_bind_int(vm, 13, row->cl);
    3989           39 :     databasevm_bind_int(vm, 14, row->seq);
    3990           39 :     databasevm_bind_blob(vm, 15, row->col_value, (uint64_t)row->col_value_len);
    3991           39 :     rc = databasevm_step(vm);
    3992           39 :     databasevm_finalize(vm);
    3993           39 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    3994           39 :     if (rc != DBRES_OK) return rc;
    3995              : 
    3996           39 :     return cloudsync_payload_apply_reassembled_fragment(data, value_id, checksum_hex, pnrows);
    3997           39 : }
    3998              : 
    3999              : // #ifndef CLOUDSYNC_OMIT_RLS_VALIDATION
    4000              : 
    4001              : // Advance the durable receive cursor (check_dbversion/check_seq) after a payload
    4002              : // (or a fully-applied chunk stream) has been applied. See the checkpoint-mode
    4003              : // documentation on cloudsync_payload_apply in cloudsync.h. The advance is
    4004              : // strictly monotonic so re-delivered rows never regress the cursor.
    4005          746 : static void cloudsync_payload_apply_checkpoint (cloudsync_context *data, int64_t checkpoint_db_version, int64_t checkpoint_seq) {
    4006              :     int64_t target_db_version;
    4007              :     int64_t target_seq;
    4008              : 
    4009          746 :     if (checkpoint_db_version == CLOUDSYNC_CHECKPOINT_NONE) return;
    4010          743 :     if (checkpoint_db_version == CLOUDSYNC_CHECKPOINT_LAST_APPLIED) {
    4011              :         // Nothing applied -> nothing to checkpoint.
    4012          742 :         if (data->apply_last_db_version < 0) return;
    4013          714 :         target_db_version = data->apply_last_db_version;
    4014          714 :         target_seq = data->apply_last_seq;
    4015          714 :     } else {
    4016            1 :         target_db_version = checkpoint_db_version;
    4017            1 :         target_seq = checkpoint_seq;
    4018              :     }
    4019              : 
    4020          715 :     int64_t cur_db_version = dbutils_settings_get_int64_value(data, CLOUDSYNC_KEY_CHECK_DBVERSION);
    4021          715 :     int64_t cur_seq = dbutils_settings_get_int64_value(data, CLOUDSYNC_KEY_CHECK_SEQ);
    4022              : 
    4023              :     // monotonic guard: never move the cursor backwards
    4024          715 :     if (target_db_version < cur_db_version) return;
    4025          572 :     if (target_db_version == cur_db_version && target_seq <= cur_seq) return;
    4026              : 
    4027              :     char buf[256];
    4028          489 :     snprintf(buf, sizeof(buf), "%" PRId64, target_db_version);
    4029          489 :     dbutils_settings_set_key_value(data, CLOUDSYNC_KEY_CHECK_DBVERSION, buf);
    4030          489 :     if (target_seq != cur_seq) {
    4031          323 :         snprintf(buf, sizeof(buf), "%" PRId64, target_seq);
    4032          323 :         dbutils_settings_set_key_value(data, CLOUDSYNC_KEY_CHECK_SEQ, buf);
    4033          323 :     }
    4034          746 : }
    4035              : 
    4036          751 : int cloudsync_payload_apply (cloudsync_context *data, const char *payload, int blen, int *pnrows, int64_t checkpoint_db_version, int64_t checkpoint_seq) {
    4037              :     // Guard against calling payload_apply before cloudsync_init: without this,
    4038              :     // the settings lookups at the top of this function would each emit a
    4039              :     // "no such table: cloudsync_settings" debug line, control would fall
    4040              :     // through to the meta-table insert, and the function would ultimately
    4041              :     // return an error with an empty errmsg — SQLite then surfaces that as
    4042              :     // the confusing "Runtime error: not an error".
    4043          751 :     if (!cloudsync_context_is_initialized(data)) {
    4044            1 :         return cloudsync_set_error(data,
    4045              :             "cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
    4046              :             "to enable sync on a table before calling cloudsync_payload_apply().",
    4047              :             DBRES_MISUSE);
    4048              :     }
    4049              : 
    4050              :     // sanity check
    4051          750 :     if (blen < (int)sizeof(cloudsync_payload_header)) return cloudsync_set_error(data, "Error on cloudsync_payload_apply: invalid payload length", DBRES_MISUSE);
    4052              : 
    4053              :     // track the last (db_version, seq) applied by this call so the receive
    4054              :     // checkpoint can be computed once, after the whole payload is applied
    4055          750 :     data->apply_last_db_version = -1;
    4056          750 :     data->apply_last_seq = -1;
    4057              : 
    4058              :     // decode header
    4059              :     cloudsync_payload_header header;
    4060          750 :     memcpy(&header, payload, sizeof(cloudsync_payload_header));
    4061              :     
    4062          750 :     header.signature = ntohl(header.signature);
    4063          750 :     header.expanded_size = ntohl(header.expanded_size);
    4064          750 :     header.ncols = ntohs(header.ncols);
    4065          750 :     header.nrows = ntohl(header.nrows);
    4066          750 :     header.schema_hash = ntohll(header.schema_hash);
    4067              :     
    4068              :     // compare schema_hash only if not disabled and if the received payload was created with the current header version
    4069              :     // to avoid schema hash mismatch when processed by a peer with a different extension version during software updates.
    4070          750 :     if (dbutils_settings_get_int64_value(data, CLOUDSYNC_KEY_SKIP_SCHEMA_HASH_CHECK) == 0 && header.version == CLOUDSYNC_PAYLOAD_VERSION_LATEST ) {
    4071          711 :         if (header.schema_hash != data->schema_hash) {
    4072            4 :             if (!database_check_schema_hash(data, header.schema_hash)) {
    4073              :                 char buffer[1024];
    4074            2 :                 snprintf(buffer, sizeof(buffer), "Cannot apply the received payload because the schema hash is unknown %" PRIu64 ".", header.schema_hash);
    4075            2 :                 return cloudsync_set_error(data, buffer, DBRES_MISUSE);
    4076              :             }
    4077            2 :         }
    4078          709 :     }
    4079              :     
    4080              :     // sanity check header
    4081          748 :     if ((header.signature != CLOUDSYNC_PAYLOAD_SIGNATURE) || (header.ncols == 0)) {
    4082            0 :         return cloudsync_set_error(data, "Error on cloudsync_payload_apply: invalid signature or column size", DBRES_MISUSE);
    4083              :     }
    4084          748 :     if (header.version < CLOUDSYNC_PAYLOAD_VERSION_1 || header.version > CLOUDSYNC_PAYLOAD_VERSION_3) {
    4085            0 :         return cloudsync_set_error(data, "Error on cloudsync_payload_apply: unsupported payload version", DBRES_MISUSE);
    4086              :     }
    4087              :     
    4088          748 :     const char *buffer = payload + sizeof(cloudsync_payload_header);
    4089          748 :     size_t buf_len = (size_t)blen - sizeof(cloudsync_payload_header);
    4090              : 
    4091              :     // sanity check checksum (only if version is >= 2)
    4092          748 :     if (header.version >= CLOUDSYNC_PAYLOAD_MIN_VERSION_WITH_CHECKSUM) {
    4093          748 :         uint64_t checksum = pk_checksum(buffer, buf_len);
    4094          748 :         if (cloudsync_payload_checksum_verify(&header, checksum) == false) {
    4095            1 :             return cloudsync_set_error(data, "Error on cloudsync_payload_apply: invalid checksum", DBRES_MISUSE);
    4096              :         }
    4097          747 :     }
    4098              : 
    4099              :     // check if payload is compressed
    4100          747 :     char *clone = NULL;
    4101          747 :     if (header.expanded_size != 0) {
    4102          692 :         clone = (char *)cloudsync_memory_alloc(header.expanded_size);
    4103          692 :         if (!clone) return cloudsync_set_error(data, "Unable to allocate memory to uncompress payload", DBRES_NOMEM);
    4104              : 
    4105          692 :         int lz4_rc = LZ4_decompress_safe(buffer, clone, (int)buf_len, (int)header.expanded_size);
    4106          692 :         if (lz4_rc <= 0 || (uint32_t)lz4_rc != header.expanded_size) {
    4107            0 :             if (clone) cloudsync_memory_free(clone);
    4108            0 :             return cloudsync_set_error(data, "Error on cloudsync_payload_apply: unable to decompress BLOB", DBRES_MISUSE);
    4109              :         }
    4110              : 
    4111          692 :         buffer = (const char *)clone;
    4112          692 :         buf_len = (size_t)header.expanded_size;
    4113          692 :     }
    4114              : 
    4115          747 :     if (header.version == CLOUDSYNC_PAYLOAD_VERSION_3) {
    4116           39 :         int rc = DBRES_OK;
    4117           39 :         int applied_rows = 0;
    4118           39 :         if (header.ncols != CLOUDSYNC_CHANGES_NCOLS) {
    4119            0 :             if (clone) cloudsync_memory_free(clone);
    4120            0 :             return cloudsync_set_error(data, "Error on cloudsync_payload_apply: invalid v3 column count", DBRES_MISUSE);
    4121              :         }
    4122           78 :         for (uint32_t i = 0; i < header.nrows; ++i) {
    4123           39 :             size_t seek = 0;
    4124           39 :             cloudsync_payload_fragment_row row = {0};
    4125           39 :             int res = pk_decode((char *)buffer, buf_len, header.ncols, &seek, -1,
    4126              :                                 cloudsync_payload_fragment_decode_callback, &row);
    4127           39 :             if (res == -1 || seek == 0 || seek > buf_len) {
    4128            0 :                 rc = cloudsync_set_error(data, "Error on cloudsync_payload_apply: invalid v3 payload row", DBRES_MISUSE);
    4129            0 :                 break;
    4130              :             }
    4131           39 :             int n = 0;
    4132           39 :             rc = cloudsync_payload_apply_fragment_row(data, &row, &n);
    4133           39 :             if (rc != DBRES_OK) break;
    4134           39 :             applied_rows += n;
    4135           39 :             buffer += seek;
    4136           39 :             buf_len -= seek;
    4137           39 :         }
    4138           39 :         if (clone) cloudsync_memory_free(clone);
    4139           39 :         if (pnrows) *pnrows = applied_rows;
    4140              :         // Advance the receive cursor only after the whole payload is applied,
    4141              :         // gated on the caller-supplied checkpoint (a non-final chunk passes
    4142              :         // CLOUDSYNC_CHECKPOINT_NONE and leaves the cursor untouched).
    4143           39 :         if (rc == DBRES_OK) cloudsync_payload_apply_checkpoint(data, checkpoint_db_version, checkpoint_seq);
    4144           39 :         return rc;
    4145              :     }
    4146              : 
    4147              :     // precompile the insert statement
    4148          708 :     dbvm_t *vm = NULL;
    4149          708 :     int rc = databasevm_prepare(data, SQL_CHANGES_INSERT_ROW, &vm, 0);
    4150          708 :     if (rc != DBRES_OK) {
    4151            0 :         if (clone) cloudsync_memory_free(clone);
    4152            0 :         return cloudsync_set_error(data, "Error on cloudsync_payload_apply: error while compiling SQL statement", rc);
    4153              :     }
    4154              :     
    4155              :     // process buffer, one row at a time
    4156          708 :     uint16_t ncols = header.ncols;
    4157          708 :     uint32_t nrows = header.nrows;
    4158          708 :     int64_t last_payload_db_version = -1;
    4159          708 :     cloudsync_pk_decode_bind_context decoded_context = {.vm = vm};
    4160              : 
    4161              :     // Initialize deferred column-batch merge
    4162          708 :     merge_pending_batch batch = {0};
    4163          708 :     data->pending_batch = &batch;
    4164          708 :     bool in_savepoint = false;
    4165          708 :     const void *last_pk = NULL;
    4166          708 :     int64_t last_pk_len = 0;
    4167          708 :     const char *last_tbl = NULL;
    4168          708 :     int64_t last_tbl_len = 0;
    4169              : 
    4170        52056 :     for (uint32_t i=0; i<nrows; ++i) {
    4171        51348 :         size_t seek = 0;
    4172        51348 :         int res = pk_decode((char *)buffer, buf_len, ncols, &seek, data->skip_decode_idx, cloudsync_payload_decode_callback, &decoded_context);
    4173        51348 :         if (res == -1) {
    4174            0 :             merge_flush_pending(data);
    4175            0 :             data->pending_batch = NULL;
    4176            0 :             if (batch.cached_vm) { databasevm_finalize(batch.cached_vm); batch.cached_vm = NULL; }
    4177            0 :             if (batch.cached_col_names) { cloudsync_memory_free(batch.cached_col_names); batch.cached_col_names = NULL; }
    4178            0 :             if (batch.entries) { cloudsync_memory_free(batch.entries); batch.entries = NULL; }
    4179            0 :             if (in_savepoint) database_rollback_savepoint(data, "cloudsync_payload_apply");
    4180            0 :             rc = DBRES_ERROR;
    4181            0 :             goto cleanup;
    4182              :         }
    4183              : 
    4184              :         // Detect PK/table/db_version boundary to flush pending batch
    4185       101988 :         bool pk_changed = (last_pk != NULL &&
    4186        50640 :                            (last_pk_len != decoded_context.pk_len ||
    4187        48720 :                             memcmp(last_pk, decoded_context.pk, last_pk_len) != 0));
    4188       101988 :         bool tbl_changed = (last_tbl != NULL &&
    4189        50640 :                             (last_tbl_len != decoded_context.tbl_len ||
    4190        50568 :                              memcmp(last_tbl, decoded_context.tbl, last_tbl_len) != 0));
    4191        51348 :         bool db_version_changed = (last_payload_db_version != decoded_context.db_version);
    4192              : 
    4193              :         // Flush pending batch before any boundary change
    4194        51348 :         if (pk_changed || tbl_changed || db_version_changed) {
    4195        19954 :             int flush_rc = merge_flush_pending(data);
    4196        19954 :             if (flush_rc != DBRES_OK) {
    4197            1 :                 rc = flush_rc;
    4198              :                 // continue processing remaining rows
    4199            1 :             }
    4200        19954 :         }
    4201              : 
    4202              :         // Per-db_version savepoints group rows with the same source db_version
    4203              :         // into one transaction. In SQLite autocommit mode, the RELEASE triggers
    4204              :         // the commit hook which bumps data->db_version and resets seq, ensuring
    4205              :         // unique (db_version, seq) tuples across groups. In PostgreSQL SPI,
    4206              :         // database_in_transaction() is always true so this block is inactive —
    4207              :         // the inner per-PK savepoint in merge_flush_pending handles RLS instead.
    4208        51348 :         if (in_savepoint && db_version_changed) {
    4209         4299 :             rc = database_commit_savepoint(data, "cloudsync_payload_apply");
    4210         4299 :             if (rc != DBRES_OK) {
    4211            0 :                 merge_pending_free_entries(&batch);
    4212            0 :                 data->pending_batch = NULL;
    4213            0 :                 cloudsync_set_error(data, "Error on cloudsync_payload_apply: unable to release a savepoint", rc);
    4214            0 :                 goto cleanup;
    4215              :             }
    4216         4299 :             in_savepoint = false;
    4217         4299 :         }
    4218              : 
    4219        51348 :         if (!in_savepoint && db_version_changed && !database_in_transaction(data)) {
    4220         5007 :             rc = database_begin_savepoint(data, "cloudsync_payload_apply");
    4221         5007 :             if (rc != DBRES_OK) {
    4222            0 :                 merge_pending_free_entries(&batch);
    4223            0 :                 data->pending_batch = NULL;
    4224            0 :                 cloudsync_set_error(data, "Error on cloudsync_payload_apply: unable to start a transaction", rc);
    4225            0 :                 goto cleanup;
    4226              :             }
    4227         5007 :             in_savepoint = true;
    4228         5007 :         }
    4229              : 
    4230              :         // Track db_version for batch-flush boundary detection
    4231        51348 :         if (db_version_changed) {
    4232         5007 :             last_payload_db_version = decoded_context.db_version;
    4233         5007 :         }
    4234              : 
    4235              :         // Update PK/table tracking
    4236        51348 :         last_pk = decoded_context.pk;
    4237        51348 :         last_pk_len = decoded_context.pk_len;
    4238        51348 :         last_tbl = decoded_context.tbl;
    4239        51348 :         last_tbl_len = decoded_context.tbl_len;
    4240              : 
    4241        51348 :         rc = databasevm_step(vm);
    4242        51348 :         if (rc != DBRES_DONE) {
    4243              :             // don't "break;", the error can be due to a RLS policy.
    4244              :             // in case of error we try to apply the following changes
    4245            2 :         }
    4246              : 
    4247        51348 :         buffer += seek;
    4248        51348 :         buf_len -= seek;
    4249        51348 :         dbvm_reset(vm);
    4250        51348 :     }
    4251              : 
    4252              :     // Final flush after loop
    4253              :     {
    4254          708 :         int flush_rc = merge_flush_pending(data);
    4255          708 :         if (flush_rc != DBRES_OK && rc == DBRES_OK) rc = flush_rc;
    4256              :     }
    4257          708 :     data->pending_batch = NULL;
    4258              : 
    4259          708 :     if (in_savepoint) {
    4260          708 :         int rc1 = database_commit_savepoint(data, "cloudsync_payload_apply");
    4261          708 :         if (rc1 != DBRES_OK) rc = rc1;
    4262          708 :     }
    4263              : 
    4264              :     // save last error (unused if function returns OK)
    4265          708 :     if (rc != DBRES_OK && rc != DBRES_DONE) {
    4266            1 :         cloudsync_set_dberror(data);
    4267            1 :     }
    4268              : 
    4269          708 :     if (rc == DBRES_DONE) rc = DBRES_OK;
    4270         1415 :     if (rc == DBRES_OK) {
    4271              :         // Record the last applied (db_version, seq) and advance the receive cursor
    4272              :         // once, gated on the caller-supplied checkpoint. A non-final chunk passes
    4273              :         // CLOUDSYNC_CHECKPOINT_NONE so the cursor never lands mid-db_version.
    4274          707 :         if (decoded_context.db_version > data->apply_last_db_version ||
    4275            0 :             (decoded_context.db_version == data->apply_last_db_version && decoded_context.seq > data->apply_last_seq)) {
    4276          707 :             data->apply_last_db_version = decoded_context.db_version;
    4277          707 :             data->apply_last_seq = decoded_context.seq;
    4278          707 :         }
    4279          707 :         cloudsync_payload_apply_checkpoint(data, checkpoint_db_version, checkpoint_seq);
    4280          707 :     }
    4281              : 
    4282              : cleanup:
    4283              :     // cleanup merge_pending_batch
    4284          708 :     if (batch.cached_vm) { databasevm_finalize(batch.cached_vm); batch.cached_vm = NULL; }
    4285          708 :     if (batch.cached_col_names) { cloudsync_memory_free(batch.cached_col_names); batch.cached_col_names = NULL; }
    4286          708 :     if (batch.entries) { cloudsync_memory_free(batch.entries); batch.entries = NULL; }  
    4287              : 
    4288              :     // cleanup vm
    4289          708 :     if (vm) databasevm_finalize(vm);
    4290              : 
    4291              :     // cleanup memory
    4292          708 :     if (clone) cloudsync_memory_free(clone);
    4293              : 
    4294              :     // error already saved in (save last error)
    4295          708 :     if (rc != DBRES_OK) return rc;
    4296              : 
    4297              :     // return the number of processed rows
    4298          707 :     if (pnrows) *pnrows = nrows;
    4299          707 :     return DBRES_OK;
    4300          751 : }
    4301              : 
    4302              : // MARK: - Payload load/store -
    4303              : 
    4304            0 : int cloudsync_payload_get (cloudsync_context *data, char **blob, int *blob_size, int *db_version, int64_t *new_db_version) {
    4305              :     // retrieve current db_version and seq
    4306            0 :     *db_version = dbutils_settings_get_int_value(data, CLOUDSYNC_KEY_SEND_DBVERSION);
    4307            0 :     if (*db_version < 0) return DBRES_ERROR;
    4308              :     
    4309              :     // retrieve BLOB
    4310              :     char sql[1024];
    4311            0 :     snprintf(sql, sizeof(sql), "WITH max_db_version AS (SELECT MAX(db_version) AS max_db_version FROM cloudsync_changes WHERE site_id=cloudsync_siteid()) "
    4312              :                                "SELECT * FROM (SELECT cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq) AS payload, max_db_version AS max_db_version FROM cloudsync_changes, max_db_version WHERE site_id=cloudsync_siteid() AND db_version>%d) WHERE payload IS NOT NULL", *db_version);
    4313              :     
    4314            0 :     int64_t len = 0;
    4315            0 :     int rc = database_select_blob_int(data, sql, blob, &len, new_db_version);
    4316            0 :     *blob_size = (int)len;
    4317            0 :     if (rc != DBRES_OK) return rc;
    4318              :     
    4319              :     // exit if there is no data to send
    4320            0 :     if (*blob == NULL || *blob_size == 0) return DBRES_OK;
    4321            0 :     return rc;
    4322            0 : }
    4323              : 
    4324              : #ifdef CLOUDSYNC_DESKTOP_OS
    4325            0 : int cloudsync_payload_save (cloudsync_context *data, const char *payload_path, int *size) {
    4326              :     DEBUG_FUNCTION("cloudsync_payload_save");
    4327              :     
    4328              :     // silently delete any other payload with the same name
    4329            0 :     cloudsync_file_delete(payload_path);
    4330              :     
    4331              :     // retrieve payload
    4332            0 :     char *blob = NULL;
    4333            0 :     int blob_size = 0, db_version = 0;
    4334            0 :     int64_t new_db_version = 0;
    4335            0 :     int rc = cloudsync_payload_get(data, &blob, &blob_size, &db_version, &new_db_version);
    4336            0 :     if (rc != DBRES_OK) {
    4337            0 :         if (db_version < 0) return cloudsync_set_error(data, "Unable to retrieve db_version", rc);
    4338            0 :         return cloudsync_set_error(data, "Unable to retrieve changes in cloudsync_payload_save", rc);
    4339              :     }
    4340              :     
    4341              :     // exit if there is no data to save
    4342            0 :     if (blob == NULL || blob_size == 0) {
    4343            0 :         if (size) *size = 0;
    4344            0 :         return DBRES_OK;
    4345              :     }
    4346              :     
    4347              :     // write payload to file
    4348            0 :     bool res = cloudsync_file_write(payload_path, blob, (size_t)blob_size);
    4349            0 :     cloudsync_memory_free(blob);
    4350            0 :     if (res == false) {
    4351            0 :         return cloudsync_set_error(data, "Unable to write payload to file path", DBRES_IOERR);
    4352              :     }
    4353              :     
    4354              :     // returns blob size
    4355            0 :     if (size) *size = blob_size;
    4356            0 :     return DBRES_OK;
    4357            0 : }
    4358              : #endif
    4359              : 
    4360              : // MARK: - Core -
    4361              : 
    4362          312 : int cloudsync_table_sanity_check (cloudsync_context *data, const char *name, CLOUDSYNC_INIT_FLAG init_flags) {
    4363              :     DEBUG_DBFUNCTION("cloudsync_table_sanity_check %s", name);
    4364              :     char buffer[2048];
    4365              :     
    4366              :     // sanity check table name
    4367          312 :     if (name == NULL) {
    4368            1 :         return cloudsync_set_error(data, "cloudsync_init requires a non-null table parameter", DBRES_ERROR);
    4369              :     }
    4370              :     
    4371              :     // avoid allocating heap memory for SQL statements by setting a maximum length of 512 characters
    4372              :     // for table names. This limit is reasonable and helps prevent memory management issues.
    4373          311 :     const size_t maxlen = CLOUDSYNC_MAX_TABLENAME_LEN;
    4374          311 :     if (strlen(name) > maxlen) {
    4375            1 :         snprintf(buffer, sizeof(buffer), "Table name cannot be longer than %d characters", (int)maxlen);
    4376            1 :         return cloudsync_set_error(data, buffer, DBRES_ERROR);
    4377              :     }
    4378              :     
    4379              :     // check if already initialized
    4380          310 :     cloudsync_table_context *table = table_lookup(data, name);
    4381          310 :     if (table) return DBRES_OK;
    4382              :     
    4383              :     // check if table exists
    4384          306 :     if (database_table_exists(data, name, cloudsync_schema(data)) == false) {
    4385            2 :         snprintf(buffer, sizeof(buffer), "Table %s does not exist", name);
    4386            2 :         return cloudsync_set_error(data, buffer, DBRES_ERROR);
    4387              :     }
    4388              :     
    4389              :     // no more than 128 columns can be used as a composite primary key (SQLite hard limit)
    4390          304 :     int npri_keys = database_count_pk(data, name, false, cloudsync_schema(data));
    4391          304 :     if (npri_keys < 0) return cloudsync_set_dberror(data);
    4392          304 :     if (npri_keys > 128) return cloudsync_set_error(data, "No more than 128 columns can be used to form a composite primary key", DBRES_ERROR);
    4393              :     
    4394              :     #if CLOUDSYNC_DISABLE_ROWIDONLY_TABLES
    4395              :     // if count == 0 means that rowid will be used as primary key (BTW: very bad choice for the user)
    4396          304 :     if (npri_keys == 0) {
    4397            1 :         snprintf(buffer, sizeof(buffer), "Rowid only tables are not supported, all primary keys must be explicitly set and declared as NOT NULL (table %s)", name);
    4398            1 :         return cloudsync_set_error(data, buffer, DBRES_ERROR);
    4399              :     }
    4400              :     #endif
    4401              :     
    4402          303 :     bool skip_int_pk_check = (init_flags & CLOUDSYNC_INIT_FLAG_SKIP_INT_PK_CHECK) != 0;
    4403          303 :     if (!skip_int_pk_check) {
    4404          240 :         if (npri_keys == 1) {
    4405              :             // the affinity of a column is determined by the declared type of the column,
    4406              :             // according to the following rules in the order shown:
    4407              :             // 1. If the declared type contains the string "INT" then it is assigned INTEGER affinity.
    4408          152 :             int npri_keys_int = database_count_int_pk(data, name, cloudsync_schema(data));
    4409          152 :             if (npri_keys_int < 0) return cloudsync_set_dberror(data);
    4410          152 :             if (npri_keys == npri_keys_int) {
    4411            1 :                 snprintf(buffer, sizeof(buffer), "Table %s uses a single-column INTEGER primary key. For CRDT replication, primary keys must be globally unique. Consider using a TEXT primary key with UUIDs or ULID to avoid conflicts across nodes. If you understand the risk and still want to use this INTEGER primary key, set the third argument of the cloudsync_init function to 1 to skip this check.", name);
    4412            1 :                 return cloudsync_set_error(data, buffer, DBRES_ERROR);
    4413              :             }
    4414              :             
    4415          151 :         }
    4416          239 :     }
    4417              :         
    4418              :     // if user declared explicit primary key(s) then make sure they are all declared as NOT NULL
    4419              :     #if CLOUDSYNC_CHECK_NOTNULL_PRIKEYS
    4420              :     bool skip_notnull_prikeys_check = (init_flags & CLOUDSYNC_INIT_FLAG_SKIP_NOT_NULL_PRIKEYS_CHECK) != 0;
    4421              :     if (!skip_notnull_prikeys_check) {
    4422              :         if (npri_keys > 0) {
    4423              :             int npri_keys_notnull = database_count_pk(data, name, true, cloudsync_schema(data));
    4424              :             if (npri_keys_notnull < 0) return cloudsync_set_dberror(data);
    4425              :             if (npri_keys != npri_keys_notnull) {
    4426              :                 snprintf(buffer, sizeof(buffer), "All primary keys must be explicitly declared as NOT NULL (table %s)", name);
    4427              :                 return cloudsync_set_error(data, buffer, DBRES_ERROR);
    4428              :             }
    4429              :         }
    4430              :     }
    4431              :     #endif
    4432              :     
    4433              :     // check for columns declared as NOT NULL without a DEFAULT value.
    4434              :     // Otherwise, col_merge_stmt would fail if changes to other columns are inserted first.
    4435          302 :     bool skip_notnull_default_check = (init_flags & CLOUDSYNC_INIT_FLAG_SKIP_NOT_NULL_DEFAULT_CHECK) != 0;
    4436          302 :     if (!skip_notnull_default_check) {
    4437          302 :         int n_notnull_nodefault = database_count_notnull_without_default(data, name, cloudsync_schema(data));
    4438          302 :         if (n_notnull_nodefault < 0) return cloudsync_set_dberror(data);
    4439          302 :         if (n_notnull_nodefault > 0) {
    4440            0 :             snprintf(buffer, sizeof(buffer), "All non-primary key columns declared as NOT NULL must have a DEFAULT value. (table %s)", name);
    4441            0 :             return cloudsync_set_error(data, buffer, DBRES_ERROR);
    4442              :         }
    4443          302 :     }
    4444              :     
    4445          302 :     return DBRES_OK;
    4446          312 : }
    4447              : 
    4448            4 : int cloudsync_cleanup_internal (cloudsync_context *data, cloudsync_table_context *table) {
    4449            4 :     if (cloudsync_context_init(data) == NULL) return DBRES_MISUSE;
    4450              : 
    4451              :     // drop meta-table
    4452            4 :     const char *table_name = table->name;
    4453            4 :     char *sql = cloudsync_memory_mprintf(SQL_DROP_CLOUDSYNC_TABLE, table->meta_ref);
    4454            4 :     int rc = database_exec(data, sql);
    4455            4 :     cloudsync_memory_free(sql);
    4456            4 :     if (rc != DBRES_OK) {
    4457              :         char buffer[1024];
    4458            0 :         snprintf(buffer, sizeof(buffer), "Unable to drop cloudsync table %s_cloudsync in cloudsync_cleanup", table_name);
    4459            0 :         return cloudsync_set_error(data, buffer, rc);
    4460              :     }
    4461              : 
    4462              :     // drop blocks table if this table has block LWW columns
    4463            4 :     if (table->blocks_ref) {
    4464            1 :         sql = cloudsync_memory_mprintf(SQL_DROP_CLOUDSYNC_TABLE, table->blocks_ref);
    4465            1 :         rc = database_exec(data, sql);
    4466            1 :         cloudsync_memory_free(sql);
    4467            1 :         if (rc != DBRES_OK) {
    4468              :             char buffer[1024];
    4469            0 :             snprintf(buffer, sizeof(buffer), "Unable to drop blocks table %s_cloudsync_blocks in cloudsync_cleanup", table_name);
    4470            0 :             return cloudsync_set_error(data, buffer, rc);
    4471              :         }
    4472            1 :     }
    4473              : 
    4474              :     // drop original triggers
    4475            4 :     rc = database_delete_triggers(data, table_name);
    4476            4 :     if (rc != DBRES_OK) {
    4477              :         char buffer[1024];
    4478            0 :         snprintf(buffer, sizeof(buffer), "Unable to delete triggers for table %s", table_name);
    4479            0 :         return cloudsync_set_error(data, buffer, rc);
    4480              :     }
    4481              :     
    4482              :     // remove all table related settings
    4483            4 :     dbutils_table_settings_set_key_value(data, table_name, NULL, NULL, NULL);
    4484            4 :     return DBRES_OK;
    4485            4 : }
    4486              : 
    4487            4 : int cloudsync_cleanup (cloudsync_context *data, const char *table_name) {
    4488            4 :     cloudsync_table_context *table = table_lookup(data, table_name);
    4489            4 :     if (!table) return DBRES_OK;
    4490              :     
    4491              :     // TODO: check what happen if cloudsync_cleanup_internal failes (not eveything dropped) and the table is still in memory?
    4492              :     
    4493            4 :     int rc = cloudsync_cleanup_internal(data, table);
    4494            4 :     if (rc != DBRES_OK) return rc;
    4495              :     
    4496            4 :     int counter = table_remove(data, table);
    4497            4 :     table_free(table);
    4498              :     
    4499            4 :     if (counter == 0) {
    4500              :         // cleanup database on last table
    4501            2 :         cloudsync_reset_siteid(data);
    4502            2 :         dbutils_settings_cleanup(data);
    4503            2 :     } else {
    4504            2 :         if (database_internal_table_exists(data, CLOUDSYNC_TABLE_SETTINGS_NAME) == true) {
    4505            2 :             cloudsync_update_schema_hash(data);
    4506            2 :         }
    4507              :     }
    4508              :     
    4509            4 :     return DBRES_OK;
    4510            4 : }
    4511              : 
    4512            0 : int cloudsync_cleanup_all (cloudsync_context *data) {
    4513            0 :     return database_cleanup(data);
    4514              : }
    4515              : 
    4516          525 : int cloudsync_terminate (cloudsync_context *data) {
    4517              :     // can't use for/loop here because data->tables_count is changed by table_remove
    4518          801 :     while (data->tables_count > 0) {
    4519          276 :         cloudsync_table_context *t = data->tables[data->tables_count - 1];
    4520          276 :         table_remove(data, t);
    4521          276 :         table_free(t);
    4522              :     }
    4523              :     
    4524          525 :     if (data->schema_version_stmt) databasevm_finalize(data->schema_version_stmt);
    4525          525 :     if (data->data_version_stmt) databasevm_finalize(data->data_version_stmt);
    4526          525 :     if (data->db_version_stmt) databasevm_finalize(data->db_version_stmt);
    4527          525 :     if (data->getset_siteid_stmt) databasevm_finalize(data->getset_siteid_stmt);
    4528          525 :     if (data->current_schema) cloudsync_memory_free(data->current_schema);
    4529              :     
    4530          525 :     data->schema_version_stmt = NULL;
    4531          525 :     data->data_version_stmt = NULL;
    4532          525 :     data->db_version_stmt = NULL;
    4533          525 :     data->getset_siteid_stmt = NULL;
    4534          525 :     data->current_schema = NULL;
    4535              :     
    4536              :     // reset the site_id so the cloudsync_context_init will be executed again
    4537              :     // if any other cloudsync function is called after terminate
    4538          525 :     data->site_id[0] = 0;
    4539              :     
    4540          525 :     return 1;
    4541              : }
    4542              : 
    4543          307 : int cloudsync_init_table (cloudsync_context *data, const char *table_name, const char *algo_name, CLOUDSYNC_INIT_FLAG init_flags) {
    4544              :     // sanity check table and its primary key(s)
    4545          307 :     int rc = cloudsync_table_sanity_check(data, table_name, init_flags);
    4546          307 :     if (rc != DBRES_OK) return rc;
    4547              :     
    4548              :     // init cloudsync_settings
    4549          305 :     if (cloudsync_context_init(data) == NULL) {
    4550            0 :         return cloudsync_set_error(data, "Unable to initialize cloudsync context", DBRES_MISUSE);
    4551              :     }
    4552              :     
    4553              :     // sanity check algo name (if exists)
    4554          305 :     table_algo algo_new = table_algo_none;
    4555          305 :     if (!algo_name) algo_name = CLOUDSYNC_DEFAULT_ALGO;
    4556              :     
    4557          305 :     algo_new = cloudsync_algo_from_name(algo_name);
    4558          305 :     if (algo_new == table_algo_none) {
    4559              :         char buffer[1024];
    4560            1 :         snprintf(buffer, sizeof(buffer), "Unknown CRDT algorithm name %s", algo_name);
    4561            1 :         return cloudsync_set_error(data, buffer, DBRES_ERROR);
    4562              :     }
    4563              : 
    4564              :     // DWS and AWS algorithms are not yet implemented in the merge logic
    4565          304 :     if (algo_new == table_algo_crdt_dws || algo_new == table_algo_crdt_aws) {
    4566              :         char buffer[1024];
    4567            2 :         snprintf(buffer, sizeof(buffer), "CRDT algorithm %s is not yet supported", algo_name);
    4568            2 :         return cloudsync_set_error(data, buffer, DBRES_ERROR);
    4569              :     }
    4570              :     
    4571              :     // check if table name was already augmented
    4572          302 :     table_algo algo_current = dbutils_table_settings_get_algo(data, table_name);
    4573              :     
    4574              :     // sanity check algorithm
    4575          302 :     if ((algo_new == algo_current) && (algo_current != table_algo_none)) {
    4576              :         // if table algorithms and the same and not none, do nothing
    4577          302 :     } else if ((algo_new == table_algo_none) && (algo_current == table_algo_none)) {
    4578              :         // nothing is written into settings because the default table_algo_crdt_cls will be used
    4579            0 :         algo_new = algo_current = table_algo_crdt_cls;
    4580          274 :     } else if ((algo_new == table_algo_none) && (algo_current != table_algo_none)) {
    4581              :         // algo is already written into settins so just use it
    4582            0 :         algo_new = algo_current;
    4583          274 :     } else if ((algo_new != table_algo_none) && (algo_current == table_algo_none)) {
    4584              :         // write table algo name in settings
    4585          274 :         dbutils_table_settings_set_key_value(data, table_name, "*", "algo", algo_name);
    4586          274 :     } else {
    4587              :         // error condition
    4588            0 :         return cloudsync_set_error(data, "The function cloudsync_cleanup(table) must be called before changing a table algorithm", DBRES_MISUSE);
    4589              :     }
    4590              :     
    4591              :     // Run the following function even if table was already augmented.
    4592              :     // It is safe to call the following function multiple times, if there is nothing to update nothing will be changed.
    4593              :     // After an alter table, in contrast, all the cloudsync triggers, tables and stmts must be recreated.
    4594              :     
    4595              :     // sync algo with table (unused in this version)
    4596              :     // cloudsync_sync_table_key(data, table_name, "*", CLOUDSYNC_KEY_ALGO, crdt_algo_name(algo_new));
    4597              :     
    4598              :     // read row-level filter from settings (if any)
    4599              :     char init_filter_buf[2048];
    4600          302 :     int init_frc = dbutils_table_settings_get_value(data, table_name, "*", "filter", init_filter_buf, sizeof(init_filter_buf));
    4601          302 :     const char *init_filter = (init_frc == DBRES_OK && init_filter_buf[0]) ? init_filter_buf : NULL;
    4602              : 
    4603              :     // check triggers
    4604          302 :     rc = database_create_triggers(data, table_name, algo_new, init_filter);
    4605          302 :     if (rc != DBRES_OK) return cloudsync_set_error(data, "An error occurred while creating triggers", DBRES_MISUSE);
    4606              :     
    4607              :     // check meta-table
    4608          302 :     rc = database_create_metatable(data, table_name);
    4609          302 :     if (rc != DBRES_OK) return cloudsync_set_error(data, "An error occurred while creating metatable", DBRES_MISUSE);
    4610              :     
    4611              :     // add prepared statements
    4612          302 :     if (cloudsync_add_dbvms(data) != DBRES_OK) {
    4613            0 :         return cloudsync_set_error(data, "An error occurred while trying to compile prepared SQL statements", DBRES_MISUSE);
    4614              :     }
    4615              :     
    4616              :     // add table to in-memory data context
    4617          302 :     if (table_add_to_context(data, algo_new, table_name) == false) {
    4618              :         char buffer[1024];
    4619            0 :         snprintf(buffer, sizeof(buffer), "An error occurred while adding %s table information to global context", table_name);
    4620            0 :         return cloudsync_set_error(data, buffer, DBRES_MISUSE);
    4621              :     }
    4622              :     
    4623          302 :     if (cloudsync_refill_metatable(data, table_name) != DBRES_OK) {
    4624            0 :         return cloudsync_set_error(data, "An error occurred while trying to fill the augmented table", DBRES_MISUSE);
    4625              :     }
    4626              :         
    4627          302 :     return DBRES_OK;
    4628          307 : }
        

Generated by: LCOV version 2.5-0