LCOV - code coverage report
Current view: top level - src - vtab.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 201 201
Test Date: 2025-08-08 11:31:42 Functions: 100.0 % 17 17

            Line data    Source code
       1              : //
       2              : //  vtab.c
       3              : //  cloudsync
       4              : //
       5              : //  Created by Marco Bambini on 23/09/24.
       6              : //
       7              : 
       8              : #include <stdio.h>
       9              : #include <string.h>
      10              : #include "vtab.h"
      11              : #include "utils.h"
      12              : #include "dbutils.h"
      13              : #include "cloudsync.h"
      14              : 
      15              : #ifndef SQLITE_CORE
      16              : SQLITE_EXTENSION_INIT3
      17              : #endif
      18              : 
      19              : typedef struct cloudsync_changes_vtab {
      20              :     sqlite3_vtab            base;       // base class, must be first
      21              :     sqlite3                 *db;
      22              :     void                    *aux;
      23              : } cloudsync_changes_vtab;
      24              : 
      25              : typedef struct cloudsync_changes_cursor {
      26              :     sqlite3_vtab_cursor     base;       // base class, must be first
      27              :     cloudsync_changes_vtab  *vtab;
      28              :     sqlite3_stmt            *vm;        // prepared statement
      29              : } cloudsync_changes_cursor;
      30              : 
      31              : char *cloudsync_changes_columns[] = {"tbl", "pk", "col_name", "col_value", "col_version", "db_version", "site_id", "cl", "seq"};
      32              : #define COLNAME_FROM_INDEX(i)       cloudsync_changes_columns[i]
      33              : #define COL_TBL_INDEX               0
      34              : #define COL_PK_INDEX                1
      35              : #define COL_NAME_INDEX              2
      36              : #define COL_VALUE_INDEX             3
      37              : #define COL_VERSION_INDEX           4
      38              : #define COL_DBVERSION_INDEX         5
      39              : #define COL_SITEID_INDEX            6
      40              : #define COL_CL_INDEX                7
      41              : #define COL_SEQ_INDEX               8
      42              : 
      43              : #if CLOUDSYNC_UNITTEST
      44              : bool force_vtab_filter_abort = false;
      45              : #define CHECK_VFILTERTEST_ABORT()   if (force_vtab_filter_abort) rc = SQLITE_ERROR
      46              : #else
      47              : #define CHECK_VFILTERTEST_ABORT()
      48              : #endif
      49              : 
      50              : // MARK: -
      51              : 
      52           31 : const char *opname_from_value (int value) {
      53           31 :     switch (value) {
      54           15 :         case SQLITE_INDEX_CONSTRAINT_EQ: return "=";
      55            5 :         case SQLITE_INDEX_CONSTRAINT_GT: return ">";
      56            1 :         case SQLITE_INDEX_CONSTRAINT_LE: return "<=";
      57            1 :         case SQLITE_INDEX_CONSTRAINT_LT: return "<";
      58            1 :         case SQLITE_INDEX_CONSTRAINT_GE: return ">=";
      59            1 :         case SQLITE_INDEX_CONSTRAINT_LIKE: return "LIKE";
      60            1 :         case SQLITE_INDEX_CONSTRAINT_GLOB: return "GLOB";
      61            1 :         case SQLITE_INDEX_CONSTRAINT_NE: return "!=";
      62            1 :         case SQLITE_INDEX_CONSTRAINT_ISNOT: return "IS NOT";
      63            1 :         case SQLITE_INDEX_CONSTRAINT_ISNOTNULL: return "IS NOT NULL";
      64            1 :         case SQLITE_INDEX_CONSTRAINT_ISNULL: return "IS NULL";
      65            1 :         case SQLITE_INDEX_CONSTRAINT_IS: return "IS";
      66              :         
      67              :         // The REGEXP operator is a special syntax for the regexp() user function.
      68              :         // No regexp() user function is defined by default and so use of the REGEXP operator will normally result in an error message.
      69              :         // If an application-defined SQL function named "regexp" is added at run-time, then the "X REGEXP Y" operator will be implemented as
      70              :         // a call to "regexp(Y,X)".
      71              :         // case SQLITE_INDEX_CONSTRAINT_REGEXP: return "REGEX";
      72              :         
      73              :         // MATCH is only valid for virtual FTS tables
      74              :         // The MATCH operator is a special syntax for the match() application-defined function.
      75              :         // The default match() function implementation raises an exception and is not really useful for anything.
      76              :         // But extensions can override the match() function with more helpful logic.
      77              :         // case SQLITE_INDEX_CONSTRAINT_MATCH: return "MATCH";
      78              :     }
      79            1 :     return NULL;
      80           31 : }
      81              : 
      82            5 : int colname_is_legal (const char *name) {
      83            5 :     int count = sizeof(cloudsync_changes_columns) / sizeof (char *);
      84              :     
      85           36 :     for (int i=0; i<count; ++i) {
      86           35 :         if (strcasecmp(cloudsync_changes_columns[i], name) == 0) return 1;
      87           31 :     }
      88            1 :     return 0;
      89            5 : }
      90              : 
      91          114 : char *build_changes_sql (sqlite3 *db, const char *idxs) {
      92              :     DEBUG_VTAB("build_changes_sql");
      93              :     
      94              :     /*
      95              :      * This SQLite query dynamically generates and executes a consolidated query
      96              :      * to fetch changes from all tables related to cloud synchronization.
      97              :      *
      98              :      * It works in the following steps:
      99              :      *
     100              :      * 1. `table_names` CTE: Retrieves all table names in the database that end
     101              :      *    with '_cloudsync' and extracts the base table name (without the '_cloudsync' suffix).
     102              :      *
     103              :      * 2. `changes_query` CTE: Constructs individual SELECT statements for each
     104              :      *    cloud sync table, fetching data about changes in columns:
     105              :      *      - `pk`: Primary key of the table.
     106              :      *      - `col_name`: Name of the changed column.
     107              :      *      - `col_version`: Version of the changed column.
     108              :      *      - `db_version`: Database version when the change was recorded.
     109              :      *      - `site_id`: Site identifier associated with the change.
     110              :      *      - `cl`: Coalesced version (either `t2.col_version` or 1 if `t2.col_version` is NULL).
     111              :      *      - `seq`: Sequence number of the change.
     112              :      *    Each query is constructed by joining the table with `cloudsync_site_id`
     113              :      *    for resolving the site ID and performing a LEFT JOIN with itself to
     114              :      *    identify columns with NULL values in `t2.col_name`.
     115              :      *
     116              :      * 3. `union_query` CTE: Combines all the constructed SELECT statements
     117              :      *    from `changes_query` into a single query using `UNION ALL`.
     118              :      *
     119              :      * 4. `final_query` CTE: Wraps the combined UNION ALL query into another
     120              :      *    SELECT statement, preparing for additional filtering.
     121              :      *
     122              :      * 5. Final SELECT: Adds a WHERE clause to filter records with `db_version`
     123              :      *    greater than a specified value (using a placeholder `?`), and orders
     124              :      *    the results by `db_version` and `seq`.
     125              :      *
     126              :      * The overall result is a consolidated view of changes across all
     127              :      * cloud sync tables, filtered and ordered based on the `db_version` and
     128              :      * `seq` fields.
     129              :      */
     130              :     
     131          114 :     const char *query =
     132              :     "WITH table_names AS ( "
     133              :     "    SELECT format('%q',SUBSTR(tbl_name, 1, LENGTH(tbl_name) - 10)) AS table_name_literal, format('%w',SUBSTR(tbl_name, 1, LENGTH(tbl_name) - 10)) AS table_name_identifier, format('%w',tbl_name) AS table_meta "
     134              :     "    FROM sqlite_master "
     135              :     "    WHERE type = 'table' AND tbl_name LIKE '%_cloudsync' "
     136              :     "), "
     137              :     "changes_query AS ( "
     138              :     "    SELECT "
     139              :     "        'SELECT "
     140              :     "        ''' || \"table_name_literal\" || ''' AS tbl, "
     141              :     "        t1.pk AS pk, "
     142              :     "        t1.col_name AS col_name, "
     143              :     "        cloudsync_col_value(''' || \"table_name_literal\" || ''', t1.col_name, t1.pk) AS col_value, "
     144              :     "        t1.col_version AS col_version, "
     145              :     "        t1.db_version AS db_version, "
     146              :     "        site_tbl.site_id AS site_id, "
     147              :     "        t1.seq AS seq, "
     148              :     "        COALESCE(t2.col_version, 1) AS cl "
     149              :     "     FROM \"' || \"table_meta\" || '\" AS t1 "
     150              :     "     LEFT JOIN cloudsync_site_id AS site_tbl ON t1.site_id = site_tbl.rowid "
     151              :     "     LEFT JOIN \"' || \"table_meta\" || '\" AS t2 ON t1.pk = t2.pk AND t2.col_name = ''" CLOUDSYNC_TOMBSTONE_VALUE "'' "
     152              :     "     WHERE col_value IS NOT ''" CLOUDSYNC_RLS_RESTRICTED_VALUE "''' "
     153              :     "    AS query_string FROM table_names "
     154              :     "), "
     155              :     "union_query AS ( "
     156              :     "    SELECT GROUP_CONCAT(query_string, ' UNION ALL ') AS union_query FROM changes_query "
     157              :     "), "
     158              :     "final_query AS ( "
     159              :     "    SELECT "
     160              :     "        'SELECT tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq FROM (' || union_query || ')' "
     161              :     "    AS final_string FROM union_query "
     162              :     ") "
     163              :     "SELECT concat(final_string, ' ";
     164          114 :     const char *final_query = ";') FROM final_query;";
     165              :     
     166              :     static size_t query_len = 0;
     167              :     static size_t final_query_len = 0;
     168          114 :     if (query_len == 0) query_len = strlen(query);
     169          114 :     if (final_query_len == 0) final_query_len = strlen(final_query);
     170              :     
     171          114 :     size_t idx_len = strlen(idxs);
     172          114 :     size_t blen = query_len + idx_len + final_query_len + 128;
     173          114 :     char *sql = (char *)cloudsync_memory_alloc((sqlite3_uint64)blen);
     174          114 :     if (!sql) return NULL;
     175              :     
     176              :     // build final sql statement taking in account the dynamic idxs string provided by the user
     177          114 :     memcpy(sql, query, query_len);
     178          114 :     memcpy(sql + (query_len), idxs, idx_len);
     179          114 :     memcpy(sql + (query_len + idx_len), final_query, final_query_len+1);
     180              :         
     181          114 :     char *value = dbutils_text_select(db, sql);
     182          114 :     cloudsync_memory_free(sql);
     183              :     
     184          114 :     return value;
     185          114 : }
     186              : 
     187              : // MARK: -
     188              : 
     189           44 : int cloudsync_changesvtab_connect (sqlite3 *db, void *aux, int argc, const char *const *argv, sqlite3_vtab **vtab, char **err) {
     190              :     DEBUG_VTAB("cloudsync_changesvtab_connect");
     191              :     
     192           44 :     int rc = sqlite3_declare_vtab(db, "CREATE TABLE x (tbl TEXT NOT NULL, pk BLOB NOT NULL, col_name TEXT NOT NULL,"
     193              :                                   "col_value TEXT, col_version INTEGER NOT NULL, db_version INTEGER NOT NULL,"
     194              :                                   "site_id BLOB NOT NULL, cl INTEGER NOT NULL, seq INTEGER NOT NULL);");
     195           44 :     if (rc == SQLITE_OK) {
     196              :         // memory internally managed by SQLite, so I cannot use memory_alloc here
     197           44 :         cloudsync_changes_vtab *vnew = sqlite3_malloc64(sizeof(cloudsync_changes_vtab));
     198           44 :         if (vnew == NULL) return SQLITE_NOMEM;
     199              :         
     200           44 :         memset(vnew, 0, sizeof(cloudsync_changes_vtab));
     201           44 :         vnew->db = db;
     202           44 :         vnew->aux = aux;
     203              :         
     204           44 :         *vtab = (sqlite3_vtab *)vnew;
     205           44 :     }
     206              :     
     207           44 :     return rc;
     208           44 : }
     209              : 
     210           44 : int cloudsync_changesvtab_disconnect (sqlite3_vtab *vtab) {
     211              :     DEBUG_VTAB("cloudsync_changesvtab_disconnect");
     212              :     
     213           44 :     cloudsync_changes_vtab *p = (cloudsync_changes_vtab *)vtab;
     214           44 :     sqlite3_free(p);
     215           44 :     return SQLITE_OK;
     216              : }
     217              : 
     218          114 : int cloudsync_changesvtab_open (sqlite3_vtab *vtab, sqlite3_vtab_cursor **pcursor) {
     219              :     DEBUG_VTAB("cloudsync_changesvtab_open");
     220              :     
     221          114 :     cloudsync_changes_cursor *cursor = cloudsync_memory_alloc(sizeof(cloudsync_changes_cursor));
     222          114 :     if (cursor == NULL) return SQLITE_NOMEM;
     223              :     
     224          114 :     memset(cursor, 0, sizeof(cloudsync_changes_cursor));
     225          114 :     cursor->vtab = (cloudsync_changes_vtab *)vtab;
     226              :     
     227          114 :     *pcursor = (sqlite3_vtab_cursor *)cursor;
     228          114 :     return SQLITE_OK;
     229          114 : }
     230              : 
     231          114 : int cloudsync_changesvtab_close (sqlite3_vtab_cursor *cursor) {
     232              :     DEBUG_VTAB("cloudsync_changesvtab_close");
     233              :     
     234          114 :     cloudsync_changes_cursor *c = (cloudsync_changes_cursor *)cursor;
     235          114 :     if (c->vm) {
     236            2 :         sqlite3_finalize(c->vm);
     237            2 :         c->vm = NULL;
     238            2 :     }
     239              :     
     240          114 :     cloudsync_memory_free(cursor);
     241          114 :     return SQLITE_OK;
     242              : }
     243              : 
     244          115 : int cloudsync_changesvtab_best_index (sqlite3_vtab *vtab, sqlite3_index_info *idxinfo) {
     245              :     DEBUG_VTAB("cloudsync_changesvtab_best_index");
     246              :     
     247              :     // the goal here is to build a WHERE clause and gives an estimate
     248              :     // of the cost of that clause
     249              :     
     250              :     // we'll incrementally build the clause and we avoid the realloc
     251              :     // of memory, so perform a quick loop to estimate memory usage
     252              :     
     253              :     // count the number of contrainst and order by clauses
     254          115 :     int count1 = idxinfo->nConstraint;
     255          115 :     int count2 = idxinfo->nOrderBy;
     256              :     
     257              :     // col_version is the longest column name (11)
     258              :     // +1 for the space
     259              :     // IS NOT NULL is the longest constraint value (11)
     260              :     // +1 for the space
     261              :     // +1 for the ? character
     262              :     // +5 for space AND space
     263              :     // +512 for the extra space and for the WHERE and ORDER BY literals
     264              :     
     265              :     // memory internally manager by SQLite, so I cannot use memory_alloc here
     266          115 :     size_t slen = (count1 * (11 + 1 + 11 + 1 + 5)) + (count2 * 11 + 1 + 5) + 512;
     267          115 :     char *s = (char *)sqlite3_malloc64((sqlite3_uint64)slen);
     268          115 :     if (!s) return SQLITE_NOMEM;
     269          115 :     size_t sindex= 0;
     270              : 
     271          115 :     int idxnum = 0;
     272          115 :     int arg_index = 1;
     273          115 :     int orderconsumed = 1;
     274              :     
     275              :     // is there a WHERE clause ?
     276          115 :     if (count1 > 0) sindex += snprintf(s+sindex, slen-sindex, "WHERE ");
     277              :     
     278              :     // check constraints
     279          145 :     for (int i=0; i < count1; ++i) {
     280              :         // analyze only usable constraints
     281           30 :         struct sqlite3_index_constraint *constraint = &idxinfo->aConstraint[i];
     282           30 :         if (constraint->usable == false) continue;
     283              :         
     284           30 :         int idx = constraint->iColumn;
     285           30 :         uint8_t op = constraint->op;
     286              :         
     287           30 :         const char *colname = (idx > 0) ? COLNAME_FROM_INDEX(idx) : "rowid";
     288           30 :         const char *opname = opname_from_value(op);
     289           30 :         if (!opname) continue;
     290              :         
     291              :         // build next constraint
     292           30 :         if (i > 0) sindex += snprintf(s+sindex, slen-sindex, " AND ");
     293              :         
     294              :         // handle special case where value is not needed
     295           30 :         if ((op == SQLITE_INDEX_CONSTRAINT_ISNULL) || (op == SQLITE_INDEX_CONSTRAINT_ISNOTNULL)) {
     296            2 :             sindex += snprintf(s+sindex, slen-sindex, "%s %s", colname, opname);
     297            2 :             idxinfo->aConstraintUsage[i].argvIndex = 0;
     298            2 :         } else {
     299           28 :             sindex += snprintf(s+sindex, slen-sindex, "%s %s ?", colname, opname);
     300           28 :             idxinfo->aConstraintUsage[i].argvIndex = arg_index++;
     301              :         }
     302           30 :         idxinfo->aConstraintUsage[i].omit = 1;
     303              :         
     304              :         //a bitmask (idxnum) is built up based on which constraints are applied
     305           30 :         if (idx == COL_DBVERSION_INDEX) idxnum |= 2;    // set bit 1
     306           17 :         else if (idx == COL_SITEID_INDEX) idxnum |= 4;  // set bit 2
     307           30 :     }
     308              :     
     309              :     // is there an ORDER BY clause ?
     310              :     // if not use a default one
     311          115 :     if (count2 > 0) sindex += snprintf(s+sindex, slen-sindex, " ORDER BY ");
     312          113 :     else sindex += snprintf(s+sindex, slen-sindex, " ORDER BY db_version, seq ASC");
     313              :     
     314          118 :     for (int i=0; i < count2; ++i) {
     315            3 :         struct sqlite3_index_orderby *orderby = &idxinfo->aOrderBy[i];
     316              :         
     317              :         // build next constraint
     318            3 :         if (i > 0) sindex += snprintf(s+sindex, slen-sindex, ", ");
     319              :         
     320            3 :         int idx = orderby->iColumn;
     321            3 :         const char *colname = COLNAME_FROM_INDEX(idx);
     322            3 :         if (!colname_is_legal(colname)) orderconsumed = 0;
     323              :         
     324            3 :         sindex += snprintf(s+sindex, slen-sindex, "%s %s", colname, orderby->desc ? " DESC" : " ASC");
     325            3 :     }
     326              :     
     327          115 :     idxinfo->idxNum = idxnum;
     328          115 :     idxinfo->idxStr = s;
     329          115 :     idxinfo->needToFreeIdxStr = 1;
     330          115 :     idxinfo->orderByConsumed = orderconsumed;
     331              :     
     332              :     // the goal of the xBestIndex function is to help SQLite's query planner decide on the most efficient way
     333              :     // to execute a query on the virtual table. It does so by evaluating which constraints (filters) can be applied
     334              :     // and providing an estimate of the cost and number of rows that the query will return.
     335              :     
     336              :     
     337              :     /*
     338              :      
     339              :      By commenting the following code we assume that the developer is using an SQLite library
     340              :      more recent than 3.8.2 released on 2013-12-06
     341              :      
     342              :      int version = sqlite3_libversion_number();
     343              :      if (version >= 3008002) {
     344              :      // field sqlite3_int64 estimatedRows is available (estimated number of rows returned)
     345              :      }
     346              :      
     347              :      if (version >= 3009000) {
     348              :      // field int idxFlags is available (mask of SQLITE_INDEX_SCAN)
     349              :      }
     350              :      
     351              :      if (version >= 3010000) {
     352              :      // field sqlite3_uint64 colUsed is available (input: mask of columns used by statement)
     353              :      }
     354              :      
     355              :      */
     356              :     
     357              :     // perform estimated cost and row count based on the constraints
     358          115 :     if ((idxnum & 6) == 6) {
     359              :         // both DbVrsn and SiteId constraints are present
     360              :         // query is expected to be highly selective, returning only one row, with a very low execution cost
     361            1 :         idxinfo->estimatedCost = 1.0;
     362            1 :         idxinfo->estimatedRows = 1;
     363          115 :     } else if ((idxnum & 2) == 2) {
     364              :         // only DbVrsn constraint is present
     365              :         // query is expected to return more rows (10) and take more time (cost of 10.0) than in the previous case
     366            9 :         idxinfo->estimatedCost = 10.0;
     367            9 :         idxinfo->estimatedRows = 10;
     368          114 :     } else if ((idxnum & 4) == 4) {
     369              :         // only SiteId constraint is present
     370              :         // query is expected to be very inefficient, returning a large number of rows and taking a long time to execute
     371           14 :         idxinfo->estimatedCost = (double)INT32_MAX;
     372           14 :         idxinfo->estimatedRows = (sqlite3_int64)INT32_MAX;
     373           14 :     } else {
     374              :         // no constraints are present
     375              :         // worst-case scenario, where the query returns all rows from the virtual table
     376           91 :         idxinfo->estimatedCost = (double)INT64_MAX;
     377           91 :         idxinfo->estimatedRows = (sqlite3_int64)INT64_MAX;
     378              :     }
     379              :     
     380          115 :     return SQLITE_OK;
     381          115 : }
     382              : 
     383              : 
     384          114 : int cloudsync_changesvtab_filter (sqlite3_vtab_cursor *cursor, int idxn, const char *idxs, int argc, sqlite3_value **argv) {
     385              :     DEBUG_VTAB("cloudsync_changesvtab_filter");
     386              :     
     387          114 :     cloudsync_changes_cursor *c = (cloudsync_changes_cursor *)cursor;
     388          114 :     sqlite3 *db = c->vtab->db;
     389          114 :     char *sql = build_changes_sql(db, idxs);
     390          114 :     if (sql == NULL) return SQLITE_NOMEM;
     391              :     
     392              :     // the xFilter method may be called multiple times on the same sqlite3_vtab_cursor*
     393          114 :     if (c->vm) sqlite3_finalize(c->vm);
     394          114 :     c->vm = NULL;
     395              :     
     396          114 :     int rc = sqlite3_prepare_v2(db, sql, -1, &c->vm, NULL);
     397          114 :     cloudsync_memory_free(sql);
     398          114 :     if (rc != SQLITE_OK) goto abort_filter;
     399              :     
     400          142 :     for (int i=0; i<argc; ++i) {
     401           28 :         rc = sqlite3_bind_value(c->vm, i+1, argv[i]);
     402           28 :         if (rc != SQLITE_OK) goto abort_filter;
     403           28 :     }
     404              :     
     405          114 :     rc = sqlite3_step(c->vm);
     406          114 :     CHECK_VFILTERTEST_ABORT();
     407              :     
     408          114 :     if (rc == SQLITE_DONE) {
     409           14 :         sqlite3_finalize(c->vm);
     410           14 :         c->vm = NULL;
     411          114 :     } else if (rc != SQLITE_ROW) {
     412            1 :         goto abort_filter;
     413              :     }
     414              :     
     415          113 :     return SQLITE_OK;
     416              :     
     417              : abort_filter:
     418              :     // error condition
     419              :     DEBUG_VTAB("cloudsync_changesvtab_filter: %s\n", sqlite3_errmsg(db));
     420            1 :     if (c->vm) {
     421            1 :         sqlite3_finalize(c->vm);
     422            1 :         c->vm = NULL;
     423            1 :     }
     424            1 :     return rc;
     425          114 : }
     426              : 
     427         4587 : int cloudsync_changesvtab_next (sqlite3_vtab_cursor *cursor) {
     428              :     DEBUG_VTAB("cloudsync_changesvtab_next");
     429              :     
     430         4587 :     cloudsync_changes_cursor *c = (cloudsync_changes_cursor *)cursor;
     431         4587 :     int rc = sqlite3_step(c->vm);
     432              :     
     433         4587 :     if (rc == SQLITE_DONE) {
     434           97 :         sqlite3_finalize(c->vm);
     435           97 :         c->vm = NULL;
     436           97 :         rc = SQLITE_OK;
     437         4587 :     } else if (rc == SQLITE_ROW) {
     438         4490 :         rc = SQLITE_OK;
     439         4490 :     }
     440              :     
     441         4587 :     if (rc != SQLITE_OK) DEBUG_VTAB("cloudsync_changesvtab_next: %s\n", sqlite3_errmsg(c->vtab->db));
     442         4587 :     return rc;
     443              : }
     444              : 
     445         4700 : int cloudsync_changesvtab_eof (sqlite3_vtab_cursor *cursor) {
     446              :     DEBUG_VTAB("cloudsync_changesvtab_eof");
     447              :     
     448              :     // we must return false (zero) if the specified cursor currently points to a valid row of data, or true (non-zero) otherwise
     449         4700 :     cloudsync_changes_cursor *c = (cloudsync_changes_cursor *)cursor;
     450         4700 :     return (c->vm) ? 0 : 1;
     451              : }
     452              : 
     453        39189 : int cloudsync_changesvtab_column (sqlite3_vtab_cursor *cursor, sqlite3_context *ctx, int col) {
     454              :     DEBUG_VTAB("cloudsync_changesvtab_column %d\n", col);
     455              :     
     456        39189 :     cloudsync_changes_cursor *c = (cloudsync_changes_cursor *)cursor;
     457        39189 :     sqlite3_value *value = sqlite3_column_value(c->vm, col);
     458        39189 :     sqlite3_result_value(ctx, value);
     459              :     
     460        39189 :     return SQLITE_OK;
     461              : }
     462              : 
     463           88 : int cloudsync_changesvtab_rowid (sqlite3_vtab_cursor *cursor, sqlite3_int64 *rowid) {
     464              :     DEBUG_VTAB("cloudsync_changesvtab_rowid");
     465              :     
     466           88 :     cloudsync_changes_cursor *c = (cloudsync_changes_cursor *)cursor;
     467           88 :     sqlite3_int64 seq = sqlite3_column_int64(c->vm, COL_SEQ_INDEX);
     468           88 :     sqlite3_int64 db_version = sqlite3_column_int64(c->vm, COL_DBVERSION_INDEX);
     469              :     
     470              :     // for an explanation see https://github.com/sqliteai/sqlite-sync/blob/main/docs/RowID.md
     471           88 :     *rowid = (db_version << 30) | seq;
     472           88 :     return SQLITE_OK;
     473              : }
     474              : 
     475         4191 : int cloudsync_changesvtab_update (sqlite3_vtab *vtab, int argc, sqlite3_value **argv, sqlite3_int64 *rowid) {
     476              :     DEBUG_VTAB("cloudsync_changesvtab_update");
     477              :     
     478              :     // only INSERT statements are allowed
     479         4191 :     bool is_insert = (argc > 1 && sqlite3_value_type(argv[0]) == SQLITE_NULL);
     480         4191 :     if (!is_insert) {
     481            1 :         cloudsync_vtab_set_error(vtab, "Only INSERT and SELECT statements are allowed against the cloudsync_changes table");
     482            1 :         return SQLITE_MISUSE;
     483              :     }
     484              :     
     485              :     // argv[0] is set only in case of DELETE statement (it contains the rowid of a row in the virtual table to be deleted)
     486              :     // argv[1] is the rowid of a new row to be inserted into the virtual table (always NULL in our case)
     487              :     // so reduce the number of meaningful arguments by 2
     488         4190 :     return cloudsync_merge_insert(vtab, argc-2, &argv[2], rowid);
     489         4191 : }
     490              : 
     491              : // MARK: -
     492              : 
     493         4190 : cloudsync_context *cloudsync_vtab_get_context (sqlite3_vtab *vtab) {
     494         4190 :     return (cloudsync_context *)(((cloudsync_changes_vtab *)vtab)->aux);
     495              : }
     496              : 
     497            1 : int cloudsync_vtab_set_error (sqlite3_vtab *vtab, const char *format, ...) {
     498              :     va_list arg;
     499            1 :     va_start (arg, format);
     500            1 :     char *err = cloudsync_memory_vmprintf(format, arg);
     501            1 :     va_end (arg);
     502              :     
     503            1 :     if (vtab->zErrMsg) cloudsync_memory_free(vtab->zErrMsg);
     504            1 :     vtab->zErrMsg = err;
     505            1 :     return SQLITE_ERROR;
     506              : }
     507              : 
     508           46 : int cloudsync_vtab_register_changes (sqlite3 *db, cloudsync_context *xdata) {
     509              :     static sqlite3_module cloudsync_changes_module = {
     510              :         /* iVersion    */ 0,
     511              :         /* xCreate     */ 0, // Eponymous only virtual table
     512              :         /* xConnect    */ cloudsync_changesvtab_connect,
     513              :         /* xBestIndex  */ cloudsync_changesvtab_best_index,
     514              :         /* xDisconnect */ cloudsync_changesvtab_disconnect,
     515              :         /* xDestroy    */ 0,
     516              :         /* xOpen       */ cloudsync_changesvtab_open,
     517              :         /* xClose      */ cloudsync_changesvtab_close,
     518              :         /* xFilter     */ cloudsync_changesvtab_filter,
     519              :         /* xNext       */ cloudsync_changesvtab_next,
     520              :         /* xEof        */ cloudsync_changesvtab_eof,
     521              :         /* xColumn     */ cloudsync_changesvtab_column,
     522              :         /* xRowid      */ cloudsync_changesvtab_rowid,
     523              :         /* xUpdate     */ cloudsync_changesvtab_update,
     524              :         /* xBegin      */ 0,
     525              :         /* xSync       */ 0,
     526              :         /* xCommit     */ 0,
     527              :         /* xRollback   */ 0,
     528              :         /* xFindMethod */ 0,
     529              :         /* xRename     */ 0,
     530              :         /* xSavepoint  */ 0,
     531              :         /* xRelease    */ 0,
     532              :         /* xRollbackTo */ 0,
     533              :         /* xShadowName */ 0,
     534              :         /* xIntegrity  */ 0
     535              :     };
     536              :     
     537           46 :     return sqlite3_create_module(db, "cloudsync_changes", &cloudsync_changes_module, (void *)xdata);
     538              : }
        

Generated by: LCOV version 2.3.1-1