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 : }
|