Line data Source code
1 : //
2 : // cloudsync_sqlite.c
3 : // cloudsync
4 : //
5 : // Created by Marco Bambini on 05/12/25.
6 : //
7 :
8 : #include "cloudsync_sqlite.h"
9 : #include "cloudsync_changes_sqlite.h"
10 : #include "../pk.h"
11 : #include "../cloudsync.h"
12 : #include "../block.h"
13 : #include "../database.h"
14 : #include "../dbutils.h"
15 : #include "../sql.h"
16 : #include <inttypes.h>
17 : #include <stddef.h>
18 :
19 : #ifndef CLOUDSYNC_OMIT_NETWORK
20 : #include "../network/network.h"
21 : #endif
22 :
23 : #ifndef SQLITE_CORE
24 : SQLITE_EXTENSION_INIT1
25 : #endif
26 :
27 : #ifndef UNUSED_PARAMETER
28 : #define UNUSED_PARAMETER(X) (void)(X)
29 : #endif
30 :
31 : #ifdef _WIN32
32 : #define APIEXPORT __declspec(dllexport)
33 : #else
34 : #define APIEXPORT
35 : #endif
36 :
37 : typedef struct {
38 : sqlite3_context *context;
39 : int index;
40 : } cloudsync_pk_decode_context;
41 :
42 : typedef struct {
43 : sqlite3_value *table_name;
44 : sqlite3_value **new_values;
45 : sqlite3_value **old_values;
46 : int count;
47 : int capacity;
48 : } cloudsync_update_payload;
49 :
50 6 : void dbsync_set_error (sqlite3_context *context, const char *format, ...) {
51 : char buffer[2048];
52 :
53 : va_list arg;
54 6 : va_start (arg, format);
55 6 : vsnprintf(buffer, sizeof(buffer), format, arg);
56 6 : va_end (arg);
57 :
58 6 : if (context) sqlite3_result_error(context, buffer, -1);
59 6 : }
60 :
61 : // MARK: - Public -
62 :
63 8 : void dbsync_version (sqlite3_context *context, int argc, sqlite3_value **argv) {
64 : DEBUG_FUNCTION("cloudsync_version");
65 8 : UNUSED_PARAMETER(argc);
66 8 : UNUSED_PARAMETER(argv);
67 8 : sqlite3_result_text(context, CLOUDSYNC_VERSION, -1, SQLITE_STATIC);
68 8 : }
69 :
70 538 : void dbsync_siteid (sqlite3_context *context, int argc, sqlite3_value **argv) {
71 : DEBUG_FUNCTION("cloudsync_siteid");
72 538 : UNUSED_PARAMETER(argc);
73 538 : UNUSED_PARAMETER(argv);
74 :
75 538 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
76 538 : sqlite3_result_blob(context, cloudsync_siteid(data), UUID_LEN, SQLITE_STATIC);
77 538 : }
78 :
79 3 : void dbsync_db_version (sqlite3_context *context, int argc, sqlite3_value **argv) {
80 : DEBUG_FUNCTION("cloudsync_db_version");
81 3 : UNUSED_PARAMETER(argc);
82 3 : UNUSED_PARAMETER(argv);
83 :
84 : // retrieve context
85 3 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
86 :
87 3 : int rc = cloudsync_dbversion_check_uptodate(data);
88 3 : if (rc != SQLITE_OK) {
89 : // When cloudsync_init was never called, data_version_stmt is NULL and
90 : // database_errmsg() falls back to "not an error", producing the
91 : // confusing "Unable to retrieve db_version (not an error)". Detect the
92 : // uninitialized state and return an actionable message instead. The
93 : // extra check only runs on the error branch, so it costs nothing on
94 : // the sync hot path (merge operations keep going through the normal
95 : // path where rc == SQLITE_OK).
96 1 : if (!cloudsync_context_is_initialized(data)) {
97 1 : dbsync_set_error(context,
98 : "cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
99 : "to enable sync on a table before calling cloudsync_db_version().");
100 1 : } else {
101 0 : dbsync_set_error(context, "Unable to retrieve db_version (%s).", database_errmsg(data));
102 : }
103 1 : return;
104 : }
105 :
106 2 : sqlite3_result_int64(context, cloudsync_dbversion(data));
107 3 : }
108 :
109 27238 : void dbsync_db_version_next (sqlite3_context *context, int argc, sqlite3_value **argv) {
110 : DEBUG_FUNCTION("cloudsync_db_version_next");
111 :
112 : // retrieve context
113 27238 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
114 :
115 27238 : sqlite3_int64 merging_version = (argc == 1) ? database_value_int(argv[0]) : CLOUDSYNC_VALUE_NOTSET;
116 27238 : sqlite3_int64 value = cloudsync_dbversion_next(data, merging_version);
117 27238 : if (value == -1) {
118 1 : if (!cloudsync_context_is_initialized(data)) {
119 1 : dbsync_set_error(context,
120 : "cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
121 : "to enable sync on a table before calling cloudsync_db_version_next().");
122 1 : } else {
123 0 : dbsync_set_error(context, "Unable to retrieve next_db_version (%s).", database_errmsg(data));
124 : }
125 1 : return;
126 : }
127 :
128 27237 : sqlite3_result_int64(context, value);
129 27238 : }
130 :
131 58 : void dbsync_seq (sqlite3_context *context, int argc, sqlite3_value **argv) {
132 : DEBUG_FUNCTION("cloudsync_seq");
133 :
134 : // retrieve context
135 58 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
136 58 : sqlite3_result_int(context, cloudsync_bumpseq(data));
137 58 : }
138 :
139 5 : void dbsync_uuid (sqlite3_context *context, int argc, sqlite3_value **argv) {
140 : DEBUG_FUNCTION("cloudsync_uuid");
141 :
142 : char value[UUID_STR_MAXLEN];
143 5 : char *uuid = cloudsync_uuid_v7_string(value, true);
144 5 : sqlite3_result_text(context, uuid, -1, SQLITE_TRANSIENT);
145 5 : }
146 :
147 : // cloudsync_uuid_text(blob, [dash_format]) -> canonical UUID string
148 4 : void dbsync_uuid_text (sqlite3_context *context, int argc, sqlite3_value **argv) {
149 : DEBUG_FUNCTION("cloudsync_uuid_text");
150 :
151 4 : if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { sqlite3_result_null(context); return; }
152 4 : if (sqlite3_value_type(argv[0]) != SQLITE_BLOB || sqlite3_value_bytes(argv[0]) != UUID_LEN) {
153 0 : sqlite3_result_error(context, "cloudsync_uuid_text: expected a 16-byte BLOB.", -1);
154 0 : return;
155 : }
156 4 : bool dash_format = (argc > 1) ? (sqlite3_value_int(argv[1]) != 0) : true;
157 : char value[UUID_STR_MAXLEN];
158 4 : cloudsync_uuid_v7_stringify((uint8_t *)sqlite3_value_blob(argv[0]), value, dash_format);
159 4 : sqlite3_result_text(context, value, -1, SQLITE_TRANSIENT);
160 4 : }
161 :
162 : // cloudsync_uuid_blob(text) -> 16-byte UUID blob (accepts dashed/undashed)
163 3 : void dbsync_uuid_blob (sqlite3_context *context, int argc, sqlite3_value **argv) {
164 : DEBUG_FUNCTION("cloudsync_uuid_blob");
165 :
166 3 : if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { sqlite3_result_null(context); return; }
167 3 : const char *str = (const char *)sqlite3_value_text(argv[0]);
168 3 : int len = sqlite3_value_bytes(argv[0]);
169 : uint8_t uuid[UUID_LEN];
170 3 : if (!str || cloudsync_uuid_v7_parse(str, len, uuid) != 0) {
171 0 : sqlite3_result_error(context, "cloudsync_uuid_blob: malformed UUID string.", -1);
172 0 : return;
173 : }
174 3 : sqlite3_result_blob(context, uuid, UUID_LEN, SQLITE_TRANSIENT);
175 3 : }
176 :
177 : // MARK: -
178 :
179 11 : void dbsync_set (sqlite3_context *context, int argc, sqlite3_value **argv) {
180 : DEBUG_FUNCTION("cloudsync_set");
181 :
182 : // sanity check parameters
183 11 : const char *key = (const char *)database_value_text(argv[0]);
184 11 : const char *value = (const char *)database_value_text(argv[1]);
185 :
186 : // silently fails
187 11 : if (key == NULL) return;
188 :
189 11 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
190 11 : dbutils_settings_set_key_value(data, key, value);
191 11 : }
192 :
193 78 : void dbsync_set_column (sqlite3_context *context, int argc, sqlite3_value **argv) {
194 : DEBUG_FUNCTION("cloudsync_set_column");
195 :
196 78 : const char *tbl = (const char *)database_value_text(argv[0]);
197 78 : const char *col = (const char *)database_value_text(argv[1]);
198 78 : const char *key = (const char *)database_value_text(argv[2]);
199 78 : const char *value = (const char *)database_value_text(argv[3]);
200 :
201 78 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
202 :
203 : // Handle block column setup: cloudsync_set_column('tbl', 'col', 'algo', 'block')
204 78 : if (key && value && strcmp(key, "algo") == 0 && strcmp(value, "block") == 0) {
205 73 : int rc = cloudsync_setup_block_column(data, tbl, col, NULL, true);
206 73 : if (rc != DBRES_OK) {
207 0 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
208 0 : }
209 73 : return;
210 : }
211 :
212 : // Handle delimiter setting: cloudsync_set_column('tbl', 'col', 'delimiter', '\n\n')
213 5 : if (key && strcmp(key, "delimiter") == 0) {
214 3 : cloudsync_table_context *table = table_lookup(data, tbl);
215 3 : if (table) {
216 3 : int col_idx = table_col_index(table, col);
217 3 : if (col_idx >= 0 && table_col_algo(table, col_idx) == col_algo_block) {
218 3 : table_set_col_delimiter(table, col_idx, value);
219 3 : }
220 3 : }
221 3 : }
222 :
223 5 : dbutils_table_settings_set_key_value(data, tbl, col, key, value);
224 78 : }
225 :
226 2 : void dbsync_set_table (sqlite3_context *context, int argc, sqlite3_value **argv) {
227 : DEBUG_FUNCTION("cloudsync_set_table");
228 :
229 2 : const char *tbl = (const char *)database_value_text(argv[0]);
230 2 : const char *key = (const char *)database_value_text(argv[1]);
231 2 : const char *value = (const char *)database_value_text(argv[2]);
232 :
233 2 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
234 2 : dbutils_table_settings_set_key_value(data, tbl, "*", key, value);
235 2 : }
236 :
237 2 : void dbsync_set_schema (sqlite3_context *context, int argc, sqlite3_value **argv) {
238 : DEBUG_FUNCTION("dbsync_set_schema");
239 :
240 2 : const char *schema = (const char *)database_value_text(argv[0]);
241 2 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
242 2 : cloudsync_set_schema(data, schema);
243 2 : }
244 :
245 2 : void dbsync_schema (sqlite3_context *context, int argc, sqlite3_value **argv) {
246 : DEBUG_FUNCTION("dbsync_schema");
247 :
248 2 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
249 2 : const char *schema = cloudsync_schema(data);
250 2 : (schema) ? sqlite3_result_text(context, schema, -1, NULL) : sqlite3_result_null(context);
251 2 : }
252 :
253 2 : void dbsync_table_schema (sqlite3_context *context, int argc, sqlite3_value **argv) {
254 : DEBUG_FUNCTION("dbsync_table_schema");
255 :
256 2 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
257 2 : const char *table_name = (const char *)database_value_text(argv[0]);
258 2 : const char *schema = cloudsync_table_schema(data, table_name);
259 2 : (schema) ? sqlite3_result_text(context, schema, -1, NULL) : sqlite3_result_null(context);
260 2 : }
261 :
262 15497 : void dbsync_is_sync (sqlite3_context *context, int argc, sqlite3_value **argv) {
263 : DEBUG_FUNCTION("cloudsync_is_sync");
264 :
265 15497 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
266 15497 : if (cloudsync_insync(data)) {
267 10205 : sqlite3_result_int(context, 1);
268 10205 : return;
269 : }
270 :
271 5292 : const char *table_name = (const char *)database_value_text(argv[0]);
272 5292 : cloudsync_table_context *table = table_lookup(data, table_name);
273 5292 : sqlite3_result_int(context, (table) ? (table_enabled(table) == 0) : 0);
274 15497 : }
275 :
276 144212 : void dbsync_col_value (sqlite3_context *context, int argc, sqlite3_value **argv) {
277 : // DEBUG_FUNCTION("cloudsync_col_value");
278 :
279 : // argv[0] -> table name
280 : // argv[1] -> column name
281 : // argv[2] -> encoded pk
282 :
283 : // retrieve column name
284 144212 : const char *col_name = (const char *)database_value_text(argv[1]);
285 144212 : if (!col_name) {
286 0 : dbsync_set_error(context, "Column name cannot be NULL");
287 0 : return;
288 : }
289 :
290 : // check for special tombstone value
291 144212 : if (strcmp(col_name, CLOUDSYNC_TOMBSTONE_VALUE) == 0) {
292 4098 : sqlite3_result_null(context);
293 4098 : return;
294 : }
295 :
296 : // lookup table
297 140114 : const char *table_name = (const char *)database_value_text(argv[0]);
298 140114 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
299 140114 : cloudsync_table_context *table = table_lookup(data, table_name);
300 140114 : if (!table) {
301 0 : dbsync_set_error(context, "Unable to retrieve table name %s in clousdsync_colvalue.", table_name);
302 0 : return;
303 : }
304 :
305 : // Block column: if col_name contains \x1F, read from blocks table
306 140114 : if (block_is_block_colname(col_name) && table_has_block_cols(table)) {
307 981 : dbvm_t *bvm = table_block_value_read_stmt(table);
308 981 : if (!bvm) {
309 0 : sqlite3_result_null(context);
310 0 : return;
311 : }
312 981 : int rc = databasevm_bind_blob(bvm, 1, database_value_blob(argv[2]), database_value_bytes(argv[2]));
313 981 : if (rc != DBRES_OK) { databasevm_reset(bvm); sqlite3_result_error(context, database_errmsg(data), -1); return; }
314 981 : rc = databasevm_bind_text(bvm, 2, col_name, -1);
315 981 : if (rc != DBRES_OK) { databasevm_reset(bvm); sqlite3_result_error(context, database_errmsg(data), -1); return; }
316 :
317 981 : rc = databasevm_step(bvm);
318 981 : if (rc == SQLITE_ROW) {
319 902 : sqlite3_result_value(context, database_column_value(bvm, 0));
320 981 : } else if (rc == SQLITE_DONE) {
321 79 : sqlite3_result_null(context);
322 79 : } else {
323 0 : sqlite3_result_error(context, database_errmsg(data), -1);
324 : }
325 981 : databasevm_reset(bvm);
326 981 : return;
327 : }
328 :
329 : // extract the right col_value vm associated to the column name
330 139133 : sqlite3_stmt *vm = table_column_lookup(table, col_name, false, NULL);
331 139133 : if (!vm) {
332 0 : sqlite3_result_error(context, "Unable to retrieve column value precompiled statement in clousdsync_colvalue.", -1);
333 0 : return;
334 : }
335 :
336 : // bind primary key values
337 139133 : int rc = pk_decode_prikey((char *)database_value_blob(argv[2]), (size_t)database_value_bytes(argv[2]), pk_decode_bind_callback, (void *)vm);
338 139133 : if (rc < 0) goto cleanup;
339 :
340 : // execute vm
341 139133 : rc = databasevm_step(vm);
342 278266 : if (rc == SQLITE_DONE) {
343 0 : rc = SQLITE_OK;
344 0 : sqlite3_result_text(context, CLOUDSYNC_RLS_RESTRICTED_VALUE, -1, SQLITE_STATIC);
345 139133 : } else if (rc == SQLITE_ROW) {
346 : // store value result
347 139133 : rc = SQLITE_OK;
348 139133 : sqlite3_result_value(context, database_column_value(vm, 0));
349 139133 : }
350 :
351 : cleanup:
352 139133 : if (rc != SQLITE_OK) {
353 0 : sqlite3_result_error(context, database_errmsg(data), -1);
354 0 : }
355 139133 : databasevm_reset(vm);
356 144212 : }
357 :
358 10569 : void dbsync_pk_encode (sqlite3_context *context, int argc, sqlite3_value **argv) {
359 10569 : size_t bsize = 0;
360 10569 : char *buffer = pk_encode_prikey((dbvalue_t **)argv, argc, NULL, &bsize);
361 10569 : if (!buffer || buffer == PRIKEY_NULL_CONSTRAINT_ERROR) {
362 1 : sqlite3_result_null(context);
363 1 : return;
364 : }
365 10568 : sqlite3_result_blob(context, (const void *)buffer, (int)bsize, SQLITE_TRANSIENT);
366 10568 : cloudsync_memory_free(buffer);
367 10569 : }
368 :
369 19 : int dbsync_pk_decode_set_result_callback (void *xdata, int index, int type, int64_t ival, double dval, char *pval) {
370 19 : cloudsync_pk_decode_context *decode_context = (cloudsync_pk_decode_context *)xdata;
371 : // decode_context->index is 1 based
372 : // index is 0 based
373 19 : if (decode_context->index != index+1) return SQLITE_OK;
374 :
375 7 : int rc = 0;
376 7 : sqlite3_context *context = decode_context->context;
377 7 : switch (type) {
378 : case SQLITE_INTEGER:
379 3 : sqlite3_result_int64(context, ival);
380 3 : break;
381 :
382 : case SQLITE_FLOAT:
383 2 : sqlite3_result_double(context, dval);
384 2 : break;
385 :
386 : case SQLITE_NULL:
387 0 : sqlite3_result_null(context);
388 0 : break;
389 :
390 : case SQLITE_TEXT:
391 1 : sqlite3_result_text(context, pval, (int)ival, SQLITE_TRANSIENT);
392 1 : break;
393 :
394 : case SQLITE_BLOB:
395 1 : sqlite3_result_blob(context, pval, (int)ival, SQLITE_TRANSIENT);
396 1 : break;
397 : }
398 :
399 7 : return rc;
400 19 : }
401 :
402 :
403 7 : void dbsync_pk_decode (sqlite3_context *context, int argc, sqlite3_value **argv) {
404 7 : const char *pk = (const char *)database_value_blob(argv[0]);
405 7 : int pk_len = database_value_bytes(argv[0]);
406 7 : int i = (int)database_value_int(argv[1]);
407 :
408 7 : cloudsync_pk_decode_context xdata = {.context = context, .index = i};
409 7 : pk_decode_prikey((char *)pk, (size_t)pk_len, dbsync_pk_decode_set_result_callback, &xdata);
410 7 : }
411 :
412 : // MARK: -
413 :
414 4857 : void dbsync_insert (sqlite3_context *context, int argc, sqlite3_value **argv) {
415 : DEBUG_FUNCTION("cloudsync_insert %s", database_value_text(argv[0]));
416 : // debug_values(argc-1, &argv[1]);
417 :
418 : // argv[0] is table name
419 : // argv[1]..[N] is primary key(s)
420 :
421 : // table_cloudsync
422 : // pk -> encode(argc-1, &argv[1])
423 : // col_name -> name
424 : // col_version -> 0/1 +1
425 : // db_version -> check
426 : // site_id 0
427 : // seq -> sqlite_master
428 :
429 : // retrieve context
430 4857 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
431 :
432 : // lookup table
433 4857 : const char *table_name = (const char *)database_value_text(argv[0]);
434 4857 : cloudsync_table_context *table = table_lookup(data, table_name);
435 4857 : if (!table) {
436 0 : dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_insert.", table_name);
437 0 : return;
438 : }
439 :
440 : // encode the primary key values into a buffer
441 : char buffer[1024];
442 4857 : size_t pklen = sizeof(buffer);
443 4857 : char *pk = pk_encode_prikey((dbvalue_t **)&argv[1], table_count_pks(table), buffer, &pklen);
444 4857 : if (!pk) {
445 0 : sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
446 0 : return;
447 : }
448 4857 : if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
449 1 : dbsync_set_error(context, "Insert aborted because primary key in table %s contains NULL values.", table_name);
450 1 : return;
451 : }
452 :
453 : // compute the next database version for tracking changes
454 4856 : int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
455 :
456 : // check if a row with the same primary key already exists
457 : // if so, this means the row might have been previously deleted (sentinel)
458 4856 : bool pk_exists = table_pk_exists(table, pk, pklen);
459 4856 : int rc = SQLITE_OK;
460 :
461 4856 : if (table_count_cols(table) == 0) {
462 : // if there are no columns other than primary keys, insert a sentinel record
463 97 : rc = local_mark_insert_sentinel_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
464 97 : if (rc != SQLITE_OK) goto cleanup;
465 4856 : } else if (pk_exists){
466 : // if a row with the same primary key already exists, update the sentinel record
467 4 : rc = local_update_sentinel(table, pk, pklen, db_version, cloudsync_bumpseq(data));
468 4 : if (rc != SQLITE_OK) goto cleanup;
469 4 : }
470 :
471 : // process each non-primary key column for insert or update
472 17200 : for (int i=0; i<table_count_cols(table); ++i) {
473 12344 : if (table_col_algo(table, i) == col_algo_block) {
474 : // Block column: read value from base table, split into blocks, store each block
475 44 : sqlite3_stmt *val_vm = (sqlite3_stmt *)table_column_lookup(table, table_colname(table, i), false, NULL);
476 44 : if (!val_vm) goto cleanup;
477 :
478 44 : rc = pk_decode_prikey(pk, pklen, pk_decode_bind_callback, (void *)val_vm);
479 44 : if (rc < 0) { databasevm_reset((dbvm_t *)val_vm); goto cleanup; }
480 :
481 44 : rc = databasevm_step((dbvm_t *)val_vm);
482 44 : if (rc == DBRES_ROW) {
483 44 : const char *text = database_column_text((dbvm_t *)val_vm, 0);
484 44 : const char *delim = table_col_delimiter(table, i);
485 44 : const char *col = table_colname(table, i);
486 :
487 44 : block_list_t *blocks = block_split(text ? text : "", delim);
488 44 : if (blocks) {
489 44 : char **positions = block_initial_positions(blocks->count);
490 44 : if (positions) {
491 360 : for (int b = 0; b < blocks->count; b++) {
492 316 : char *block_cn = block_build_colname(col, positions[b]);
493 316 : if (block_cn) {
494 316 : rc = local_mark_insert_or_update_meta(table, pk, pklen, block_cn, db_version, cloudsync_bumpseq(data));
495 :
496 : // Store block value in blocks table
497 316 : dbvm_t *wvm = table_block_value_write_stmt(table);
498 316 : if (wvm && rc == SQLITE_OK) {
499 316 : databasevm_bind_blob(wvm, 1, pk, (int)pklen);
500 316 : databasevm_bind_text(wvm, 2, block_cn, -1);
501 316 : databasevm_bind_text(wvm, 3, blocks->entries[b].content, -1);
502 316 : databasevm_step(wvm);
503 316 : databasevm_reset(wvm);
504 316 : }
505 :
506 316 : cloudsync_memory_free(block_cn);
507 316 : }
508 316 : cloudsync_memory_free(positions[b]);
509 316 : if (rc != SQLITE_OK) break;
510 316 : }
511 44 : cloudsync_memory_free(positions);
512 44 : }
513 44 : block_list_free(blocks);
514 44 : }
515 44 : }
516 44 : databasevm_reset((dbvm_t *)val_vm);
517 44 : if (rc == DBRES_ROW || rc == DBRES_DONE) rc = SQLITE_OK;
518 44 : if (rc != SQLITE_OK) goto cleanup;
519 44 : } else {
520 : // Regular column: mark as inserted or updated in the metadata
521 12300 : rc = local_mark_insert_or_update_meta(table, pk, pklen, table_colname(table, i), db_version, cloudsync_bumpseq(data));
522 12300 : if (rc != SQLITE_OK) goto cleanup;
523 : }
524 17200 : }
525 :
526 : cleanup:
527 4856 : if (rc != SQLITE_OK) sqlite3_result_error(context, database_errmsg(data), -1);
528 : // free memory if the primary key was dynamically allocated
529 4856 : if (pk != buffer) cloudsync_memory_free(pk);
530 4857 : }
531 :
532 36 : void dbsync_delete (sqlite3_context *context, int argc, sqlite3_value **argv) {
533 : DEBUG_FUNCTION("cloudsync_delete %s", database_value_text(argv[0]));
534 : // debug_values(argc-1, &argv[1]);
535 :
536 : // retrieve context
537 36 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
538 :
539 : // lookup table
540 36 : const char *table_name = (const char *)database_value_text(argv[0]);
541 36 : cloudsync_table_context *table = table_lookup(data, table_name);
542 36 : if (!table) {
543 0 : dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_delete.", table_name);
544 0 : return;
545 : }
546 :
547 : // compute the next database version for tracking changes
548 36 : int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
549 36 : int rc = SQLITE_OK;
550 :
551 : // encode the primary key values into a buffer
552 : char buffer[1024];
553 36 : size_t pklen = sizeof(buffer);
554 36 : char *pk = pk_encode_prikey((dbvalue_t **)&argv[1], table_count_pks(table), buffer, &pklen);
555 36 : if (!pk) {
556 0 : sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
557 0 : return;
558 : }
559 :
560 36 : if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
561 0 : dbsync_set_error(context, "Delete aborted because primary key in table %s contains NULL values.", table_name);
562 0 : return;
563 : }
564 :
565 : // mark the row as deleted by inserting a delete sentinel into the metadata
566 36 : rc = local_mark_delete_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
567 36 : if (rc != SQLITE_OK) goto cleanup;
568 :
569 : // remove any metadata related to the old rows associated with this primary key
570 36 : rc = local_drop_meta(table, pk, pklen);
571 36 : if (rc != SQLITE_OK) goto cleanup;
572 :
573 : cleanup:
574 36 : if (rc != SQLITE_OK) sqlite3_result_error(context, database_errmsg(data), -1);
575 : // free memory if the primary key was dynamically allocated
576 36 : if (pk != buffer) cloudsync_memory_free(pk);
577 36 : }
578 :
579 : // MARK: -
580 :
581 447 : void dbsync_update_payload_free (cloudsync_update_payload *payload) {
582 2664 : for (int i=0; i<payload->count; i++) {
583 2217 : database_value_free(payload->new_values[i]);
584 2217 : database_value_free(payload->old_values[i]);
585 2217 : }
586 447 : cloudsync_memory_free(payload->new_values);
587 447 : cloudsync_memory_free(payload->old_values);
588 447 : database_value_free(payload->table_name);
589 447 : payload->new_values = NULL;
590 447 : payload->old_values = NULL;
591 447 : payload->table_name = NULL;
592 447 : payload->count = 0;
593 447 : payload->capacity = 0;
594 447 : }
595 :
596 2217 : int dbsync_update_payload_append (cloudsync_update_payload *payload, sqlite3_value *v1, sqlite3_value *v2, sqlite3_value *v3) {
597 2217 : if (payload->count >= payload->capacity) {
598 450 : int newcap = payload->capacity ? payload->capacity * 2 : 128;
599 :
600 450 : sqlite3_value **new_values_2 = (sqlite3_value **)cloudsync_memory_realloc(payload->new_values, newcap * sizeof(*new_values_2));
601 450 : if (!new_values_2) return SQLITE_NOMEM;
602 :
603 450 : sqlite3_value **old_values_2 = (sqlite3_value **)cloudsync_memory_realloc(payload->old_values, newcap * sizeof(*old_values_2));
604 450 : if (!old_values_2) {
605 : // new_values_2 succeeded but old_values failed; keep new_values_2 pointer
606 : // (it's still valid, just larger) but don't update capacity
607 0 : payload->new_values = new_values_2;
608 0 : return SQLITE_NOMEM;
609 : }
610 :
611 450 : payload->new_values = new_values_2;
612 450 : payload->old_values = old_values_2;
613 450 : payload->capacity = newcap;
614 450 : }
615 :
616 2217 : int index = payload->count;
617 2217 : if (payload->table_name == NULL) payload->table_name = database_value_dup(v1);
618 1770 : else if (dbutils_value_compare(payload->table_name, v1) != 0) return SQLITE_NOMEM;
619 :
620 2217 : payload->new_values[index] = database_value_dup(v2);
621 2217 : payload->old_values[index] = database_value_dup(v3);
622 :
623 : // sanity check memory allocations before committing count
624 2217 : bool v1_can_be_null = (database_value_type(v1) == SQLITE_NULL);
625 2217 : bool v2_can_be_null = (database_value_type(v2) == SQLITE_NULL);
626 2217 : bool v3_can_be_null = (database_value_type(v3) == SQLITE_NULL);
627 :
628 2217 : bool oom = false;
629 2217 : if ((payload->table_name == NULL) && (!v1_can_be_null)) oom = true;
630 2217 : if ((payload->new_values[index] == NULL) && (!v2_can_be_null)) oom = true;
631 2217 : if ((payload->old_values[index] == NULL) && (!v3_can_be_null)) oom = true;
632 :
633 2217 : if (oom) {
634 : // clean up partial allocations at this index to prevent leaks
635 0 : if (payload->new_values[index]) { database_value_free(payload->new_values[index]); payload->new_values[index] = NULL; }
636 0 : if (payload->old_values[index]) { database_value_free(payload->old_values[index]); payload->old_values[index] = NULL; }
637 0 : return SQLITE_NOMEM;
638 : }
639 :
640 2217 : payload->count++;
641 2217 : return SQLITE_OK;
642 2217 : }
643 :
644 2217 : void dbsync_update_step (sqlite3_context *context, int argc, sqlite3_value **argv) {
645 : // argv[0] => table_name
646 : // argv[1] => new_column_value
647 : // argv[2] => old_column_value
648 :
649 : // allocate/get the update payload
650 2217 : cloudsync_update_payload *payload = (cloudsync_update_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_update_payload));
651 2217 : if (!payload) {sqlite3_result_error_nomem(context); return;}
652 :
653 2217 : if (dbsync_update_payload_append(payload, argv[0], argv[1], argv[2]) != SQLITE_OK) {
654 0 : sqlite3_result_error_nomem(context);
655 0 : }
656 2217 : }
657 :
658 447 : void dbsync_update_final (sqlite3_context *context) {
659 447 : cloudsync_update_payload *payload = (cloudsync_update_payload *)sqlite3_aggregate_context(context, sizeof(cloudsync_update_payload));
660 447 : if (!payload || payload->count == 0) return;
661 :
662 : // retrieve context
663 447 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
664 :
665 : // lookup table
666 447 : const char *table_name = (const char *)database_value_text(payload->table_name);
667 447 : cloudsync_table_context *table = table_lookup(data, table_name);
668 447 : if (!table) {
669 0 : dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_update.", table_name);
670 0 : dbsync_update_payload_free(payload);
671 0 : return;
672 : }
673 :
674 : // compute the next database version for tracking changes
675 447 : int64_t db_version = cloudsync_dbversion_next(data, CLOUDSYNC_VALUE_NOTSET);
676 447 : int rc = SQLITE_OK;
677 :
678 : // Check if the primary key(s) have changed
679 447 : bool prikey_changed = false;
680 1177 : for (int i=0; i<table_count_pks(table); ++i) {
681 759 : if (dbutils_value_compare(payload->old_values[i], payload->new_values[i]) != 0) {
682 29 : prikey_changed = true;
683 29 : break;
684 : }
685 730 : }
686 :
687 : // encode the NEW primary key values into a buffer (used later for indexing)
688 : char buffer[1024];
689 : char buffer2[1024];
690 447 : size_t pklen = sizeof(buffer);
691 447 : size_t oldpklen = sizeof(buffer2);
692 447 : char *oldpk = NULL;
693 :
694 447 : char *pk = pk_encode_prikey((dbvalue_t **)payload->new_values, table_count_pks(table), buffer, &pklen);
695 447 : if (!pk) {
696 0 : sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
697 0 : dbsync_update_payload_free(payload);
698 0 : return;
699 : }
700 447 : if (pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
701 0 : dbsync_set_error(context, "Update aborted because primary key in table %s contains NULL values.", table_name);
702 0 : dbsync_update_payload_free(payload);
703 0 : return;
704 : }
705 :
706 447 : if (prikey_changed) {
707 : // if the primary key has changed, we need to handle the row differently:
708 : // 1. mark the old row (OLD primary key) as deleted
709 : // 2. create a new row (NEW primary key)
710 :
711 : // encode the OLD primary key into a buffer
712 29 : oldpk = pk_encode_prikey((dbvalue_t **)payload->old_values, table_count_pks(table), buffer2, &oldpklen);
713 29 : if (!oldpk) {
714 : // no check here about PRIKEY_NULL_CONSTRAINT_ERROR because by design oldpk cannot contain NULL values
715 0 : if (pk != buffer) cloudsync_memory_free(pk);
716 0 : sqlite3_result_error(context, "Not enough memory to encode the primary key(s).", -1);
717 0 : dbsync_update_payload_free(payload);
718 0 : return;
719 : }
720 :
721 : // mark the rows with the old primary key as deleted in the metadata (old row handling)
722 29 : rc = local_mark_delete_meta(table, oldpk, oldpklen, db_version, cloudsync_bumpseq(data));
723 29 : if (rc != SQLITE_OK) goto cleanup;
724 :
725 : // move non-sentinel metadata entries from OLD primary key to NEW primary key
726 : // handles the case where some metadata is retained across primary key change
727 : // see https://github.com/sqliteai/sqlite-sync/blob/main/docs/PriKey.md for more details
728 29 : rc = local_update_move_meta(table, pk, pklen, oldpk, oldpklen, db_version);
729 29 : if (rc != SQLITE_OK) goto cleanup;
730 :
731 : // mark a new sentinel row with the new primary key in the metadata
732 29 : rc = local_mark_insert_sentinel_meta(table, pk, pklen, db_version, cloudsync_bumpseq(data));
733 29 : if (rc != SQLITE_OK) goto cleanup;
734 :
735 : // free memory if the OLD primary key was dynamically allocated
736 29 : if (oldpk != buffer2) cloudsync_memory_free(oldpk);
737 29 : oldpk = NULL;
738 29 : }
739 :
740 : // compare NEW and OLD values (excluding primary keys) to handle column updates
741 1877 : for (int i=0; i<table_count_cols(table); i++) {
742 1430 : int col_index = table_count_pks(table) + i; // Regular columns start after primary keys
743 :
744 1430 : if (dbutils_value_compare(payload->old_values[col_index], payload->new_values[col_index]) != 0) {
745 731 : if (table_col_algo(table, i) == col_algo_block) {
746 : // Block column: diff old and new text, emit per-block metadata changes
747 93 : const char *new_text = (const char *)database_value_text(payload->new_values[col_index]);
748 93 : const char *delim = table_col_delimiter(table, i);
749 93 : const char *col = table_colname(table, i);
750 :
751 : // Read existing blocks from blocks table
752 93 : block_list_t *old_blocks = block_list_create_empty();
753 93 : if (table_block_list_stmt(table)) {
754 93 : char *like_pattern = block_build_colname(col, "%");
755 93 : if (like_pattern) {
756 : // Query blocks table directly for existing block names and values
757 93 : char *list_sql = cloudsync_memory_mprintf(
758 : "SELECT col_name, col_value FROM %s WHERE pk = ?1 AND col_name LIKE ?2 ORDER BY col_name",
759 93 : table_blocks_ref(table));
760 93 : if (list_sql) {
761 93 : dbvm_t *list_vm = NULL;
762 93 : if (databasevm_prepare(data, list_sql, &list_vm, 0) == DBRES_OK) {
763 93 : databasevm_bind_blob(list_vm, 1, pk, (int)pklen);
764 93 : databasevm_bind_text(list_vm, 2, like_pattern, -1);
765 1447 : while (databasevm_step(list_vm) == DBRES_ROW) {
766 1354 : const char *bcn = database_column_text(list_vm, 0);
767 1354 : const char *bval = database_column_text(list_vm, 1);
768 1354 : const char *pos = block_extract_position_id(bcn);
769 1354 : if (pos && old_blocks) {
770 1354 : block_list_add(old_blocks, bval ? bval : "", pos);
771 1354 : }
772 : }
773 93 : databasevm_finalize(list_vm);
774 93 : }
775 93 : cloudsync_memory_free(list_sql);
776 93 : }
777 93 : cloudsync_memory_free(like_pattern);
778 93 : }
779 93 : }
780 :
781 : // Split new text into parts (NULL text = all blocks removed)
782 93 : block_list_t *new_blocks = new_text ? block_split(new_text, delim) : block_list_create_empty();
783 93 : if (new_blocks && old_blocks) {
784 : // Build array of new content strings (NULL when count is 0)
785 93 : const char **new_parts = NULL;
786 93 : if (new_blocks->count > 0) {
787 92 : new_parts = (const char **)cloudsync_memory_alloc(
788 92 : (uint64_t)(new_blocks->count * sizeof(char *)));
789 92 : if (new_parts) {
790 1500 : for (int b = 0; b < new_blocks->count; b++) {
791 1408 : new_parts[b] = new_blocks->entries[b].content;
792 1408 : }
793 92 : }
794 92 : }
795 :
796 93 : if (new_parts || new_blocks->count == 0) {
797 186 : block_diff_t *diff = block_diff(old_blocks->entries, old_blocks->count,
798 93 : new_parts, new_blocks->count);
799 93 : if (diff) {
800 229 : for (int d = 0; d < diff->count; d++) {
801 136 : block_diff_entry_t *de = &diff->entries[d];
802 136 : char *block_cn = block_build_colname(col, de->position_id);
803 136 : if (!block_cn) continue;
804 :
805 136 : if (de->type == BLOCK_DIFF_ADDED || de->type == BLOCK_DIFF_MODIFIED) {
806 190 : rc = local_mark_insert_or_update_meta(table, pk, pklen, block_cn,
807 95 : db_version, cloudsync_bumpseq(data));
808 : // Store block value
809 95 : if (rc == SQLITE_OK && table_block_value_write_stmt(table)) {
810 95 : dbvm_t *wvm = table_block_value_write_stmt(table);
811 95 : databasevm_bind_blob(wvm, 1, pk, (int)pklen);
812 95 : databasevm_bind_text(wvm, 2, block_cn, -1);
813 95 : databasevm_bind_text(wvm, 3, de->content, -1);
814 95 : databasevm_step(wvm);
815 95 : databasevm_reset(wvm);
816 95 : }
817 136 : } else if (de->type == BLOCK_DIFF_REMOVED) {
818 : // Mark block as deleted in metadata (even col_version)
819 82 : rc = local_mark_delete_block_meta(table, pk, pklen, block_cn,
820 41 : db_version, cloudsync_bumpseq(data));
821 : // Remove from blocks table
822 41 : if (rc == SQLITE_OK) {
823 41 : block_delete_value_external(data, table, pk, pklen, block_cn);
824 41 : }
825 41 : }
826 136 : cloudsync_memory_free(block_cn);
827 136 : if (rc != SQLITE_OK) break;
828 136 : }
829 93 : block_diff_free(diff);
830 93 : }
831 93 : if (new_parts) cloudsync_memory_free((void *)new_parts);
832 93 : }
833 93 : }
834 93 : if (new_blocks) block_list_free(new_blocks);
835 93 : if (old_blocks) block_list_free(old_blocks);
836 93 : if (rc != SQLITE_OK) goto cleanup;
837 93 : } else {
838 : // Regular column: mark as updated in the metadata (columns are in cid order)
839 638 : rc = local_mark_insert_or_update_meta(table, pk, pklen, table_colname(table, i), db_version, cloudsync_bumpseq(data));
840 638 : if (rc != SQLITE_OK) goto cleanup;
841 : }
842 731 : }
843 1877 : }
844 :
845 : cleanup:
846 447 : if (rc != SQLITE_OK) sqlite3_result_error(context, database_errmsg(data), -1);
847 447 : if (pk != buffer) cloudsync_memory_free(pk);
848 447 : if (oldpk && (oldpk != buffer2)) cloudsync_memory_free(oldpk);
849 :
850 447 : dbsync_update_payload_free(payload);
851 447 : }
852 :
853 : // MARK: -
854 :
855 4 : void dbsync_cleanup (sqlite3_context *context, int argc, sqlite3_value **argv) {
856 : DEBUG_FUNCTION("cloudsync_cleanup");
857 :
858 4 : const char *table = (const char *)database_value_text(argv[0]);
859 4 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
860 :
861 4 : int rc = cloudsync_cleanup(data, table);
862 4 : if (rc != DBRES_OK) {
863 0 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
864 0 : sqlite3_result_error_code(context, rc);
865 0 : }
866 4 : }
867 :
868 6 : void dbsync_enable_disable (sqlite3_context *context, const char *table_name, bool value) {
869 : DEBUG_FUNCTION("cloudsync_enable_disable");
870 :
871 6 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
872 6 : cloudsync_table_context *table = table_lookup(data, table_name);
873 6 : if (!table) return;
874 :
875 6 : table_set_enabled(table, value);
876 6 : }
877 :
878 3 : void dbsync_enable (sqlite3_context *context, int argc, sqlite3_value **argv) {
879 : DEBUG_FUNCTION("cloudsync_enable");
880 :
881 3 : const char *table = (const char *)database_value_text(argv[0]);
882 3 : dbsync_enable_disable(context, table, true);
883 3 : }
884 :
885 3 : void dbsync_disable (sqlite3_context *context, int argc, sqlite3_value **argv) {
886 : DEBUG_FUNCTION("cloudsync_disable");
887 :
888 3 : const char *table = (const char *)database_value_text(argv[0]);
889 3 : dbsync_enable_disable(context, table, false);
890 3 : }
891 :
892 2858 : void dbsync_is_enabled (sqlite3_context *context, int argc, sqlite3_value **argv) {
893 : DEBUG_FUNCTION("cloudsync_is_enabled");
894 :
895 2858 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
896 2858 : const char *table_name = (const char *)database_value_text(argv[0]);
897 2858 : cloudsync_table_context *table = table_lookup(data, table_name);
898 :
899 2858 : int result = (table && table_enabled(table)) ? 1 : 0;
900 2858 : sqlite3_result_int(context, result);
901 2858 : }
902 :
903 255 : void dbsync_terminate (sqlite3_context *context, int argc, sqlite3_value **argv) {
904 : DEBUG_FUNCTION("cloudsync_terminate");
905 :
906 255 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
907 255 : int rc = cloudsync_terminate(data);
908 255 : sqlite3_result_int(context, rc);
909 255 : }
910 :
911 : // MARK: -
912 :
913 284 : void dbsync_init (sqlite3_context *context, const char *table, const char *algo, CLOUDSYNC_INIT_FLAG init_flags) {
914 284 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
915 :
916 284 : int rc = database_begin_savepoint(data, "cloudsync_init");
917 284 : if (rc != SQLITE_OK) {
918 0 : dbsync_set_error(context, "Unable to create cloudsync_init savepoint. %s", database_errmsg(data));
919 0 : sqlite3_result_error_code(context, rc);
920 0 : return;
921 : }
922 :
923 284 : rc = cloudsync_init_table(data, table, algo, init_flags);
924 284 : if (rc == SQLITE_OK) {
925 279 : rc = database_commit_savepoint(data, "cloudsync_init");
926 279 : if (rc != SQLITE_OK) {
927 0 : dbsync_set_error(context, "Unable to release cloudsync_init savepoint. %s", database_errmsg(data));
928 0 : sqlite3_result_error_code(context, rc);
929 0 : }
930 279 : } else {
931 : // in case of error, rollback transaction
932 5 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
933 5 : sqlite3_result_error_code(context, rc);
934 5 : database_rollback_savepoint(data, "cloudsync_init");
935 5 : return;
936 : }
937 :
938 279 : cloudsync_update_schema_hash(data);
939 :
940 : // returns site_id as TEXT
941 : char buffer[UUID_STR_MAXLEN];
942 279 : cloudsync_uuid_v7_stringify(cloudsync_siteid(data), buffer, false);
943 279 : sqlite3_result_text(context, buffer, -1, SQLITE_TRANSIENT);
944 284 : }
945 :
946 46 : void dbsync_init3 (sqlite3_context *context, int argc, sqlite3_value **argv) {
947 : DEBUG_FUNCTION("cloudsync_init2");
948 :
949 46 : const char *table = (const char *)database_value_text(argv[0]);
950 46 : const char *algo = (const char *)database_value_text(argv[1]);
951 46 : int init_flags = database_value_int(argv[2]);
952 46 : dbsync_init(context, table, algo, init_flags);
953 46 : }
954 :
955 86 : void dbsync_init2 (sqlite3_context *context, int argc, sqlite3_value **argv) {
956 : DEBUG_FUNCTION("cloudsync_init2");
957 :
958 86 : const char *table = (const char *)database_value_text(argv[0]);
959 86 : const char *algo = (const char *)database_value_text(argv[1]);
960 86 : dbsync_init(context, table, algo, CLOUDSYNC_INIT_FLAG_NONE);
961 86 : }
962 :
963 152 : void dbsync_init1 (sqlite3_context *context, int argc, sqlite3_value **argv) {
964 : DEBUG_FUNCTION("cloudsync_init1");
965 :
966 152 : const char *table = (const char *)database_value_text(argv[0]);
967 152 : dbsync_init(context, table, NULL, CLOUDSYNC_INIT_FLAG_NONE);
968 152 : }
969 :
970 : // MARK: -
971 :
972 24 : void dbsync_begin_alter (sqlite3_context *context, int argc, sqlite3_value **argv) {
973 : DEBUG_FUNCTION("dbsync_begin_alter");
974 :
975 24 : const char *table_name = (const char *)database_value_text(argv[0]);
976 24 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
977 :
978 24 : int rc = database_begin_savepoint(data, "cloudsync_alter");
979 24 : if (rc != DBRES_OK) {
980 0 : sqlite3_result_error(context, "Unable to create cloudsync_alter savepoint", -1);
981 0 : sqlite3_result_error_code(context, rc);
982 0 : return;
983 : }
984 :
985 24 : rc = cloudsync_begin_alter(data, table_name);
986 24 : if (rc != DBRES_OK) {
987 1 : database_rollback_savepoint(data, "cloudsync_alter");
988 1 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
989 1 : sqlite3_result_error_code(context, rc);
990 1 : }
991 24 : }
992 :
993 24 : void dbsync_commit_alter (sqlite3_context *context, int argc, sqlite3_value **argv) {
994 : DEBUG_FUNCTION("cloudsync_commit_alter");
995 :
996 : //retrieve table argument
997 24 : const char *table_name = (const char *)database_value_text(argv[0]);
998 :
999 : // retrieve context
1000 24 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
1001 :
1002 24 : int rc = cloudsync_commit_alter(data, table_name);
1003 24 : if (rc != DBRES_OK) {
1004 1 : database_rollback_savepoint(data, "cloudsync_alter");
1005 1 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
1006 1 : sqlite3_result_error_code(context, rc);
1007 1 : return;
1008 : }
1009 :
1010 23 : rc = database_commit_savepoint(data, "cloudsync_alter");
1011 23 : if (rc != DBRES_OK) {
1012 0 : sqlite3_result_error(context, database_errmsg(data), -1);
1013 0 : sqlite3_result_error_code(context, rc);
1014 0 : return;
1015 : }
1016 :
1017 23 : cloudsync_update_schema_hash(data);
1018 24 : }
1019 :
1020 : // MARK: - Payload -
1021 :
1022 49700 : void dbsync_payload_encode_step (sqlite3_context *context, int argc, sqlite3_value **argv) {
1023 : // allocate/get the session context
1024 49700 : cloudsync_payload_context *payload = (cloudsync_payload_context *)sqlite3_aggregate_context(context, (int)cloudsync_payload_context_size(NULL));
1025 49700 : if (!payload) {
1026 0 : sqlite3_result_error(context, "Not enough memory to allocate payload session context", -1);
1027 0 : sqlite3_result_error_code(context, SQLITE_NOMEM);
1028 0 : return;
1029 : }
1030 :
1031 : // retrieve context
1032 49700 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
1033 :
1034 49700 : int rc = cloudsync_payload_encode_step(payload, data, argc, (dbvalue_t **)argv);
1035 49700 : if (rc != SQLITE_OK) {
1036 0 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
1037 0 : sqlite3_result_error_code(context, rc);
1038 0 : }
1039 49700 : }
1040 :
1041 696 : void dbsync_payload_encode_final (sqlite3_context *context) {
1042 : // get the session context
1043 696 : cloudsync_payload_context *payload = (cloudsync_payload_context *)sqlite3_aggregate_context(context, (int)cloudsync_payload_context_size(NULL));
1044 696 : if (!payload) {
1045 0 : sqlite3_result_error(context, "Unable to extract payload session context", -1);
1046 0 : sqlite3_result_error_code(context, SQLITE_NOMEM);
1047 0 : return;
1048 : }
1049 :
1050 : // retrieve context
1051 696 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
1052 :
1053 696 : int rc = cloudsync_payload_encode_final(payload, data);
1054 696 : if (rc != SQLITE_OK) {
1055 0 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
1056 0 : sqlite3_result_error_code(context, rc);
1057 0 : return;
1058 : }
1059 :
1060 : // result is OK so get BLOB and returns it
1061 696 : int64_t blob_size = 0;
1062 696 : char *blob = cloudsync_payload_blob (payload, &blob_size, NULL);
1063 696 : if (!blob) {
1064 8 : sqlite3_result_null(context);
1065 8 : } else {
1066 688 : sqlite3_result_blob64(context, blob, blob_size, SQLITE_TRANSIENT);
1067 688 : cloudsync_memory_free(blob);
1068 : }
1069 :
1070 : // from: https://sqlite.org/c3ref/aggregate_context.html
1071 : // SQLite automatically frees the memory allocated by sqlite3_aggregate_context() when the aggregate query concludes.
1072 696 : }
1073 :
1074 750 : void dbsync_payload_decode (sqlite3_context *context, int argc, sqlite3_value **argv) {
1075 : DEBUG_FUNCTION("dbsync_payload_decode");
1076 : //debug_values(argc, argv);
1077 :
1078 : // sanity check payload type
1079 750 : if (database_value_type(argv[0]) != SQLITE_BLOB) {
1080 0 : sqlite3_result_error(context, "Error on cloudsync_payload_decode: value must be a BLOB.", -1);
1081 0 : sqlite3_result_error_code(context, SQLITE_MISUSE);
1082 0 : return;
1083 : }
1084 :
1085 : // sanity check payload size
1086 750 : int blen = database_value_bytes(argv[0]);
1087 750 : size_t header_size = 0;
1088 750 : cloudsync_payload_context_size(&header_size);
1089 750 : if (blen < (int)header_size) {
1090 3 : sqlite3_result_error(context, "Error on cloudsync_payload_decode: invalid input size.", -1);
1091 3 : sqlite3_result_error_code(context, SQLITE_MISUSE);
1092 3 : return;
1093 : }
1094 :
1095 : // obtain payload
1096 747 : const char *payload = (const char *)database_value_blob(argv[0]);
1097 :
1098 : // apply changes
1099 : // The public SQL function applies a single complete payload: advance the
1100 : // receive cursor to its last applied (db_version, seq) (legacy behavior, safe
1101 : // for a payload that ends on a db_version boundary). The chunked-download
1102 : // receive path gates cursor advancement on stream completion via the C-level
1103 : // checkpoint argument instead (see cloudsync_payload_apply in cloudsync.h).
1104 747 : int nrows = 0;
1105 747 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
1106 747 : int rc = cloudsync_payload_apply(data, payload, blen, &nrows, CLOUDSYNC_CHECKPOINT_LAST_APPLIED, 0);
1107 747 : if (rc != SQLITE_OK) {
1108 5 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
1109 5 : sqlite3_result_error_code(context, rc);
1110 5 : return;
1111 : }
1112 :
1113 : // returns number of applied rows
1114 742 : sqlite3_result_int(context, nrows);
1115 750 : }
1116 :
1117 : typedef struct {
1118 : sqlite3_vtab base;
1119 : sqlite3 *db;
1120 : cloudsync_context *data;
1121 : } cloudsync_payload_chunks_vtab;
1122 :
1123 : typedef struct {
1124 : sqlite3_vtab_cursor base;
1125 : cloudsync_payload_chunks_vtab *vtab;
1126 : sqlite3_stmt *src;
1127 : bool eof;
1128 : bool has_row;
1129 : int chunk_index;
1130 : char *payload;
1131 : int64_t payload_size;
1132 : int64_t rows;
1133 : int64_t dbv_min;
1134 : int64_t dbv_max;
1135 : int64_t watermark;
1136 : bool frag_active;
1137 : int frag_part;
1138 : int frag_count;
1139 : int frag_target;
1140 : int64_t frag_offset;
1141 : int64_t frag_total;
1142 : uint64_t frag_checksum;
1143 : char value_header[16];
1144 : int value_header_len;
1145 : const char *value_data;
1146 : int64_t value_data_len;
1147 : // Positional-cursor outputs: the resume point AFTER the chunk currently held.
1148 : // These live in the per-scan reset region (after eof) so xFilter's bulk memset
1149 : // clears them. next_* is the (db_version, seq, frag_offset) a follow-up call
1150 : // passes back as resume_* to continue exactly where this chunk stopped.
1151 : int64_t next_dbv;
1152 : int64_t next_seq;
1153 : int64_t next_frag_offset;
1154 : bool is_final;
1155 : } cloudsync_payload_chunks_cursor;
1156 :
1157 9 : static int payload_chunks_connect(sqlite3 *db, void *aux, int argc, const char *const *argv, sqlite3_vtab **vtab, char **err) {
1158 9 : UNUSED_PARAMETER(argc); UNUSED_PARAMETER(argv); UNUSED_PARAMETER(err);
1159 9 : int rc = sqlite3_declare_vtab(db,
1160 : "CREATE TABLE x(payload BLOB, chunk_index INTEGER, payload_size INTEGER, rows INTEGER, "
1161 : "db_version_min INTEGER, db_version_max INTEGER, watermark_db_version INTEGER, "
1162 : "since_db_version HIDDEN, site_id HIDDEN, until_db_version HIDDEN, exclude_filter_site_id HIDDEN, "
1163 : // Positional-cursor outputs (cols 11..14): the resume point after the
1164 : // emitted chunk, plus a final-chunk flag. A stateless /check passes these
1165 : // back as the resume_* inputs (cols 15..17) to continue the drain without
1166 : // a spool table — O(1) seek per chunk instead of replaying from since.
1167 : "next_db_version INTEGER, next_seq INTEGER, next_frag_offset INTEGER, is_final INTEGER, "
1168 : "resume_db_version HIDDEN, resume_seq HIDDEN, resume_frag_offset HIDDEN)");
1169 9 : if (rc != SQLITE_OK) return rc;
1170 9 : cloudsync_payload_chunks_vtab *p = sqlite3_malloc64(sizeof(*p));
1171 9 : if (!p) return SQLITE_NOMEM;
1172 9 : memset(p, 0, sizeof(*p));
1173 9 : p->db = db;
1174 9 : p->data = (cloudsync_context *)aux;
1175 9 : *vtab = (sqlite3_vtab *)p;
1176 9 : return SQLITE_OK;
1177 9 : }
1178 :
1179 9 : static int payload_chunks_disconnect(sqlite3_vtab *vtab) {
1180 9 : sqlite3_free(vtab);
1181 9 : return SQLITE_OK;
1182 : }
1183 :
1184 22 : static int payload_chunks_open(sqlite3_vtab *vtab, sqlite3_vtab_cursor **cursor) {
1185 22 : cloudsync_payload_chunks_cursor *c = cloudsync_memory_zeroalloc(sizeof(*c));
1186 22 : if (!c) return SQLITE_NOMEM;
1187 22 : c->vtab = (cloudsync_payload_chunks_vtab *)vtab;
1188 22 : *cursor = (sqlite3_vtab_cursor *)c;
1189 22 : return SQLITE_OK;
1190 22 : }
1191 :
1192 22 : static int payload_chunks_close(sqlite3_vtab_cursor *cursor) {
1193 22 : cloudsync_payload_chunks_cursor *c = (cloudsync_payload_chunks_cursor *)cursor;
1194 22 : if (c->src) sqlite3_finalize(c->src);
1195 22 : if (c->payload) cloudsync_memory_free(c->payload);
1196 22 : cloudsync_memory_free(c);
1197 22 : return SQLITE_OK;
1198 : }
1199 :
1200 13 : static int payload_chunks_best_index(sqlite3_vtab *vtab, sqlite3_index_info *idxinfo) {
1201 13 : UNUSED_PARAMETER(vtab);
1202 : // Assign argvIndex in a canonical hidden-column order so xFilter can read argv
1203 : // in a fixed order regardless of how SQLite presents constraints. idxNum bit k
1204 : // is set when handled_cols[k] is bound; xFilter reads argv in this same order.
1205 : // bit0=since_db_version(7) bit1=site_id(8) bit2=until_db_version(9)
1206 : // bit3=exclude_filter_site_id(10) bit4=resume_db_version(15)
1207 : // bit5=resume_seq(16) bit6=resume_frag_offset(17)
1208 : static const int handled_cols[] = {7, 8, 9, 10, 15, 16, 17};
1209 13 : int argv_index = 1;
1210 13 : int idxnum = 0;
1211 104 : for (size_t k = 0; k < sizeof(handled_cols) / sizeof(handled_cols[0]); ++k) {
1212 91 : int col = handled_cols[k];
1213 207 : for (int i = 0; i < idxinfo->nConstraint; ++i) {
1214 137 : struct sqlite3_index_constraint *cn = &idxinfo->aConstraint[i];
1215 137 : if (!cn->usable || cn->op != SQLITE_INDEX_CONSTRAINT_EQ || cn->iColumn != col) continue;
1216 21 : idxinfo->aConstraintUsage[i].argvIndex = argv_index++;
1217 21 : idxinfo->aConstraintUsage[i].omit = 1;
1218 21 : idxnum |= (1 << k);
1219 21 : break; // at most one constraint consumed per hidden column
1220 : }
1221 91 : }
1222 13 : idxinfo->idxNum = idxnum;
1223 13 : idxinfo->estimatedCost = 10.0;
1224 13 : idxinfo->estimatedRows = 10;
1225 13 : return SQLITE_OK;
1226 : }
1227 :
1228 2061 : static int payload_chunks_step_source(cloudsync_payload_chunks_cursor *c) {
1229 2061 : int rc = sqlite3_step(c->src);
1230 2061 : if (rc == SQLITE_ROW) { c->has_row = true; return SQLITE_OK; }
1231 11 : c->has_row = false;
1232 11 : if (rc == SQLITE_DONE) return SQLITE_OK;
1233 : // copy the inner statement's message onto this vtab or SQLite surfaces the
1234 : // error as a bare "SQL logic error"
1235 0 : if (c->vtab->base.zErrMsg) sqlite3_free(c->vtab->base.zErrMsg);
1236 0 : c->vtab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(c->vtab->db));
1237 0 : return rc;
1238 2061 : }
1239 :
1240 14 : static int payload_chunks_plan_fragment(cloudsync_payload_chunks_cursor *c) {
1241 14 : cloudsync_context *data = c->vtab->data;
1242 28 : int target = cloudsync_payload_fragment_data_size(data,
1243 14 : (const char *)sqlite3_column_text(c->src, 0), sqlite3_column_bytes(c->src, 0),
1244 14 : sqlite3_column_blob(c->src, 1), sqlite3_column_bytes(c->src, 1),
1245 14 : (const char *)sqlite3_column_text(c->src, 2), sqlite3_column_bytes(c->src, 2),
1246 14 : sqlite3_column_int64(c->src, 4), sqlite3_column_int64(c->src, 5),
1247 14 : sqlite3_column_blob(c->src, 6), sqlite3_column_bytes(c->src, 6),
1248 14 : sqlite3_column_int64(c->src, 7), sqlite3_column_int64(c->src, 8),
1249 14 : c->frag_total, 0, 1);
1250 14 : if (target <= 0) return SQLITE_TOOBIG;
1251 :
1252 14 : int count = 0;
1253 14 : for (int i = 0; i < CLOUDSYNC_PAYLOAD_FRAGMENT_SIZE_FIXPOINT_ITERATIONS; ++i) {
1254 14 : count = cloudsync_payload_fragment_count(c->frag_total, target);
1255 14 : if (count <= 0) return SQLITE_TOOBIG;
1256 28 : int planned = cloudsync_payload_fragment_data_size(data,
1257 14 : (const char *)sqlite3_column_text(c->src, 0), sqlite3_column_bytes(c->src, 0),
1258 14 : sqlite3_column_blob(c->src, 1), sqlite3_column_bytes(c->src, 1),
1259 14 : (const char *)sqlite3_column_text(c->src, 2), sqlite3_column_bytes(c->src, 2),
1260 14 : sqlite3_column_int64(c->src, 4), sqlite3_column_int64(c->src, 5),
1261 14 : sqlite3_column_blob(c->src, 6), sqlite3_column_bytes(c->src, 6),
1262 14 : sqlite3_column_int64(c->src, 7), sqlite3_column_int64(c->src, 8),
1263 14 : c->frag_total, count - 1, count);
1264 14 : if (planned <= 0) return SQLITE_TOOBIG;
1265 14 : if (planned == target) break;
1266 0 : target = planned;
1267 0 : }
1268 :
1269 14 : c->frag_target = target;
1270 14 : c->frag_count = cloudsync_payload_fragment_count(c->frag_total, target);
1271 14 : if (c->frag_count <= 0) return SQLITE_TOOBIG;
1272 14 : return SQLITE_OK;
1273 14 : }
1274 :
1275 : // Set up fragment state for the current source row (a single value larger than
1276 : // max_chunk_size) so emit_fragment can stream it. start_offset is the byte offset
1277 : // within the encoded value to resume from (0 when first reaching the value;
1278 : // >0 when a positional cursor resumes mid-value). frag_part is derived from the
1279 : // offset so the fragment's part index is consistent whether reached by streaming
1280 : // or by a seek. The plan (frag_target/frag_count) is a deterministic function of
1281 : // the row, so a resumed fragment tiles identically to a streamed one.
1282 14 : static int payload_chunks_begin_fragment(cloudsync_payload_chunks_cursor *c, int64_t start_offset) {
1283 14 : dbvalue_t *col_value = (dbvalue_t *)sqlite3_column_value(c->src, 3);
1284 14 : int type = database_value_type(col_value);
1285 14 : if (type != DBTYPE_TEXT && type != DBTYPE_BLOB) return SQLITE_TOOBIG;
1286 14 : int64_t raw_len = 0;
1287 14 : int header_len = cloudsync_payload_encoded_value_header(col_value, c->value_header, sizeof(c->value_header), &raw_len);
1288 14 : if (header_len <= 0) return SQLITE_ERROR;
1289 14 : c->value_header_len = header_len;
1290 14 : c->value_data = (const char *)database_value_blob(col_value);
1291 14 : c->value_data_len = raw_len;
1292 14 : c->frag_total = header_len + raw_len;
1293 14 : c->frag_offset = start_offset;
1294 14 : int rc = payload_chunks_plan_fragment(c);
1295 14 : if (rc != SQLITE_OK) return rc;
1296 14 : c->frag_part = (c->frag_target > 0) ? (int)(start_offset / c->frag_target) : 0;
1297 14 : c->frag_checksum = cloudsync_payload_encoded_value_checksum(col_value);
1298 14 : c->frag_active = true;
1299 14 : return SQLITE_OK;
1300 14 : }
1301 :
1302 32 : static int payload_chunks_emit_fragment(cloudsync_payload_chunks_cursor *c) {
1303 32 : cloudsync_context *data = c->vtab->data;
1304 32 : if (c->payload) { cloudsync_memory_free(c->payload); c->payload = NULL; }
1305 32 : int64_t remaining = c->frag_total - c->frag_offset;
1306 32 : int frag_len = remaining > c->frag_target ? c->frag_target : (int)remaining;
1307 32 : if (frag_len <= 0) return SQLITE_CORRUPT;
1308 32 : char *frag = cloudsync_memory_alloc((uint64_t)frag_len);
1309 32 : if (!frag) return SQLITE_NOMEM;
1310 32 : int copied = 0;
1311 32 : int64_t off = c->frag_offset;
1312 32 : if (off < c->value_header_len) {
1313 8 : int n = c->value_header_len - (int)off;
1314 8 : if (n > frag_len) n = frag_len;
1315 8 : memcpy(frag, c->value_header + off, (size_t)n);
1316 8 : copied += n;
1317 8 : off += n;
1318 8 : }
1319 32 : if (copied < frag_len) {
1320 32 : int64_t data_off = off - c->value_header_len;
1321 32 : memcpy(frag + copied, c->value_data + data_off, (size_t)(frag_len - copied));
1322 32 : }
1323 :
1324 32 : cloudsync_payload_context *payload = cloudsync_memory_zeroalloc((uint64_t)cloudsync_payload_context_size(NULL));
1325 32 : if (!payload) { cloudsync_memory_free(frag); return SQLITE_NOMEM; }
1326 64 : int rc = cloudsync_payload_encode_fragment_step(payload, data,
1327 32 : (const char *)sqlite3_column_text(c->src, 0), sqlite3_column_bytes(c->src, 0),
1328 32 : sqlite3_column_blob(c->src, 1), sqlite3_column_bytes(c->src, 1),
1329 32 : (const char *)sqlite3_column_text(c->src, 2), sqlite3_column_bytes(c->src, 2),
1330 32 : frag, frag_len,
1331 32 : sqlite3_column_int64(c->src, 4), sqlite3_column_int64(c->src, 5),
1332 32 : sqlite3_column_blob(c->src, 6), sqlite3_column_bytes(c->src, 6),
1333 32 : sqlite3_column_int64(c->src, 7), sqlite3_column_int64(c->src, 8),
1334 32 : c->frag_checksum, c->frag_total, c->frag_part, c->frag_count);
1335 32 : cloudsync_memory_free(frag);
1336 32 : if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
1337 32 : rc = cloudsync_payload_encode_final(payload, data);
1338 32 : if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
1339 32 : c->payload = cloudsync_payload_blob(payload, &c->payload_size, &c->rows);
1340 32 : cloudsync_memory_free(payload);
1341 32 : c->dbv_min = sqlite3_column_int64(c->src, 5);
1342 32 : c->dbv_max = c->dbv_min;
1343 32 : c->chunk_index++;
1344 32 : c->frag_offset += frag_len;
1345 32 : c->frag_part++;
1346 32 : if (c->frag_part >= c->frag_count) {
1347 8 : c->frag_active = false;
1348 8 : rc = payload_chunks_step_source(c);
1349 8 : }
1350 32 : return rc;
1351 32 : }
1352 :
1353 62 : static int payload_chunks_build_next(cloudsync_payload_chunks_cursor *c) {
1354 62 : cloudsync_context *data = c->vtab->data;
1355 62 : int rc = SQLITE_OK;
1356 62 : if (c->payload) { cloudsync_memory_free(c->payload); c->payload = NULL; }
1357 62 : c->payload_size = c->rows = c->dbv_min = c->dbv_max = 0;
1358 62 : if (c->frag_active) return payload_chunks_emit_fragment(c);
1359 38 : if (!c->has_row) { c->eof = true; return SQLITE_OK; }
1360 :
1361 28 : int max_size = cloudsync_payload_max_chunk_size(data);
1362 28 : size_t payload_header_size = 0;
1363 28 : cloudsync_payload_context_size(&payload_header_size);
1364 28 : cloudsync_payload_context *payload = cloudsync_memory_zeroalloc((uint64_t)cloudsync_payload_context_size(NULL));
1365 28 : if (!payload) return SQLITE_NOMEM;
1366 2061 : while (c->has_row) {
1367 : sqlite3_value *rowv[9];
1368 20550 : for (int i = 0; i < 9; ++i) rowv[i] = sqlite3_column_value(c->src, i);
1369 2055 : size_t row_size = pk_encode_size((dbvalue_t **)rowv, 9, 0, -1);
1370 2055 : if (row_size == SIZE_MAX) { cloudsync_memory_free(payload); return SQLITE_NOMEM; }
1371 :
1372 2055 : if ((int64_t)row_size + (int64_t)payload_header_size + CLOUDSYNC_PAYLOAD_CHUNK_SAFETY_MARGIN > max_size) {
1373 14 : if (cloudsync_payload_context_nrows(payload) > 0) break;
1374 8 : cloudsync_memory_free(payload);
1375 8 : rc = payload_chunks_begin_fragment(c, 0);
1376 8 : if (rc != SQLITE_OK) return rc;
1377 8 : return payload_chunks_emit_fragment(c);
1378 : }
1379 :
1380 2041 : if (cloudsync_payload_context_nrows(payload) > 0 && cloudsync_payload_context_bused(payload) + row_size > (size_t)max_size) break;
1381 2033 : rc = cloudsync_payload_encode_step(payload, data, 9, (dbvalue_t **)rowv);
1382 2033 : if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
1383 2033 : int64_t dbv = sqlite3_column_int64(c->src, 5);
1384 2033 : if (cloudsync_payload_context_nrows(payload) == 1) c->dbv_min = dbv;
1385 2033 : c->dbv_max = dbv;
1386 2033 : rc = payload_chunks_step_source(c);
1387 2033 : if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
1388 : }
1389 :
1390 20 : if (cloudsync_payload_context_nrows(payload) == 0) { cloudsync_memory_free(payload); c->eof = true; return SQLITE_OK; }
1391 20 : rc = cloudsync_payload_encode_final(payload, data);
1392 20 : if (rc != SQLITE_OK) { cloudsync_memory_free(payload); return rc; }
1393 20 : c->payload = cloudsync_payload_blob(payload, &c->payload_size, &c->rows);
1394 20 : cloudsync_memory_free(payload);
1395 20 : c->chunk_index++;
1396 20 : return SQLITE_OK;
1397 62 : }
1398 :
1399 : // Record the resume point a stateless caller passes back to continue after the
1400 : // chunk just built. Reads the source statement, which is positioned at the next
1401 : // unconsumed row (or the same row when a value is still mid-fragment). Must be
1402 : // called only after build_next produced a chunk (i.e. !eof).
1403 52 : static void payload_chunks_set_next_cursor(cloudsync_payload_chunks_cursor *c) {
1404 52 : if (c->frag_active) {
1405 : // Mid-value: resume the same row at the next byte offset.
1406 24 : c->next_dbv = sqlite3_column_int64(c->src, 5);
1407 24 : c->next_seq = sqlite3_column_int64(c->src, 8);
1408 24 : c->next_frag_offset = c->frag_offset;
1409 24 : c->is_final = false;
1410 52 : } else if (c->has_row) {
1411 : // Row boundary: the next chunk starts at the current (unconsumed) row.
1412 18 : c->next_dbv = sqlite3_column_int64(c->src, 5);
1413 18 : c->next_seq = sqlite3_column_int64(c->src, 8);
1414 18 : c->next_frag_offset = 0;
1415 18 : c->is_final = false;
1416 18 : } else {
1417 : // Stream exhausted: this was the last chunk of the window.
1418 10 : c->next_dbv = c->watermark;
1419 10 : c->next_seq = 0;
1420 10 : c->next_frag_offset = 0;
1421 10 : c->is_final = true;
1422 : }
1423 52 : }
1424 :
1425 62 : static int payload_chunks_advance(cloudsync_payload_chunks_cursor *c) {
1426 62 : int rc = payload_chunks_build_next(c);
1427 62 : if (rc == SQLITE_OK && !c->eof) payload_chunks_set_next_cursor(c);
1428 62 : return rc;
1429 : }
1430 :
1431 22 : static int payload_chunks_filter(sqlite3_vtab_cursor *cursor, int idxnum, const char *idxstr, int argc, sqlite3_value **argv) {
1432 22 : UNUSED_PARAMETER(idxstr); UNUSED_PARAMETER(argc);
1433 22 : cloudsync_payload_chunks_cursor *c = (cloudsync_payload_chunks_cursor *)cursor;
1434 22 : cloudsync_context *data = c->vtab->data;
1435 22 : if (!cloudsync_context_is_initialized(data)) {
1436 1 : c->vtab->base.zErrMsg = sqlite3_mprintf(
1437 : "cloudsync is not initialized: call SELECT cloudsync_init('<table_name>') "
1438 : "to enable sync on a table before querying cloudsync_payload_chunks.");
1439 1 : return SQLITE_ERROR;
1440 : }
1441 21 : if (c->src) { sqlite3_finalize(c->src); c->src = NULL; }
1442 21 : if (c->payload) { cloudsync_memory_free(c->payload); c->payload = NULL; }
1443 : // Contract: all per-scan state that can be bulk-reset here must live at or
1444 : // after eof. Fields before eof are cursor lifetime state preserved across
1445 : // xFilter calls.
1446 21 : memset(&c->eof, 0, sizeof(*c) - offsetof(cloudsync_payload_chunks_cursor, eof));
1447 :
1448 21 : int argi = 0;
1449 21 : int64_t since = dbutils_settings_get_int64_value(data, CLOUDSYNC_KEY_SEND_DBVERSION);
1450 21 : const void *site_id = NULL;
1451 21 : int site_id_len = 0;
1452 21 : bool site_id_given = false;
1453 21 : int64_t until = 0;
1454 21 : bool exclude = false;
1455 : // Positional resume cursor (cols 15..17): when resume_db_version is bound the
1456 : // scan starts at (resume_db_version, resume_seq) inclusive and the first chunk
1457 : // resumes a mid-value fragment at resume_frag_offset, instead of replaying the
1458 : // whole window from `since`. Lets a stateless /check page the stream with an
1459 : // O(1) seek per call and no spool table.
1460 21 : bool positional = false;
1461 21 : int64_t resume_dbv = 0, resume_seq = 0, resume_frag = 0;
1462 21 : if (idxnum & 1) since = sqlite3_value_int64(argv[argi++]);
1463 21 : if (idxnum & 2) {
1464 2 : if (sqlite3_value_type(argv[argi]) != SQLITE_NULL) {
1465 2 : site_id = sqlite3_value_blob(argv[argi]);
1466 2 : site_id_len = sqlite3_value_bytes(argv[argi]);
1467 2 : site_id_given = true;
1468 2 : }
1469 2 : argi++;
1470 2 : }
1471 21 : if (idxnum & 4) until = sqlite3_value_int64(argv[argi++]);
1472 21 : if (idxnum & 8) exclude = (sqlite3_value_int(argv[argi++]) != 0);
1473 21 : if (idxnum & 16) { resume_dbv = sqlite3_value_int64(argv[argi++]); positional = true; }
1474 21 : if (idxnum & 32) resume_seq = sqlite3_value_int64(argv[argi++]);
1475 21 : if (idxnum & 64) resume_frag = sqlite3_value_int64(argv[argi++]);
1476 :
1477 : // Resolve the site filter:
1478 : // exclude=true -> all sites except filter_site_id (CHECK path); site required
1479 : // filter given -> only that site
1480 : // default -> local site (send path, unchanged)
1481 : const char *site_op;
1482 21 : if (exclude) {
1483 2 : if (!site_id_given) {
1484 1 : c->vtab->base.zErrMsg = sqlite3_mprintf(
1485 : "cloudsync_payload_chunks: exclude_filter_site_id requires a non-NULL site_id");
1486 1 : return SQLITE_ERROR;
1487 : }
1488 1 : site_op = "<>";
1489 1 : } else {
1490 19 : site_op = "=";
1491 19 : if (!site_id_given) { site_id = cloudsync_siteid(data); site_id_len = UUID_LEN; }
1492 : }
1493 :
1494 20 : if (until == 0) {
1495 10 : char *mxsql = sqlite3_mprintf(
1496 10 : "SELECT COALESCE(MAX(db_version),0) FROM cloudsync_changes WHERE site_id%s?", site_op);
1497 10 : if (!mxsql) return SQLITE_NOMEM;
1498 10 : sqlite3_stmt *mx = NULL;
1499 10 : int rc = sqlite3_prepare_v2(c->vtab->db, mxsql, -1, &mx, NULL);
1500 10 : sqlite3_free(mxsql);
1501 10 : if (rc != SQLITE_OK) return rc;
1502 10 : sqlite3_bind_blob(mx, 1, site_id, site_id_len, SQLITE_TRANSIENT);
1503 : // MAX() yields exactly one row, so anything else is an error: propagate
1504 : // it instead of silently scanning an empty window with until=0
1505 10 : rc = sqlite3_step(mx);
1506 10 : if (rc == SQLITE_ROW) until = sqlite3_column_int64(mx, 0);
1507 10 : sqlite3_finalize(mx);
1508 10 : if (rc != SQLITE_ROW) {
1509 0 : if (c->vtab->base.zErrMsg) sqlite3_free(c->vtab->base.zErrMsg);
1510 0 : c->vtab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(c->vtab->db));
1511 0 : return rc == SQLITE_DONE ? SQLITE_ERROR : rc;
1512 : }
1513 10 : }
1514 20 : c->watermark = until;
1515 :
1516 : // Window upper bound is always `until`. The lower bound is either the legacy
1517 : // exclusive `since` (db_version > since) or the inclusive positional cursor
1518 : // (db_version, seq) >= (resume_dbv, resume_seq).
1519 : char *sql;
1520 20 : if (positional) {
1521 10 : sql = sqlite3_mprintf(
1522 : "SELECT tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq "
1523 : "FROM cloudsync_changes WHERE db_version<=? AND site_id%s? AND "
1524 : "(db_version>? OR (db_version=? AND seq>=?)) ORDER BY db_version, seq ASC",
1525 10 : site_op);
1526 10 : } else {
1527 10 : sql = sqlite3_mprintf(
1528 : "SELECT tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq "
1529 : "FROM cloudsync_changes WHERE db_version>? AND site_id%s? AND db_version<=? ORDER BY db_version, seq ASC",
1530 10 : site_op);
1531 : }
1532 20 : if (!sql) return SQLITE_NOMEM;
1533 20 : int rc = sqlite3_prepare_v2(c->vtab->db, sql, -1, &c->src, NULL);
1534 20 : sqlite3_free(sql);
1535 20 : if (rc != SQLITE_OK) return rc;
1536 20 : if (positional) {
1537 10 : sqlite3_bind_int64(c->src, 1, until);
1538 10 : sqlite3_bind_blob(c->src, 2, site_id, site_id_len, SQLITE_TRANSIENT);
1539 10 : sqlite3_bind_int64(c->src, 3, resume_dbv);
1540 10 : sqlite3_bind_int64(c->src, 4, resume_dbv);
1541 10 : sqlite3_bind_int64(c->src, 5, resume_seq);
1542 10 : } else {
1543 10 : sqlite3_bind_int64(c->src, 1, since);
1544 10 : sqlite3_bind_blob(c->src, 2, site_id, site_id_len, SQLITE_TRANSIENT);
1545 10 : sqlite3_bind_int64(c->src, 3, until);
1546 : }
1547 20 : rc = payload_chunks_step_source(c);
1548 20 : if (rc != SQLITE_OK) return rc;
1549 : // Resuming inside a value that was fragmented across chunks: the first row is
1550 : // that value; re-establish the fragment plan and skip to resume_frag.
1551 20 : if (positional && resume_frag > 0 && c->has_row) {
1552 6 : rc = payload_chunks_begin_fragment(c, resume_frag);
1553 6 : if (rc != SQLITE_OK) return rc;
1554 6 : }
1555 20 : return payload_chunks_advance(c);
1556 22 : }
1557 :
1558 42 : static int payload_chunks_next(sqlite3_vtab_cursor *cursor) {
1559 42 : return payload_chunks_advance((cloudsync_payload_chunks_cursor *)cursor);
1560 : }
1561 :
1562 62 : static int payload_chunks_eof(sqlite3_vtab_cursor *cursor) {
1563 62 : return ((cloudsync_payload_chunks_cursor *)cursor)->eof;
1564 : }
1565 :
1566 187 : static int payload_chunks_column(sqlite3_vtab_cursor *cursor, sqlite3_context *ctx, int col) {
1567 187 : cloudsync_payload_chunks_cursor *c = (cloudsync_payload_chunks_cursor *)cursor;
1568 187 : switch (col) {
1569 52 : case 0: sqlite3_result_blob64(ctx, c->payload, (sqlite3_uint64)c->payload_size, SQLITE_TRANSIENT); break;
1570 42 : case 1: sqlite3_result_int(ctx, c->chunk_index - 1); break;
1571 17 : case 2: sqlite3_result_int64(ctx, c->payload_size); break;
1572 17 : case 3: sqlite3_result_int64(ctx, c->rows); break;
1573 3 : case 4: sqlite3_result_int64(ctx, c->dbv_min); break;
1574 3 : case 5: sqlite3_result_int64(ctx, c->dbv_max); break;
1575 13 : case 6: sqlite3_result_int64(ctx, c->watermark); break;
1576 10 : case 11: sqlite3_result_int64(ctx, c->next_dbv); break;
1577 10 : case 12: sqlite3_result_int64(ctx, c->next_seq); break;
1578 10 : case 13: sqlite3_result_int64(ctx, c->next_frag_offset); break;
1579 10 : case 14: sqlite3_result_int(ctx, c->is_final ? 1 : 0); break;
1580 0 : default: sqlite3_result_null(ctx); break;
1581 : }
1582 187 : return SQLITE_OK;
1583 : }
1584 :
1585 0 : static int payload_chunks_rowid(sqlite3_vtab_cursor *cursor, sqlite3_int64 *rowid) {
1586 0 : *rowid = ((cloudsync_payload_chunks_cursor *)cursor)->chunk_index;
1587 0 : return SQLITE_OK;
1588 : }
1589 :
1590 : static sqlite3_module cloudsync_payload_chunks_module = {
1591 : /* iVersion */ 0,
1592 : /* xCreate */ NULL,
1593 : /* xConnect */ payload_chunks_connect,
1594 : /* xBestIndex */ payload_chunks_best_index,
1595 : /* xDisconnect */ payload_chunks_disconnect,
1596 : /* xDestroy */ NULL,
1597 : /* xOpen */ payload_chunks_open,
1598 : /* xClose */ payload_chunks_close,
1599 : /* xFilter */ payload_chunks_filter,
1600 : /* xNext */ payload_chunks_next,
1601 : /* xEof */ payload_chunks_eof,
1602 : /* xColumn */ payload_chunks_column,
1603 : /* xRowid */ payload_chunks_rowid,
1604 : /* xUpdate */ NULL,
1605 : /* xBegin */ NULL,
1606 : /* xSync */ NULL,
1607 : /* xCommit */ NULL,
1608 : /* xRollback */ NULL,
1609 : /* xFindMethod */ NULL,
1610 : /* xRename */ NULL,
1611 : /* xSavepoint */ NULL,
1612 : /* xRelease */ NULL,
1613 : /* xRollbackTo */ NULL,
1614 : /* xShadowName */ NULL,
1615 : /* xIntegrity */ NULL
1616 : };
1617 :
1618 1060 : static int payload_estimated_size_add(sqlite3_int64 *acc, sqlite3_int64 value) {
1619 1060 : if (value < 0 || *acc > INT64_MAX - value) return SQLITE_TOOBIG;
1620 1060 : *acc += value;
1621 1060 : return SQLITE_OK;
1622 1060 : }
1623 :
1624 6 : static int payload_blob_checked_estimate(sqlite3 *db, const void *site_id, int site_id_len,
1625 : sqlite3_int64 since, sqlite3_int64 since_seq,
1626 : bool exclude, sqlite3_int64 *estimated,
1627 : sqlite3_int64 *watermark) {
1628 6 : sqlite3_stmt *stmt = NULL;
1629 6 : sqlite3_stmt *mx = NULL;
1630 6 : int rc = SQLITE_OK;
1631 6 : sqlite3_int64 until = 0;
1632 6 : size_t header_size = 0;
1633 6 : bool has_rows = false;
1634 6 : const char *site_op = exclude ? "<>" : "=";
1635 6 : *estimated = 0;
1636 :
1637 6 : char *mxsql = sqlite3_mprintf(
1638 6 : "SELECT COALESCE(MAX(db_version),0) FROM cloudsync_changes WHERE site_id%s?", site_op);
1639 6 : if (!mxsql) return SQLITE_NOMEM;
1640 6 : rc = sqlite3_prepare_v2(db, mxsql, -1, &mx, NULL);
1641 6 : sqlite3_free(mxsql);
1642 6 : if (rc != SQLITE_OK) goto error;
1643 6 : sqlite3_bind_blob(mx, 1, site_id, site_id_len, SQLITE_TRANSIENT);
1644 6 : rc = sqlite3_step(mx);
1645 6 : if (rc == SQLITE_ROW) until = sqlite3_column_int64(mx, 0);
1646 0 : else if (rc != SQLITE_DONE) goto error;
1647 6 : sqlite3_finalize(mx);
1648 6 : mx = NULL;
1649 6 : if (watermark) *watermark = until;
1650 :
1651 6 : if (until < since) return SQLITE_OK;
1652 :
1653 4 : char *sql = sqlite3_mprintf(
1654 : "SELECT tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq "
1655 : "FROM cloudsync_changes WHERE (db_version>? OR (db_version=? AND seq>?)) "
1656 : "AND site_id%s? AND db_version<=? ORDER BY db_version, seq ASC",
1657 4 : site_op);
1658 4 : if (!sql) return SQLITE_NOMEM;
1659 4 : rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
1660 4 : sqlite3_free(sql);
1661 4 : if (rc != SQLITE_OK) goto error;
1662 4 : sqlite3_bind_int64(stmt, 1, since);
1663 4 : sqlite3_bind_int64(stmt, 2, since);
1664 4 : sqlite3_bind_int64(stmt, 3, since_seq);
1665 4 : sqlite3_bind_blob(stmt, 4, site_id, site_id_len, SQLITE_TRANSIENT);
1666 4 : sqlite3_bind_int64(stmt, 5, until);
1667 :
1668 4 : cloudsync_payload_context_size(&header_size);
1669 1060 : while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
1670 : sqlite3_value *rowv[9];
1671 10560 : for (int i = 0; i < 9; ++i) rowv[i] = sqlite3_column_value(stmt, i);
1672 1056 : size_t row_size = pk_encode_size((dbvalue_t **)rowv, 9, 0, -1);
1673 1056 : if (row_size == SIZE_MAX || row_size > (size_t)INT64_MAX) {
1674 0 : rc = SQLITE_TOOBIG;
1675 0 : goto error;
1676 : }
1677 1056 : if (!has_rows) {
1678 4 : if (header_size > (size_t)INT64_MAX) {
1679 0 : rc = SQLITE_TOOBIG;
1680 0 : goto error;
1681 : }
1682 4 : rc = payload_estimated_size_add(estimated, (sqlite3_int64)header_size);
1683 4 : if (rc != SQLITE_OK) goto error;
1684 4 : has_rows = true;
1685 4 : }
1686 1056 : rc = payload_estimated_size_add(estimated, (sqlite3_int64)row_size);
1687 1056 : if (rc != SQLITE_OK) goto error;
1688 : }
1689 4 : if (rc != SQLITE_DONE) goto error;
1690 4 : sqlite3_finalize(stmt);
1691 4 : return SQLITE_OK;
1692 :
1693 : error:
1694 0 : if (stmt) sqlite3_finalize(stmt);
1695 0 : if (mx) sqlite3_finalize(mx);
1696 0 : return rc;
1697 6 : }
1698 :
1699 7 : void dbsync_payload_blob_checked(sqlite3_context *context, int argc, sqlite3_value **argv) {
1700 : DEBUG_FUNCTION("cloudsync_payload_blob_checked");
1701 7 : UNUSED_PARAMETER(argc);
1702 :
1703 7 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
1704 7 : sqlite3 *db = sqlite3_context_db_handle(context);
1705 7 : sqlite3_stmt *stmt = NULL;
1706 7 : cloudsync_payload_context *payload = NULL;
1707 7 : int rc = SQLITE_OK;
1708 7 : sqlite3_int64 since = 0;
1709 7 : sqlite3_int64 since_seq = 0;
1710 7 : sqlite3_int64 max_estimated_size = 0;
1711 7 : sqlite3_int64 estimated = 0;
1712 7 : sqlite3_int64 watermark = 0;
1713 7 : const void *site_id = NULL;
1714 7 : int site_id_len = 0;
1715 7 : bool exclude = false;
1716 :
1717 7 : if (sqlite3_value_type(argv[0]) != SQLITE_NULL) since = sqlite3_value_int64(argv[0]);
1718 7 : if (sqlite3_value_type(argv[1]) != SQLITE_NULL) since_seq = sqlite3_value_int64(argv[1]);
1719 7 : if (sqlite3_value_type(argv[2]) != SQLITE_NULL) {
1720 3 : site_id = sqlite3_value_blob(argv[2]);
1721 3 : site_id_len = sqlite3_value_bytes(argv[2]);
1722 3 : }
1723 7 : exclude = sqlite3_value_type(argv[3]) != SQLITE_NULL && sqlite3_value_int(argv[3]) != 0;
1724 7 : if (sqlite3_value_type(argv[4]) == SQLITE_NULL || sqlite3_value_int64(argv[4]) <= 0) {
1725 0 : sqlite3_result_error(context, "cloudsync_payload_blob_checked: max_estimated_payload_size must be positive", -1);
1726 0 : return;
1727 : }
1728 7 : max_estimated_size = sqlite3_value_int64(argv[4]);
1729 :
1730 7 : if (exclude && !site_id) {
1731 1 : sqlite3_result_error(context,
1732 : "cloudsync_payload_blob_checked: exclude_filter_site_id requires a non-NULL site_id", -1);
1733 1 : return;
1734 : }
1735 6 : if (!exclude && !site_id) {
1736 3 : site_id = cloudsync_siteid(data);
1737 3 : site_id_len = UUID_LEN;
1738 3 : }
1739 :
1740 6 : rc = payload_blob_checked_estimate(db, site_id, site_id_len, since, since_seq, exclude, &estimated, &watermark);
1741 6 : if (rc != SQLITE_OK) goto error;
1742 6 : if (estimated == 0) {
1743 2 : sqlite3_result_null(context);
1744 2 : return;
1745 : }
1746 4 : if (estimated > max_estimated_size) {
1747 2 : dbsync_set_error(context,
1748 : CLOUDSYNC_ERRCODE_PAYLOAD_TOO_LARGE "cloudsync_payload_blob_checked: estimated payload size %" PRId64 " exceeds max_estimated_payload_size %" PRId64,
1749 1 : (int64_t)estimated, (int64_t)max_estimated_size);
1750 1 : return;
1751 : }
1752 :
1753 3 : const char *site_op = exclude ? "<>" : "=";
1754 3 : char *sql = sqlite3_mprintf(
1755 : "SELECT tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq "
1756 : "FROM cloudsync_changes WHERE (db_version>? OR (db_version=? AND seq>?)) "
1757 : "AND site_id%s? AND db_version<=? ORDER BY db_version, seq ASC",
1758 3 : site_op);
1759 3 : if (!sql) { rc = SQLITE_NOMEM; goto error; }
1760 3 : rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
1761 3 : sqlite3_free(sql);
1762 3 : if (rc != SQLITE_OK) goto error;
1763 3 : sqlite3_bind_int64(stmt, 1, since);
1764 3 : sqlite3_bind_int64(stmt, 2, since);
1765 3 : sqlite3_bind_int64(stmt, 3, since_seq);
1766 3 : sqlite3_bind_blob(stmt, 4, site_id, site_id_len, SQLITE_TRANSIENT);
1767 3 : sqlite3_bind_int64(stmt, 5, watermark);
1768 :
1769 3 : payload = cloudsync_memory_zeroalloc((uint64_t)cloudsync_payload_context_size(NULL));
1770 3 : if (!payload) { rc = SQLITE_NOMEM; goto error; }
1771 :
1772 533 : while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
1773 : sqlite3_value *rowv[9];
1774 5300 : for (int i = 0; i < 9; ++i) rowv[i] = sqlite3_column_value(stmt, i);
1775 530 : rc = cloudsync_payload_encode_step(payload, data, 9, (dbvalue_t **)rowv);
1776 530 : if (rc != SQLITE_OK) goto error;
1777 : }
1778 3 : if (rc != SQLITE_DONE) goto error;
1779 3 : sqlite3_finalize(stmt);
1780 3 : stmt = NULL;
1781 :
1782 3 : rc = cloudsync_payload_encode_final(payload, data);
1783 3 : if (rc != SQLITE_OK) goto error;
1784 3 : int64_t blob_size = 0;
1785 3 : char *blob = cloudsync_payload_blob(payload, &blob_size, NULL);
1786 3 : if (!blob) {
1787 0 : sqlite3_result_null(context);
1788 0 : cloudsync_payload_context_free(payload);
1789 0 : } else {
1790 3 : sqlite3_result_blob64(context, blob, (sqlite3_uint64)blob_size, cloudsync_memory_free);
1791 3 : cloudsync_memory_free(payload);
1792 : }
1793 3 : return;
1794 :
1795 : error:
1796 0 : if (stmt) sqlite3_finalize(stmt);
1797 0 : if (payload) cloudsync_payload_context_free(payload);
1798 0 : if (rc == SQLITE_NOMEM) sqlite3_result_error_nomem(context);
1799 0 : else if (rc == SQLITE_TOOBIG) sqlite3_result_error(context, CLOUDSYNC_ERRCODE_PAYLOAD_TOO_LARGE "cloudsync_payload_blob_checked: payload estimate is too large", -1);
1800 0 : else sqlite3_result_error(context, sqlite3_errmsg(db), -1);
1801 7 : }
1802 :
1803 : #ifdef CLOUDSYNC_DESKTOP_OS
1804 0 : void dbsync_payload_save (sqlite3_context *context, int argc, sqlite3_value **argv) {
1805 : DEBUG_FUNCTION("dbsync_payload_save");
1806 :
1807 : // sanity check argument
1808 0 : if (database_value_type(argv[0]) != SQLITE_TEXT) {
1809 0 : sqlite3_result_error(context, "Unable to retrieve file path.", -1);
1810 0 : return;
1811 : }
1812 :
1813 : // retrieve full path to file
1814 0 : const char *payload_path = (const char *)database_value_text(argv[0]);
1815 :
1816 : // retrieve global context
1817 0 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
1818 :
1819 0 : int blob_size = 0;
1820 0 : int rc = cloudsync_payload_save(data, payload_path, &blob_size);
1821 0 : if (rc == SQLITE_OK) {
1822 : // if OK then returns blob size
1823 0 : sqlite3_result_int64(context, (sqlite3_int64)blob_size);
1824 0 : return;
1825 : }
1826 :
1827 0 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
1828 0 : sqlite3_result_error_code(context, rc);
1829 0 : }
1830 :
1831 0 : void dbsync_payload_load (sqlite3_context *context, int argc, sqlite3_value **argv) {
1832 : DEBUG_FUNCTION("dbsync_payload_load");
1833 :
1834 : // sanity check argument
1835 0 : if (database_value_type(argv[0]) != SQLITE_TEXT) {
1836 0 : sqlite3_result_error(context, "Unable to retrieve file path.", -1);
1837 0 : return;
1838 : }
1839 :
1840 : // retrieve full path to file
1841 0 : const char *path = (const char *)database_value_text(argv[0]);
1842 :
1843 0 : int64_t payload_size = 0;
1844 0 : char *payload = cloudsync_file_read(path, &payload_size);
1845 0 : if (!payload) {
1846 0 : if (payload_size < 0) {
1847 0 : sqlite3_result_error(context, "Unable to read payload from file path.", -1);
1848 0 : sqlite3_result_error_code(context, SQLITE_IOERR);
1849 0 : return;
1850 : }
1851 : // no rows affected but no error either
1852 0 : sqlite3_result_int(context, 0);
1853 0 : return;
1854 : }
1855 :
1856 0 : int nrows = 0;
1857 0 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
1858 : // File-based load applies a complete monolithic payload: legacy last-applied
1859 : // checkpoint (ends on a db_version boundary, so it is safe).
1860 0 : int rc = cloudsync_payload_apply (data, payload, (int)payload_size, &nrows, CLOUDSYNC_CHECKPOINT_LAST_APPLIED, 0);
1861 0 : if (payload) cloudsync_memory_free(payload);
1862 :
1863 0 : if (rc != SQLITE_OK) {
1864 0 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
1865 0 : sqlite3_result_error_code(context, rc);
1866 0 : return;
1867 : }
1868 :
1869 : // returns number of applied rows
1870 0 : sqlite3_result_int(context, nrows);
1871 0 : }
1872 : #endif
1873 :
1874 : // MARK: - Register -
1875 :
1876 10626 : int dbsync_register_with_flags (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), void (*xstep)(sqlite3_context*,int,sqlite3_value**), void (*xfinal)(sqlite3_context*), int nargs, int flags, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
1877 :
1878 10626 : int rc = sqlite3_create_function_v2(db, name, nargs, flags, ctx, xfunc, xstep, xfinal, ctx_free);
1879 :
1880 10626 : if (rc != SQLITE_OK) {
1881 0 : if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Error creating function %s: %s", name, sqlite3_errmsg(db));
1882 0 : return rc;
1883 : }
1884 10626 : return SQLITE_OK;
1885 10626 : }
1886 :
1887 8855 : int dbsync_register (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), void (*xstep)(sqlite3_context*,int,sqlite3_value**), void (*xfinal)(sqlite3_context*), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
1888 8855 : const int FLAGS_VOLATILE = SQLITE_UTF8;
1889 : DEBUG_DBFUNCTION("dbsync_register %s", name);
1890 8855 : return dbsync_register_with_flags(db, name, xfunc, xstep, xfinal, nargs, FLAGS_VOLATILE, pzErrMsg, ctx, ctx_free);
1891 : }
1892 :
1893 8602 : int dbsync_register_function (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
1894 : DEBUG_DBFUNCTION("dbsync_register_function %s", name);
1895 8602 : return dbsync_register(db, name, xfunc, NULL, NULL, nargs, pzErrMsg, ctx, ctx_free);
1896 : }
1897 :
1898 759 : int dbsync_register_pure_function (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
1899 759 : const int FLAGS_PURE = SQLITE_UTF8 | SQLITE_INNOCUOUS | SQLITE_DETERMINISTIC;
1900 : DEBUG_DBFUNCTION("dbsync_register_pure_function %s", name);
1901 759 : return dbsync_register_with_flags(db, name, xfunc, NULL, NULL, nargs, FLAGS_PURE, pzErrMsg, ctx, ctx_free);
1902 : }
1903 :
1904 759 : int dbsync_register_trigger_function (sqlite3 *db, const char *name, void (*xfunc)(sqlite3_context*,int,sqlite3_value**), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
1905 759 : const int FLAGS_TRIGGER = SQLITE_UTF8 | SQLITE_INNOCUOUS;
1906 : DEBUG_DBFUNCTION("dbsync_register_trigger_function %s", name);
1907 759 : return dbsync_register_with_flags(db, name, xfunc, NULL, NULL, nargs, FLAGS_TRIGGER, pzErrMsg, ctx, ctx_free);
1908 : }
1909 :
1910 253 : int dbsync_register_aggregate (sqlite3 *db, const char *name, void (*xstep)(sqlite3_context*,int,sqlite3_value**), void (*xfinal)(sqlite3_context*), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
1911 : DEBUG_DBFUNCTION("dbsync_register_aggregate %s", name);
1912 253 : return dbsync_register(db, name, NULL, xstep, xfinal, nargs, pzErrMsg, ctx, ctx_free);
1913 : }
1914 :
1915 253 : int dbsync_register_trigger_aggregate (sqlite3 *db, const char *name, void (*xstep)(sqlite3_context*,int,sqlite3_value**), void (*xfinal)(sqlite3_context*), int nargs, char **pzErrMsg, void *ctx, void (*ctx_free)(void *)) {
1916 253 : const int FLAGS_TRIGGER = SQLITE_UTF8 | SQLITE_INNOCUOUS;
1917 : DEBUG_DBFUNCTION("dbsync_register_trigger_aggregate %s", name);
1918 253 : return dbsync_register_with_flags(db, name, NULL, xstep, xfinal, nargs, FLAGS_TRIGGER, pzErrMsg, ctx, ctx_free);
1919 : }
1920 :
1921 : // MARK: - Block-level LWW -
1922 :
1923 50 : void dbsync_text_materialize (sqlite3_context *context, int argc, sqlite3_value **argv) {
1924 : DEBUG_FUNCTION("cloudsync_text_materialize");
1925 :
1926 : // argv[0] -> table name
1927 : // argv[1] -> column name
1928 : // argv[2..N] -> primary key values
1929 :
1930 50 : if (argc < 3) {
1931 0 : sqlite3_result_error(context, "cloudsync_text_materialize requires at least 3 arguments: table, column, pk...", -1);
1932 0 : return;
1933 : }
1934 :
1935 50 : const char *table_name = (const char *)database_value_text(argv[0]);
1936 50 : const char *col_name = (const char *)database_value_text(argv[1]);
1937 50 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
1938 :
1939 50 : cloudsync_table_context *table = table_lookup(data, table_name);
1940 50 : if (!table) {
1941 0 : dbsync_set_error(context, "Unable to retrieve table name %s in cloudsync_text_materialize.", table_name);
1942 0 : return;
1943 : }
1944 :
1945 50 : int col_idx = table_col_index(table, col_name);
1946 50 : if (col_idx < 0 || table_col_algo(table, col_idx) != col_algo_block) {
1947 0 : dbsync_set_error(context, "Column %s in table %s is not configured as block-level.", col_name, table_name);
1948 0 : return;
1949 : }
1950 :
1951 : // Encode primary keys
1952 50 : int npks = table_count_pks(table);
1953 50 : if (argc - 2 != npks) {
1954 0 : sqlite3_result_error(context, "Wrong number of primary key values for cloudsync_text_materialize.", -1);
1955 0 : return;
1956 : }
1957 :
1958 : char buffer[1024];
1959 50 : size_t pklen = sizeof(buffer);
1960 50 : char *pk = pk_encode_prikey((dbvalue_t **)&argv[2], npks, buffer, &pklen);
1961 50 : if (!pk || pk == PRIKEY_NULL_CONSTRAINT_ERROR) {
1962 0 : sqlite3_result_error(context, "Failed to encode primary key(s).", -1);
1963 0 : return;
1964 : }
1965 :
1966 : // Materialize the column
1967 50 : int rc = block_materialize_column(data, table, pk, (int)pklen, col_name);
1968 50 : if (rc != DBRES_OK) {
1969 0 : sqlite3_result_error(context, cloudsync_errmsg(data), -1);
1970 0 : } else {
1971 50 : sqlite3_result_int(context, 1);
1972 : }
1973 :
1974 50 : if (pk != buffer) cloudsync_memory_free(pk);
1975 50 : }
1976 :
1977 : // MARK: - Row Filter -
1978 :
1979 21 : void dbsync_set_filter (sqlite3_context *context, int argc, sqlite3_value **argv) {
1980 : DEBUG_FUNCTION("cloudsync_set_filter");
1981 :
1982 21 : const char *tbl = (const char *)database_value_text(argv[0]);
1983 21 : const char *filter_expr = (const char *)database_value_text(argv[1]);
1984 21 : if (!tbl || !filter_expr) {
1985 0 : dbsync_set_error(context, "cloudsync_set_filter: table and filter expression required");
1986 0 : return;
1987 : }
1988 :
1989 21 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
1990 :
1991 : // Guard against calling set_filter before the target table has been set
1992 : // up for sync: without this, we'd hit "no such table:
1993 : // cloudsync_table_settings" or "no such table: main.<tbl>" deep inside
1994 : // the trigger recreation path, which is not actionable.
1995 21 : if (!cloudsync_context_is_initialized(data) || !table_lookup(data, tbl)) {
1996 2 : dbsync_set_error(context,
1997 : "cloudsync_set_filter: table '%s' is not configured for sync. "
1998 1 : "Call SELECT cloudsync_init('%s') first.", tbl, tbl);
1999 1 : return;
2000 : }
2001 :
2002 : // Store filter in table settings
2003 20 : dbutils_table_settings_set_key_value(data, tbl, "*", "filter", filter_expr);
2004 :
2005 : // Read current algo
2006 20 : table_algo algo = dbutils_table_settings_get_algo(data, tbl);
2007 20 : if (algo == table_algo_none) algo = table_algo_crdt_cls;
2008 :
2009 : // Drop and recreate triggers with the filter
2010 20 : database_delete_triggers(data, tbl);
2011 20 : int rc = database_create_triggers(data, tbl, algo, filter_expr);
2012 20 : if (rc != DBRES_OK) {
2013 0 : dbsync_set_error(context, "cloudsync_set_filter: error recreating triggers");
2014 0 : sqlite3_result_error_code(context, rc);
2015 0 : return;
2016 : }
2017 :
2018 : // Clean and refill metatable with the new filter
2019 20 : rc = cloudsync_reset_metatable(data, tbl);
2020 20 : if (rc != DBRES_OK) {
2021 0 : dbsync_set_error(context, "cloudsync_set_filter: error resetting metatable");
2022 0 : sqlite3_result_error_code(context, rc);
2023 0 : return;
2024 : }
2025 :
2026 20 : sqlite3_result_int(context, 1);
2027 21 : }
2028 :
2029 5 : void dbsync_clear_filter (sqlite3_context *context, int argc, sqlite3_value **argv) {
2030 : DEBUG_FUNCTION("cloudsync_clear_filter");
2031 :
2032 5 : const char *tbl = (const char *)database_value_text(argv[0]);
2033 5 : if (!tbl) {
2034 0 : dbsync_set_error(context, "cloudsync_clear_filter: table name required");
2035 0 : return;
2036 : }
2037 :
2038 5 : cloudsync_context *data = (cloudsync_context *)sqlite3_user_data(context);
2039 :
2040 : // Guard against calling clear_filter before the target table has been set
2041 : // up for sync — see dbsync_set_filter for the same rationale.
2042 5 : if (!cloudsync_context_is_initialized(data) || !table_lookup(data, tbl)) {
2043 2 : dbsync_set_error(context,
2044 : "cloudsync_clear_filter: table '%s' is not configured for sync. "
2045 1 : "Call SELECT cloudsync_init('%s') first.", tbl, tbl);
2046 1 : return;
2047 : }
2048 :
2049 : // Remove filter from table settings (set to NULL/empty)
2050 4 : dbutils_table_settings_set_key_value(data, tbl, "*", "filter", NULL);
2051 :
2052 : // Read current algo
2053 4 : table_algo algo = dbutils_table_settings_get_algo(data, tbl);
2054 4 : if (algo == table_algo_none) algo = table_algo_crdt_cls;
2055 :
2056 : // Drop and recreate triggers without filter
2057 4 : database_delete_triggers(data, tbl);
2058 4 : int rc = database_create_triggers(data, tbl, algo, NULL);
2059 4 : if (rc != DBRES_OK) {
2060 0 : dbsync_set_error(context, "cloudsync_clear_filter: error recreating triggers");
2061 0 : sqlite3_result_error_code(context, rc);
2062 0 : return;
2063 : }
2064 :
2065 : // Clean and refill metatable without filter (all rows)
2066 4 : rc = cloudsync_reset_metatable(data, tbl);
2067 4 : if (rc != DBRES_OK) {
2068 0 : dbsync_set_error(context, "cloudsync_clear_filter: error resetting metatable");
2069 0 : sqlite3_result_error_code(context, rc);
2070 0 : return;
2071 : }
2072 :
2073 4 : sqlite3_result_int(context, 1);
2074 5 : }
2075 :
2076 255 : int dbsync_register_functions (sqlite3 *db, char **pzErrMsg) {
2077 255 : int rc = SQLITE_OK;
2078 :
2079 : // there's no built-in way to verify if sqlite3_cloudsync_init has already been called
2080 : // for this specific database connection, we use a workaround: we attempt to retrieve the
2081 : // cloudsync_version and check for an error, an error indicates that initialization has not been performed
2082 255 : if (sqlite3_exec(db, "SELECT cloudsync_version();", NULL, NULL, NULL) == SQLITE_OK) return SQLITE_OK;
2083 :
2084 : // init memory debugger (NOOP in production)
2085 : cloudsync_memory_init(1);
2086 :
2087 : // set fractional-indexing allocator to use cloudsync memory
2088 253 : block_init_allocator();
2089 :
2090 : // init context
2091 253 : void *ctx = cloudsync_context_create(db);
2092 253 : if (!ctx) {
2093 0 : if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Not enought memory to create a database context");
2094 0 : return SQLITE_NOMEM;
2095 : }
2096 :
2097 : // register functions
2098 :
2099 : // PUBLIC functions
2100 253 : rc = dbsync_register_pure_function(db, "cloudsync_version", dbsync_version, 0, pzErrMsg, ctx, cloudsync_context_free);
2101 253 : if (rc != SQLITE_OK) return rc;
2102 :
2103 253 : rc = dbsync_register_function(db, "cloudsync_init", dbsync_init1, 1, pzErrMsg, ctx, NULL);
2104 253 : if (rc != SQLITE_OK) return rc;
2105 :
2106 253 : rc = dbsync_register_function(db, "cloudsync_init", dbsync_init2, 2, pzErrMsg, ctx, NULL);
2107 253 : if (rc != SQLITE_OK) return rc;
2108 :
2109 253 : rc = dbsync_register_function(db, "cloudsync_init", dbsync_init3, 3, pzErrMsg, ctx, NULL);
2110 253 : if (rc != SQLITE_OK) return rc;
2111 :
2112 253 : rc = dbsync_register_function(db, "cloudsync_enable", dbsync_enable, 1, pzErrMsg, ctx, NULL);
2113 253 : if (rc != SQLITE_OK) return rc;
2114 :
2115 253 : rc = dbsync_register_function(db, "cloudsync_disable", dbsync_disable, 1, pzErrMsg, ctx, NULL);
2116 253 : if (rc != SQLITE_OK) return rc;
2117 :
2118 253 : rc = dbsync_register_function(db, "cloudsync_is_enabled", dbsync_is_enabled, 1, pzErrMsg, ctx, NULL);
2119 253 : if (rc != SQLITE_OK) return rc;
2120 :
2121 253 : rc = dbsync_register_function(db, "cloudsync_cleanup", dbsync_cleanup, 1, pzErrMsg, ctx, NULL);
2122 253 : if (rc != SQLITE_OK) return rc;
2123 :
2124 253 : rc = dbsync_register_function(db, "cloudsync_terminate", dbsync_terminate, 0, pzErrMsg, ctx, NULL);
2125 253 : if (rc != SQLITE_OK) return rc;
2126 :
2127 253 : rc = dbsync_register_function(db, "cloudsync_set", dbsync_set, 2, pzErrMsg, ctx, NULL);
2128 253 : if (rc != SQLITE_OK) return rc;
2129 :
2130 253 : rc = dbsync_register_function(db, "cloudsync_set_table", dbsync_set_table, 3, pzErrMsg, ctx, NULL);
2131 253 : if (rc != SQLITE_OK) return rc;
2132 :
2133 253 : rc = dbsync_register_function(db, "cloudsync_set_filter", dbsync_set_filter, 2, pzErrMsg, ctx, NULL);
2134 253 : if (rc != SQLITE_OK) return rc;
2135 :
2136 253 : rc = dbsync_register_function(db, "cloudsync_clear_filter", dbsync_clear_filter, 1, pzErrMsg, ctx, NULL);
2137 253 : if (rc != SQLITE_OK) return rc;
2138 :
2139 253 : rc = dbsync_register_function(db, "cloudsync_set_schema", dbsync_set_schema, 1, pzErrMsg, ctx, NULL);
2140 253 : if (rc != SQLITE_OK) return rc;
2141 :
2142 253 : rc = dbsync_register_function(db, "cloudsync_schema", dbsync_schema, 0, pzErrMsg, ctx, NULL);
2143 253 : if (rc != SQLITE_OK) return rc;
2144 :
2145 253 : rc = dbsync_register_function(db, "cloudsync_table_schema", dbsync_table_schema, 1, pzErrMsg, ctx, NULL);
2146 253 : if (rc != SQLITE_OK) return rc;
2147 :
2148 253 : rc = dbsync_register_function(db, "cloudsync_set_column", dbsync_set_column, 4, pzErrMsg, ctx, NULL);
2149 253 : if (rc != SQLITE_OK) return rc;
2150 :
2151 253 : rc = dbsync_register_function(db, "cloudsync_siteid", dbsync_siteid, 0, pzErrMsg, ctx, NULL);
2152 253 : if (rc != SQLITE_OK) return rc;
2153 :
2154 253 : rc = dbsync_register_function(db, "cloudsync_db_version", dbsync_db_version, 0, pzErrMsg, ctx, NULL);
2155 253 : if (rc != SQLITE_OK) return rc;
2156 :
2157 253 : rc = dbsync_register_function(db, "cloudsync_db_version_next", dbsync_db_version_next, 0, pzErrMsg, ctx, NULL);
2158 253 : if (rc != SQLITE_OK) return rc;
2159 :
2160 253 : rc = dbsync_register_function(db, "cloudsync_db_version_next", dbsync_db_version_next, 1, pzErrMsg, ctx, NULL);
2161 253 : if (rc != SQLITE_OK) return rc;
2162 :
2163 253 : rc = dbsync_register_function(db, "cloudsync_begin_alter", dbsync_begin_alter, 1, pzErrMsg, ctx, NULL);
2164 253 : if (rc != SQLITE_OK) return rc;
2165 :
2166 253 : rc = dbsync_register_function(db, "cloudsync_commit_alter", dbsync_commit_alter, 1, pzErrMsg, ctx, NULL);
2167 253 : if (rc != SQLITE_OK) return rc;
2168 :
2169 253 : rc = dbsync_register_function(db, "cloudsync_uuid", dbsync_uuid, 0, pzErrMsg, ctx, NULL);
2170 253 : if (rc != SQLITE_OK) return rc;
2171 :
2172 253 : rc = dbsync_register_function(db, "cloudsync_uuid_text", dbsync_uuid_text, 1, pzErrMsg, ctx, NULL);
2173 253 : if (rc != SQLITE_OK) return rc;
2174 253 : rc = dbsync_register_function(db, "cloudsync_uuid_text", dbsync_uuid_text, 2, pzErrMsg, ctx, NULL);
2175 253 : if (rc != SQLITE_OK) return rc;
2176 253 : rc = dbsync_register_function(db, "cloudsync_uuid_blob", dbsync_uuid_blob, 1, pzErrMsg, ctx, NULL);
2177 253 : if (rc != SQLITE_OK) return rc;
2178 :
2179 : // PAYLOAD
2180 253 : rc = dbsync_register_aggregate(db, "cloudsync_payload_encode", dbsync_payload_encode_step, dbsync_payload_encode_final, -1, pzErrMsg, ctx, NULL);
2181 253 : if (rc != SQLITE_OK) return rc;
2182 :
2183 : // alias
2184 253 : rc = dbsync_register_function(db, "cloudsync_payload_decode", dbsync_payload_decode, -1, pzErrMsg, ctx, NULL);
2185 253 : if (rc != SQLITE_OK) return rc;
2186 253 : rc = dbsync_register_function(db, "cloudsync_payload_apply", dbsync_payload_decode, -1, pzErrMsg, ctx, NULL);
2187 253 : if (rc != SQLITE_OK) return rc;
2188 :
2189 253 : rc = sqlite3_create_module(db, "cloudsync_payload_chunks", &cloudsync_payload_chunks_module, (void *)ctx);
2190 253 : if (rc != SQLITE_OK) return rc;
2191 :
2192 253 : rc = dbsync_register_function(db, "cloudsync_payload_blob_checked", dbsync_payload_blob_checked, 5, pzErrMsg, ctx, NULL);
2193 253 : if (rc != SQLITE_OK) return rc;
2194 :
2195 : #ifdef CLOUDSYNC_DESKTOP_OS
2196 253 : rc = dbsync_register_function(db, "cloudsync_payload_save", dbsync_payload_save, 1, pzErrMsg, ctx, NULL);
2197 253 : if (rc != SQLITE_OK) return rc;
2198 :
2199 253 : rc = dbsync_register_function(db, "cloudsync_payload_load", dbsync_payload_load, 1, pzErrMsg, ctx, NULL);
2200 253 : if (rc != SQLITE_OK) return rc;
2201 : #endif
2202 :
2203 : // PRIVATE functions (used inside triggers — require SQLITE_INNOCUOUS)
2204 253 : rc = dbsync_register_trigger_function(db, "cloudsync_is_sync", dbsync_is_sync, 1, pzErrMsg, ctx, NULL);
2205 253 : if (rc != SQLITE_OK) return rc;
2206 :
2207 253 : rc = dbsync_register_trigger_function(db, "cloudsync_insert", dbsync_insert, -1, pzErrMsg, ctx, NULL);
2208 253 : if (rc != SQLITE_OK) return rc;
2209 :
2210 253 : rc = dbsync_register_trigger_aggregate(db, "cloudsync_update", dbsync_update_step, dbsync_update_final, 3, pzErrMsg, ctx, NULL);
2211 253 : if (rc != SQLITE_OK) return rc;
2212 :
2213 253 : rc = dbsync_register_trigger_function(db, "cloudsync_delete", dbsync_delete, -1, pzErrMsg, ctx, NULL);
2214 253 : if (rc != SQLITE_OK) return rc;
2215 :
2216 253 : rc = dbsync_register_function(db, "cloudsync_col_value", dbsync_col_value, 3, pzErrMsg, ctx, NULL);
2217 253 : if (rc != SQLITE_OK) return rc;
2218 :
2219 253 : rc = dbsync_register_pure_function(db, "cloudsync_pk_encode", dbsync_pk_encode, -1, pzErrMsg, ctx, NULL);
2220 253 : if (rc != SQLITE_OK) return rc;
2221 :
2222 253 : rc = dbsync_register_pure_function(db, "cloudsync_pk_decode", dbsync_pk_decode, 2, pzErrMsg, ctx, NULL);
2223 253 : if (rc != SQLITE_OK) return rc;
2224 :
2225 253 : rc = dbsync_register_function(db, "cloudsync_seq", dbsync_seq, 0, pzErrMsg, ctx, NULL);
2226 253 : if (rc != SQLITE_OK) return rc;
2227 :
2228 253 : rc = dbsync_register_function(db, "cloudsync_text_materialize", dbsync_text_materialize, -1, pzErrMsg, ctx, NULL);
2229 253 : if (rc != SQLITE_OK) return rc;
2230 :
2231 : // NETWORK LAYER
2232 : #ifndef CLOUDSYNC_OMIT_NETWORK
2233 0 : rc = cloudsync_network_register(db, pzErrMsg, ctx);
2234 0 : if (rc != SQLITE_OK) return rc;
2235 : #endif
2236 :
2237 253 : cloudsync_context *data = (cloudsync_context *)ctx;
2238 253 : sqlite3_commit_hook(db, cloudsync_commit_hook, ctx);
2239 253 : sqlite3_rollback_hook(db, cloudsync_rollback_hook, ctx);
2240 :
2241 : // register eponymous only changes virtual table
2242 253 : rc = cloudsync_vtab_register_changes (db, data);
2243 253 : if (rc != SQLITE_OK) {
2244 0 : if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("Error creating changes virtual table: %s", sqlite3_errmsg(db));
2245 0 : return rc;
2246 : }
2247 :
2248 : // load config, if exists
2249 253 : if (cloudsync_config_exists(data)) {
2250 5 : if (cloudsync_context_init(data) == NULL) {
2251 : // Do not free ctx here: it is already owned by the cloudsync_version
2252 : // function (registered above with cloudsync_context_free as its
2253 : // destructor). SQLite will release it when the connection is closed.
2254 : // Freeing it manually would cause a double-free on sqlite3_close.
2255 0 : if (pzErrMsg) *pzErrMsg = sqlite3_mprintf("An error occurred while trying to initialize context");
2256 0 : return SQLITE_ERROR;
2257 : }
2258 :
2259 : // update schema hash if upgrading from an older version
2260 5 : if (dbutils_settings_check_version(data, NULL) != 0) {
2261 0 : cloudsync_update_schema_hash(data);
2262 0 : }
2263 :
2264 : // make sure to update internal version to current version
2265 5 : dbutils_settings_set_key_value(data, CLOUDSYNC_KEY_LIBVERSION, CLOUDSYNC_VERSION);
2266 5 : }
2267 :
2268 253 : return SQLITE_OK;
2269 255 : }
2270 :
2271 : // MARK: - Main Entrypoint -
2272 :
2273 255 : APIEXPORT int sqlite3_cloudsync_init (sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
2274 : DEBUG_FUNCTION("sqlite3_cloudsync_init");
2275 :
2276 : #ifndef SQLITE_CORE
2277 : SQLITE_EXTENSION_INIT2(pApi);
2278 : #endif
2279 :
2280 255 : return dbsync_register_functions(db, pzErrMsg);
2281 : }
|