LCOV - code coverage report
Current view: top level - src - cloudsync.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 88.5 % 3102 2746
Test Date: 2026-10-21 16:35:02 Functions: 92.0 % 174 160

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

Generated by: LCOV version 2.5-0