@tanstack/db-sqlite-persistence-core 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1588 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const db = require("@tanstack/db");
4
+ const errors = require("./errors.cjs");
5
+ const persisted = require("./persisted.cjs");
6
+ const DEFAULT_SCHEMA_VERSION = 1;
7
+ const DEFAULT_PULL_SINCE_RELOAD_THRESHOLD = 128;
8
+ const SQLITE_MAX_IN_BATCH_SIZE = 900;
9
+ const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
10
+ const FORBIDDEN_SQL_FRAGMENT_PATTERN = /(;|--|\/\*)/;
11
+ const PERSISTED_TYPE_TAG = `__tanstack_db_persisted_type__`;
12
+ const PERSISTED_VALUE_TAG = `value`;
13
+ const persistedTaggedValueTypes = /* @__PURE__ */ new Set([
14
+ `bigint`,
15
+ `date`,
16
+ `nan`,
17
+ `infinity`,
18
+ `-infinity`
19
+ ]);
20
+ const orderByObjectIds = /* @__PURE__ */ new WeakMap();
21
+ let nextOrderByObjectId = 1;
22
+ function isDuplicateColumnAddError(error, columnName) {
23
+ if (!(error instanceof Error)) {
24
+ return false;
25
+ }
26
+ const message = error.message.toLowerCase();
27
+ const normalizedColumnName = columnName.toLowerCase();
28
+ return message.includes(`duplicate column name`) && message.includes(normalizedColumnName) || message.includes(`already exists`) && message.includes(normalizedColumnName);
29
+ }
30
+ function quoteIdentifier(identifier) {
31
+ if (!SAFE_IDENTIFIER_PATTERN.test(identifier)) {
32
+ throw new errors.InvalidPersistedCollectionConfigError(
33
+ `Invalid SQLite identifier "${identifier}"`
34
+ );
35
+ }
36
+ return `"${identifier}"`;
37
+ }
38
+ function isPersistedTaggedValue(value) {
39
+ if (typeof value !== `object` || value === null) {
40
+ return false;
41
+ }
42
+ const taggedValue = value;
43
+ if (typeof taggedValue[PERSISTED_TYPE_TAG] !== `string` || typeof taggedValue[PERSISTED_VALUE_TAG] !== `string`) {
44
+ return false;
45
+ }
46
+ return persistedTaggedValueTypes.has(
47
+ taggedValue[PERSISTED_TYPE_TAG]
48
+ );
49
+ }
50
+ function encodePersistedJsonValue(value) {
51
+ if (value === void 0 || value === null) {
52
+ return value;
53
+ }
54
+ if (typeof value === `bigint`) {
55
+ return {
56
+ [PERSISTED_TYPE_TAG]: `bigint`,
57
+ [PERSISTED_VALUE_TAG]: value.toString()
58
+ };
59
+ }
60
+ if (value instanceof Date) {
61
+ return {
62
+ [PERSISTED_TYPE_TAG]: `date`,
63
+ [PERSISTED_VALUE_TAG]: value.toISOString()
64
+ };
65
+ }
66
+ if (typeof value === `number`) {
67
+ if (Number.isNaN(value)) {
68
+ return {
69
+ [PERSISTED_TYPE_TAG]: `nan`,
70
+ [PERSISTED_VALUE_TAG]: `NaN`
71
+ };
72
+ }
73
+ if (value === Number.POSITIVE_INFINITY) {
74
+ return {
75
+ [PERSISTED_TYPE_TAG]: `infinity`,
76
+ [PERSISTED_VALUE_TAG]: `Infinity`
77
+ };
78
+ }
79
+ if (value === Number.NEGATIVE_INFINITY) {
80
+ return {
81
+ [PERSISTED_TYPE_TAG]: `-infinity`,
82
+ [PERSISTED_VALUE_TAG]: `-Infinity`
83
+ };
84
+ }
85
+ return value;
86
+ }
87
+ if (Array.isArray(value)) {
88
+ return value.map((entry) => encodePersistedJsonValue(entry));
89
+ }
90
+ if (typeof value === `object`) {
91
+ const encodedRecord = {};
92
+ const recordValue = value;
93
+ for (const [key, entryValue] of Object.entries(recordValue)) {
94
+ const encodedValue = encodePersistedJsonValue(entryValue);
95
+ if (encodedValue !== void 0) {
96
+ encodedRecord[key] = encodedValue;
97
+ }
98
+ }
99
+ return encodedRecord;
100
+ }
101
+ return value;
102
+ }
103
+ function decodePersistedJsonValue(value) {
104
+ if (value === void 0 || value === null) {
105
+ return value;
106
+ }
107
+ if (isPersistedTaggedValue(value)) {
108
+ switch (value[PERSISTED_TYPE_TAG]) {
109
+ case `bigint`:
110
+ return BigInt(value[PERSISTED_VALUE_TAG]);
111
+ case `date`: {
112
+ const parsedDate = new Date(value[PERSISTED_VALUE_TAG]);
113
+ return Number.isNaN(parsedDate.getTime()) ? null : parsedDate;
114
+ }
115
+ case `nan`:
116
+ return Number.NaN;
117
+ case `infinity`:
118
+ return Number.POSITIVE_INFINITY;
119
+ case `-infinity`:
120
+ return Number.NEGATIVE_INFINITY;
121
+ default:
122
+ return value;
123
+ }
124
+ }
125
+ if (Array.isArray(value)) {
126
+ return value.map((entry) => decodePersistedJsonValue(entry));
127
+ }
128
+ if (typeof value === `object`) {
129
+ const decodedRecord = {};
130
+ const recordValue = value;
131
+ for (const [key, entryValue] of Object.entries(recordValue)) {
132
+ decodedRecord[key] = decodePersistedJsonValue(entryValue);
133
+ }
134
+ return decodedRecord;
135
+ }
136
+ return value;
137
+ }
138
+ function serializePersistedRowValue(value) {
139
+ return JSON.stringify(encodePersistedJsonValue(value));
140
+ }
141
+ function deserializePersistedRowValue(value) {
142
+ const parsedJson = JSON.parse(value);
143
+ return decodePersistedJsonValue(parsedJson);
144
+ }
145
+ function toSqliteParameterValue(value) {
146
+ if (value == null) {
147
+ return null;
148
+ }
149
+ if (typeof value === `number`) {
150
+ if (!Number.isFinite(value)) {
151
+ return null;
152
+ }
153
+ return value;
154
+ }
155
+ if (typeof value === `bigint`) {
156
+ return value.toString();
157
+ }
158
+ if (typeof value === `boolean`) {
159
+ return value ? 1 : 0;
160
+ }
161
+ if (typeof value === `string`) {
162
+ return value;
163
+ }
164
+ if (value instanceof Date) {
165
+ return value.toISOString();
166
+ }
167
+ return serializePersistedRowValue(value);
168
+ }
169
+ function toSqliteLiteral(value) {
170
+ if (value === null) {
171
+ return `NULL`;
172
+ }
173
+ if (typeof value === `number`) {
174
+ return Number.isFinite(value) ? String(value) : `NULL`;
175
+ }
176
+ return `'${value.replace(/'/g, `''`)}'`;
177
+ }
178
+ function inlineSqlParams(sql, params) {
179
+ let index = 0;
180
+ const inlinedSql = sql.replace(/\?/g, () => {
181
+ const paramValue = params[index];
182
+ index++;
183
+ return toSqliteLiteral(paramValue ?? null);
184
+ });
185
+ if (index !== params.length) {
186
+ throw new errors.InvalidPersistedCollectionConfigError(
187
+ `Unable to inline SQL params; placeholder count did not match provided params`
188
+ );
189
+ }
190
+ return inlinedSql;
191
+ }
192
+ function collectAliasQualifiedRefSegments(expression, segments = /* @__PURE__ */ new Set()) {
193
+ if (expression.type === `ref`) {
194
+ if (expression.path.length > 1) {
195
+ const rootSegment = String(expression.path[0]);
196
+ if (rootSegment.length > 0) {
197
+ segments.add(rootSegment);
198
+ }
199
+ }
200
+ return segments;
201
+ }
202
+ if (expression.type === `func`) {
203
+ for (const arg of expression.args) {
204
+ collectAliasQualifiedRefSegments(arg, segments);
205
+ }
206
+ }
207
+ return segments;
208
+ }
209
+ function createAliasAwareRowProxy(row, aliasSegments) {
210
+ return new Proxy(row, {
211
+ get(target, prop, receiver) {
212
+ if (typeof prop !== `string`) {
213
+ return Reflect.get(target, prop, receiver);
214
+ }
215
+ if (Object.prototype.hasOwnProperty.call(target, prop)) {
216
+ const value = Reflect.get(target, prop, receiver);
217
+ if (value !== void 0 || !aliasSegments.has(prop)) {
218
+ return value;
219
+ }
220
+ return target;
221
+ }
222
+ if (aliasSegments.has(prop)) {
223
+ return target;
224
+ }
225
+ return void 0;
226
+ }
227
+ });
228
+ }
229
+ function compileRowExpressionEvaluator(expression) {
230
+ let baseEvaluator;
231
+ try {
232
+ baseEvaluator = db.compileSingleRowExpression(expression);
233
+ } catch (error) {
234
+ throw new errors.InvalidPersistedCollectionConfigError(
235
+ `Unsupported expression for SQLite adapter fallback evaluator: ${error.message}`
236
+ );
237
+ }
238
+ const aliasSegments = collectAliasQualifiedRefSegments(expression);
239
+ if (aliasSegments.size === 0) {
240
+ return (row) => baseEvaluator(row);
241
+ }
242
+ const proxyCache = /* @__PURE__ */ new WeakMap();
243
+ return (row) => {
244
+ let proxy = proxyCache.get(row);
245
+ if (!proxy) {
246
+ proxy = createAliasAwareRowProxy(row, aliasSegments);
247
+ proxyCache.set(row, proxy);
248
+ }
249
+ return baseEvaluator(proxy);
250
+ };
251
+ }
252
+ function getOrderByObjectId(value) {
253
+ const existing = orderByObjectIds.get(value);
254
+ if (existing !== void 0) {
255
+ return existing;
256
+ }
257
+ const nextId = nextOrderByObjectId;
258
+ nextOrderByObjectId++;
259
+ orderByObjectIds.set(value, nextId);
260
+ return nextId;
261
+ }
262
+ function compareOrderByValues(left, right, compareOptions) {
263
+ if (left == null && right == null) {
264
+ return 0;
265
+ }
266
+ if (left == null) {
267
+ return compareOptions.nulls === `first` ? -1 : 1;
268
+ }
269
+ if (right == null) {
270
+ return compareOptions.nulls === `first` ? 1 : -1;
271
+ }
272
+ if (typeof left === `string` && typeof right === `string`) {
273
+ if (compareOptions.stringSort === `locale`) {
274
+ return left.localeCompare(
275
+ right,
276
+ compareOptions.locale,
277
+ compareOptions.localeOptions
278
+ );
279
+ }
280
+ if (left < right) {
281
+ return -1;
282
+ }
283
+ if (left > right) {
284
+ return 1;
285
+ }
286
+ return 0;
287
+ }
288
+ if (Array.isArray(left) && Array.isArray(right)) {
289
+ const maxIndex = Math.min(left.length, right.length);
290
+ for (let index = 0; index < maxIndex; index++) {
291
+ const comparison = compareOrderByValues(
292
+ left[index],
293
+ right[index],
294
+ compareOptions
295
+ );
296
+ if (comparison !== 0) {
297
+ return comparison;
298
+ }
299
+ }
300
+ return left.length - right.length;
301
+ }
302
+ if (left instanceof Date && right instanceof Date) {
303
+ return left.getTime() - right.getTime();
304
+ }
305
+ const leftIsObject = typeof left === `object`;
306
+ const rightIsObject = typeof right === `object`;
307
+ if (leftIsObject || rightIsObject) {
308
+ if (leftIsObject && rightIsObject) {
309
+ return getOrderByObjectId(left) - getOrderByObjectId(right);
310
+ }
311
+ return leftIsObject ? 1 : -1;
312
+ }
313
+ if (left < right) {
314
+ return -1;
315
+ }
316
+ if (left > right) {
317
+ return 1;
318
+ }
319
+ return 0;
320
+ }
321
+ function createJsonPath(path) {
322
+ if (path.length === 0) {
323
+ return null;
324
+ }
325
+ let jsonPath = `$`;
326
+ for (const segment of path) {
327
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)) {
328
+ jsonPath = `${jsonPath}.${segment}`;
329
+ continue;
330
+ }
331
+ if (/^[0-9]+$/.test(segment)) {
332
+ jsonPath = `${jsonPath}[${segment}]`;
333
+ continue;
334
+ }
335
+ return null;
336
+ }
337
+ return jsonPath;
338
+ }
339
+ function getLiteralValueKind(value) {
340
+ if (typeof value === `bigint`) {
341
+ return `bigint`;
342
+ }
343
+ if (value instanceof Date) {
344
+ return `datetime`;
345
+ }
346
+ return `unknown`;
347
+ }
348
+ function getCompiledValueKind(fragment) {
349
+ return fragment.valueKind ?? `unknown`;
350
+ }
351
+ function resolveComparisonValueKind(leftExpression, rightExpression, leftCompiled, rightCompiled) {
352
+ const leftKind = getCompiledValueKind(leftCompiled);
353
+ const rightKind = getCompiledValueKind(rightCompiled);
354
+ const hasBigIntLiteral = leftExpression.type === `val` && typeof leftExpression.value === `bigint` || rightExpression.type === `val` && typeof rightExpression.value === `bigint`;
355
+ if (hasBigIntLiteral || leftKind === `bigint` || rightKind === `bigint`) {
356
+ return `bigint`;
357
+ }
358
+ const hasDateLiteral = leftExpression.type === `val` && leftExpression.value instanceof Date || rightExpression.type === `val` && rightExpression.value instanceof Date;
359
+ if (hasDateLiteral || leftKind === `datetime` || rightKind === `datetime`) {
360
+ return `datetime`;
361
+ }
362
+ if (leftKind === `date` || rightKind === `date`) {
363
+ return `date`;
364
+ }
365
+ return `unknown`;
366
+ }
367
+ function compileComparisonSql(operator, leftSql, rightSql, valueKind) {
368
+ if (valueKind === `bigint`) {
369
+ return `(CAST(${leftSql} AS NUMERIC) ${operator} CAST(${rightSql} AS NUMERIC))`;
370
+ }
371
+ if (valueKind === `date`) {
372
+ return `(date(${leftSql}) ${operator} date(${rightSql}))`;
373
+ }
374
+ if (valueKind === `datetime`) {
375
+ return `(datetime(${leftSql}) ${operator} datetime(${rightSql}))`;
376
+ }
377
+ return `(${leftSql} ${operator} ${rightSql})`;
378
+ }
379
+ function compileRefExpressionSql(jsonPath) {
380
+ const typePath = `${jsonPath}.${PERSISTED_TYPE_TAG}`;
381
+ const taggedValuePath = `${jsonPath}.${PERSISTED_VALUE_TAG}`;
382
+ return {
383
+ supported: true,
384
+ sql: `(CASE json_extract(value, ?)
385
+ WHEN 'bigint' THEN CAST(json_extract(value, ?) AS NUMERIC)
386
+ WHEN 'date' THEN json_extract(value, ?)
387
+ WHEN 'nan' THEN NULL
388
+ WHEN 'infinity' THEN NULL
389
+ WHEN '-infinity' THEN NULL
390
+ ELSE json_extract(value, ?)
391
+ END)`,
392
+ params: [typePath, taggedValuePath, taggedValuePath, jsonPath],
393
+ valueKind: `unknown`
394
+ };
395
+ }
396
+ function sanitizeExpressionSqlFragment(fragment) {
397
+ if (fragment.trim().length === 0 || FORBIDDEN_SQL_FRAGMENT_PATTERN.test(fragment)) {
398
+ throw new errors.InvalidPersistedCollectionConfigError(
399
+ `Invalid persisted index SQL fragment: "${fragment}"`
400
+ );
401
+ }
402
+ return fragment;
403
+ }
404
+ function decodeStoredSqliteRows(storedRows) {
405
+ return storedRows.map((row) => {
406
+ const key = persisted.decodePersistedStorageKey(row.key);
407
+ const value = deserializePersistedRowValue(row.value);
408
+ return {
409
+ key,
410
+ value,
411
+ metadata: row.metadata != null ? deserializePersistedRowValue(row.metadata) : void 0,
412
+ rowVersion: row.row_version
413
+ };
414
+ });
415
+ }
416
+ function stableStringify(value) {
417
+ return serializePersistedRowValue(value);
418
+ }
419
+ function compileSqlExpression(expression) {
420
+ if (expression.type === `val`) {
421
+ const valueKind = getLiteralValueKind(expression.value);
422
+ return {
423
+ supported: true,
424
+ sql: `?`,
425
+ params: [toSqliteParameterValue(expression.value)],
426
+ valueKind
427
+ };
428
+ }
429
+ if (expression.type === `ref`) {
430
+ const jsonPath = createJsonPath(expression.path.map(String));
431
+ if (!jsonPath) {
432
+ return {
433
+ supported: false,
434
+ sql: ``,
435
+ params: []
436
+ };
437
+ }
438
+ return compileRefExpressionSql(jsonPath);
439
+ }
440
+ const compiledArgs = expression.args.map((arg) => compileSqlExpression(arg));
441
+ if (compiledArgs.some((arg) => !arg.supported)) {
442
+ return {
443
+ supported: false,
444
+ sql: ``,
445
+ params: []
446
+ };
447
+ }
448
+ const params = compiledArgs.flatMap((arg) => arg.params);
449
+ const argSql = compiledArgs.map((arg) => arg.sql);
450
+ switch (expression.name) {
451
+ case `eq`:
452
+ case `gt`:
453
+ case `gte`:
454
+ case `lt`:
455
+ case `lte`: {
456
+ if (expression.args.length !== 2 || !argSql[0] || !argSql[1] || !compiledArgs[0] || !compiledArgs[1]) {
457
+ return { supported: false, sql: ``, params: [] };
458
+ }
459
+ const valueKind = resolveComparisonValueKind(
460
+ expression.args[0],
461
+ expression.args[1],
462
+ compiledArgs[0],
463
+ compiledArgs[1]
464
+ );
465
+ const operatorByName = {
466
+ eq: `=`,
467
+ gt: `>`,
468
+ gte: `>=`,
469
+ lt: `<`,
470
+ lte: `<=`
471
+ };
472
+ return {
473
+ supported: true,
474
+ sql: compileComparisonSql(
475
+ operatorByName[expression.name],
476
+ argSql[0],
477
+ argSql[1],
478
+ valueKind
479
+ ),
480
+ params
481
+ };
482
+ }
483
+ case `and`: {
484
+ if (argSql.length < 2) {
485
+ return { supported: false, sql: ``, params: [] };
486
+ }
487
+ return {
488
+ supported: true,
489
+ sql: argSql.map((sql) => `(${sql})`).join(` AND `),
490
+ params
491
+ };
492
+ }
493
+ case `or`: {
494
+ if (argSql.length < 2) {
495
+ return { supported: false, sql: ``, params: [] };
496
+ }
497
+ return {
498
+ supported: true,
499
+ sql: argSql.map((sql) => `(${sql})`).join(` OR `),
500
+ params
501
+ };
502
+ }
503
+ case `not`: {
504
+ if (argSql.length !== 1) {
505
+ return { supported: false, sql: ``, params: [] };
506
+ }
507
+ return {
508
+ supported: true,
509
+ sql: `(NOT (${argSql[0]}))`,
510
+ params
511
+ };
512
+ }
513
+ case `in`: {
514
+ if (expression.args.length !== 2 || expression.args[1]?.type !== `val`) {
515
+ return { supported: false, sql: ``, params: [] };
516
+ }
517
+ const listValue = expression.args[1].value;
518
+ if (!Array.isArray(listValue)) {
519
+ return { supported: false, sql: ``, params: [] };
520
+ }
521
+ if (listValue.length === 0) {
522
+ return { supported: true, sql: `(0 = 1)`, params: [] };
523
+ }
524
+ const leftSql = argSql[0];
525
+ const leftParams = compiledArgs[0]?.params ?? [];
526
+ if (!leftSql) {
527
+ return { supported: false, sql: ``, params: [] };
528
+ }
529
+ if (listValue.length > SQLITE_MAX_IN_BATCH_SIZE) {
530
+ const hasBigIntValues2 = listValue.some(
531
+ (value) => typeof value === `bigint`
532
+ );
533
+ const inLeftSql2 = hasBigIntValues2 ? `CAST(${leftSql} AS NUMERIC)` : leftSql;
534
+ const chunkClauses = [];
535
+ const batchedParams = [];
536
+ for (let startIndex = 0; startIndex < listValue.length; startIndex += SQLITE_MAX_IN_BATCH_SIZE) {
537
+ const chunkValues = listValue.slice(
538
+ startIndex,
539
+ startIndex + SQLITE_MAX_IN_BATCH_SIZE
540
+ );
541
+ chunkClauses.push(
542
+ `(${inLeftSql2} IN (${chunkValues.map(() => `?`).join(`, `)}))`
543
+ );
544
+ batchedParams.push(...leftParams);
545
+ batchedParams.push(
546
+ ...chunkValues.map((value) => toSqliteParameterValue(value))
547
+ );
548
+ }
549
+ return {
550
+ supported: true,
551
+ sql: `(${chunkClauses.join(` OR `)})`,
552
+ params: batchedParams
553
+ };
554
+ }
555
+ const hasBigIntValues = listValue.some(
556
+ (value) => typeof value === `bigint`
557
+ );
558
+ const inLeftSql = hasBigIntValues ? `CAST(${leftSql} AS NUMERIC)` : leftSql;
559
+ const listPlaceholders = listValue.map(() => `?`).join(`, `);
560
+ return {
561
+ supported: true,
562
+ sql: `(${inLeftSql} IN (${listPlaceholders}))`,
563
+ params: [
564
+ ...leftParams,
565
+ ...listValue.map((value) => toSqliteParameterValue(value))
566
+ ]
567
+ };
568
+ }
569
+ case `like`:
570
+ return {
571
+ supported: true,
572
+ sql: `(${argSql[0]} LIKE ${argSql[1]})`,
573
+ params
574
+ };
575
+ case `ilike`:
576
+ return {
577
+ supported: true,
578
+ sql: `(LOWER(${argSql[0]}) LIKE LOWER(${argSql[1]}))`,
579
+ params
580
+ };
581
+ case `isNull`:
582
+ case `isUndefined`:
583
+ return { supported: true, sql: `(${argSql[0]} IS NULL)`, params };
584
+ case `upper`:
585
+ return { supported: true, sql: `UPPER(${argSql[0]})`, params };
586
+ case `lower`:
587
+ return { supported: true, sql: `LOWER(${argSql[0]})`, params };
588
+ case `length`:
589
+ return { supported: true, sql: `LENGTH(${argSql[0]})`, params };
590
+ case `concat`:
591
+ return { supported: true, sql: `(${argSql.join(` || `)})`, params };
592
+ case `coalesce`:
593
+ return { supported: true, sql: `COALESCE(${argSql.join(`, `)})`, params };
594
+ case `add`:
595
+ return { supported: true, sql: `(${argSql[0]} + ${argSql[1]})`, params };
596
+ case `subtract`:
597
+ return { supported: true, sql: `(${argSql[0]} - ${argSql[1]})`, params };
598
+ case `multiply`:
599
+ return { supported: true, sql: `(${argSql[0]} * ${argSql[1]})`, params };
600
+ case `divide`:
601
+ return { supported: true, sql: `(${argSql[0]} / ${argSql[1]})`, params };
602
+ case `date`:
603
+ return {
604
+ supported: true,
605
+ sql: `date(${argSql[0]})`,
606
+ params,
607
+ valueKind: `date`
608
+ };
609
+ case `datetime`:
610
+ return {
611
+ supported: true,
612
+ sql: `datetime(${argSql[0]})`,
613
+ params,
614
+ valueKind: `datetime`
615
+ };
616
+ case `strftime`:
617
+ return {
618
+ supported: true,
619
+ sql: `strftime(${argSql.join(`, `)})`,
620
+ params
621
+ };
622
+ default:
623
+ return {
624
+ supported: false,
625
+ sql: ``,
626
+ params: []
627
+ };
628
+ }
629
+ }
630
+ function compileOrderByClauses(orderBy) {
631
+ if (!orderBy || orderBy.length === 0) {
632
+ return {
633
+ supported: true,
634
+ sql: ``,
635
+ params: []
636
+ };
637
+ }
638
+ const parts = [];
639
+ const params = [];
640
+ for (const clause of orderBy) {
641
+ const compiledExpression = compileSqlExpression(clause.expression);
642
+ if (!compiledExpression.supported) {
643
+ return {
644
+ supported: false,
645
+ sql: ``,
646
+ params: []
647
+ };
648
+ }
649
+ params.push(...compiledExpression.params);
650
+ const direction = clause.compareOptions.direction === `desc` ? `DESC` : `ASC`;
651
+ const nulls = clause.compareOptions.nulls === `first` ? `NULLS FIRST` : `NULLS LAST`;
652
+ parts.push(`${compiledExpression.sql} ${direction} ${nulls}`);
653
+ }
654
+ return {
655
+ supported: true,
656
+ sql: parts.join(`, `),
657
+ params
658
+ };
659
+ }
660
+ function isExpressionLikeShape(value) {
661
+ if (typeof value !== `object` || value === null) {
662
+ return false;
663
+ }
664
+ const candidate = value;
665
+ if (candidate.type === `val`) {
666
+ return Object.prototype.hasOwnProperty.call(candidate, `value`);
667
+ }
668
+ if (candidate.type === `ref`) {
669
+ return Array.isArray(candidate.path);
670
+ }
671
+ if (candidate.type === `func`) {
672
+ if (typeof candidate.name !== `string` || !Array.isArray(candidate.args)) {
673
+ return false;
674
+ }
675
+ return candidate.args.every((arg) => isExpressionLikeShape(arg));
676
+ }
677
+ return false;
678
+ }
679
+ function normalizeIndexSqlFragment(fragment) {
680
+ const trimmed = fragment.trim();
681
+ if (trimmed.length === 0) {
682
+ throw new errors.InvalidPersistedCollectionConfigError(
683
+ `Index SQL fragment cannot be empty`
684
+ );
685
+ }
686
+ let parsedJson;
687
+ let hasParsedJson = false;
688
+ try {
689
+ parsedJson = JSON.parse(trimmed);
690
+ hasParsedJson = true;
691
+ } catch {
692
+ }
693
+ if (hasParsedJson && isExpressionLikeShape(parsedJson)) {
694
+ const compiled = compileSqlExpression(parsedJson);
695
+ if (!compiled.supported) {
696
+ throw new errors.InvalidPersistedCollectionConfigError(
697
+ `Persisted index expression is not supported by the SQLite compiler`
698
+ );
699
+ }
700
+ return inlineSqlParams(compiled.sql, compiled.params);
701
+ }
702
+ return sanitizeExpressionSqlFragment(fragment);
703
+ }
704
+ function mergeObjectRows(existing, incoming) {
705
+ if (typeof existing === `object` && existing !== null) {
706
+ return Object.assign({}, existing, incoming);
707
+ }
708
+ return incoming;
709
+ }
710
+ function buildIndexName(collectionId, signature) {
711
+ const sanitizedSignature = signature.toLowerCase().replace(/[^a-z0-9]+/g, `_`).replace(/^_+|_+$/g, ``).slice(0, 24);
712
+ const hashSource = `${collectionId}:${signature}`;
713
+ const hashedPart = persisted.createPersistedTableName(hashSource, `c`);
714
+ const suffix = sanitizedSignature.length > 0 ? sanitizedSignature : `sig`;
715
+ return `idx_${hashedPart}_${suffix}`;
716
+ }
717
+ class SQLiteCorePersistenceAdapter {
718
+ constructor(options) {
719
+ this.initialized = false;
720
+ this.collectionTableCache = /* @__PURE__ */ new Map();
721
+ this.collectionTableLoads = /* @__PURE__ */ new Map();
722
+ const schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
723
+ if (!Number.isInteger(schemaVersion) || schemaVersion < 0) {
724
+ throw new errors.InvalidPersistedCollectionConfigError(
725
+ `SQLite adapter schemaVersion must be a non-negative integer`
726
+ );
727
+ }
728
+ if (options.appliedTxPruneMaxRows !== void 0 && (!Number.isInteger(options.appliedTxPruneMaxRows) || options.appliedTxPruneMaxRows < 0)) {
729
+ throw new errors.InvalidPersistedCollectionConfigError(
730
+ `SQLite adapter appliedTxPruneMaxRows must be a non-negative integer when provided`
731
+ );
732
+ }
733
+ if (options.appliedTxPruneMaxAgeSeconds !== void 0 && (!Number.isInteger(options.appliedTxPruneMaxAgeSeconds) || options.appliedTxPruneMaxAgeSeconds < 0)) {
734
+ throw new errors.InvalidPersistedCollectionConfigError(
735
+ `SQLite adapter appliedTxPruneMaxAgeSeconds must be a non-negative integer when provided`
736
+ );
737
+ }
738
+ const pullSinceReloadThreshold = options.pullSinceReloadThreshold ?? DEFAULT_PULL_SINCE_RELOAD_THRESHOLD;
739
+ if (!Number.isInteger(pullSinceReloadThreshold) || pullSinceReloadThreshold < 0) {
740
+ throw new errors.InvalidPersistedCollectionConfigError(
741
+ `SQLite adapter pullSinceReloadThreshold must be a non-negative integer`
742
+ );
743
+ }
744
+ this.driver = options.driver;
745
+ this.schemaVersion = schemaVersion;
746
+ this.schemaMismatchPolicy = options.schemaMismatchPolicy ?? `sync-present-reset`;
747
+ this.appliedTxPruneMaxRows = options.appliedTxPruneMaxRows;
748
+ this.appliedTxPruneMaxAgeSeconds = options.appliedTxPruneMaxAgeSeconds;
749
+ this.pullSinceReloadThreshold = pullSinceReloadThreshold;
750
+ }
751
+ runInTransaction(fn) {
752
+ if (typeof this.driver.transactionWithDriver === `function`) {
753
+ return this.driver.transactionWithDriver(fn);
754
+ }
755
+ return this.driver.transaction(fn);
756
+ }
757
+ async loadSubset(collectionId, options, ctx) {
758
+ const tableMapping = await this.ensureCollectionReady(collectionId);
759
+ await this.touchRequiredIndexes(collectionId, ctx?.requiredIndexSignatures);
760
+ if (options.cursor) {
761
+ const whereCurrentOptions = {
762
+ where: options.where ? new db.IR.Func(`and`, [options.where, options.cursor.whereCurrent]) : options.cursor.whereCurrent,
763
+ orderBy: options.orderBy
764
+ };
765
+ const whereFromOptions = {
766
+ where: options.where ? new db.IR.Func(`and`, [options.where, options.cursor.whereFrom]) : options.cursor.whereFrom,
767
+ orderBy: options.orderBy,
768
+ limit: options.limit
769
+ };
770
+ const [whereCurrentRows, whereFromRows] = await Promise.all([
771
+ this.loadSubsetInternal(tableMapping, whereCurrentOptions),
772
+ this.loadSubsetInternal(tableMapping, whereFromOptions)
773
+ ]);
774
+ const mergedRows = /* @__PURE__ */ new Map();
775
+ for (const row of [...whereCurrentRows, ...whereFromRows]) {
776
+ mergedRows.set(persisted.encodePersistedStorageKey(row.key), row);
777
+ }
778
+ const orderedRows = this.applyInMemoryOrderBy(
779
+ Array.from(mergedRows.values()),
780
+ options.orderBy
781
+ );
782
+ return orderedRows.map((row) => ({
783
+ key: row.key,
784
+ value: row.value,
785
+ metadata: row.metadata
786
+ }));
787
+ }
788
+ const rows = await this.loadSubsetInternal(tableMapping, options);
789
+ return rows.map((row) => ({
790
+ key: row.key,
791
+ value: row.value,
792
+ metadata: row.metadata
793
+ }));
794
+ }
795
+ async applyCommittedTx(collectionId, tx) {
796
+ const tableMapping = await this.ensureCollectionReady(collectionId);
797
+ const collectionTableSql = quoteIdentifier(tableMapping.tableName);
798
+ const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
799
+ await this.runInTransaction(async (transactionDriver) => {
800
+ const alreadyApplied = await transactionDriver.query(
801
+ `SELECT 1 AS applied
802
+ FROM applied_tx
803
+ WHERE collection_id = ? AND term = ? AND seq = ?
804
+ LIMIT 1`,
805
+ [collectionId, tx.term, tx.seq]
806
+ );
807
+ if (alreadyApplied.length > 0) {
808
+ return;
809
+ }
810
+ const versionRows = await transactionDriver.query(
811
+ `SELECT latest_row_version
812
+ FROM collection_version
813
+ WHERE collection_id = ?
814
+ LIMIT 1`,
815
+ [collectionId]
816
+ );
817
+ const currentRowVersion = versionRows[0]?.latest_row_version ?? 0;
818
+ const nextRowVersion = Math.max(currentRowVersion + 1, tx.rowVersion);
819
+ const replayDelta = tx.truncate ? null : {
820
+ txId: tx.txId,
821
+ latestRowVersion: nextRowVersion,
822
+ changedRows: tx.mutations.filter((mutation) => mutation.type !== `delete`).map((mutation) => ({
823
+ key: mutation.key,
824
+ value: mutation.value
825
+ })),
826
+ deletedKeys: tx.mutations.filter((mutation) => mutation.type === `delete`).map((mutation) => mutation.key),
827
+ rowMetadataMutations: tx.rowMetadataMutations ?? [],
828
+ collectionMetadataMutations: tx.collectionMetadataMutations ?? []
829
+ };
830
+ if (tx.truncate) {
831
+ await transactionDriver.run(`DELETE FROM ${collectionTableSql}`);
832
+ await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`);
833
+ }
834
+ for (const mutation of tx.mutations) {
835
+ const encodedKey = persisted.encodePersistedStorageKey(mutation.key);
836
+ if (mutation.type === `delete`) {
837
+ await transactionDriver.run(
838
+ `DELETE FROM ${collectionTableSql}
839
+ WHERE key = ?`,
840
+ [encodedKey]
841
+ );
842
+ await transactionDriver.run(
843
+ `INSERT INTO ${tombstoneTableSql} (key, value, row_version, deleted_at)
844
+ VALUES (?, ?, ?, ?)
845
+ ON CONFLICT(key) DO UPDATE SET
846
+ value = excluded.value,
847
+ row_version = excluded.row_version,
848
+ deleted_at = excluded.deleted_at`,
849
+ [
850
+ encodedKey,
851
+ serializePersistedRowValue(mutation.value),
852
+ nextRowVersion,
853
+ (/* @__PURE__ */ new Date()).toISOString()
854
+ ]
855
+ );
856
+ continue;
857
+ }
858
+ const existingRows = await transactionDriver.query(
859
+ `SELECT value, metadata
860
+ FROM ${collectionTableSql}
861
+ WHERE key = ?
862
+ LIMIT 1`,
863
+ [encodedKey]
864
+ );
865
+ const existingValue = existingRows[0]?.value ? deserializePersistedRowValue(existingRows[0].value) : void 0;
866
+ const existingMetadata = existingRows[0]?.metadata != null ? deserializePersistedRowValue(existingRows[0].metadata) : void 0;
867
+ const mergedValue = mutation.type === `update` ? mergeObjectRows(existingValue, mutation.value) : mutation.value;
868
+ const nextMetadata = mutation.metadataChanged === true ? mutation.metadata : existingMetadata;
869
+ await transactionDriver.run(
870
+ `INSERT INTO ${collectionTableSql} (key, value, metadata, row_version)
871
+ VALUES (?, ?, ?, ?)
872
+ ON CONFLICT(key) DO UPDATE SET
873
+ value = excluded.value,
874
+ metadata = excluded.metadata,
875
+ row_version = excluded.row_version`,
876
+ [
877
+ encodedKey,
878
+ serializePersistedRowValue(mergedValue),
879
+ nextMetadata === void 0 ? null : serializePersistedRowValue(nextMetadata),
880
+ nextRowVersion
881
+ ]
882
+ );
883
+ await transactionDriver.run(
884
+ `DELETE FROM ${tombstoneTableSql}
885
+ WHERE key = ?`,
886
+ [encodedKey]
887
+ );
888
+ }
889
+ for (const rowMetadataMutation of tx.rowMetadataMutations ?? []) {
890
+ const encodedKey = persisted.encodePersistedStorageKey(rowMetadataMutation.key);
891
+ if (rowMetadataMutation.type === `delete`) {
892
+ await transactionDriver.run(
893
+ `UPDATE ${collectionTableSql}
894
+ SET metadata = NULL
895
+ WHERE key = ?`,
896
+ [encodedKey]
897
+ );
898
+ } else {
899
+ await transactionDriver.run(
900
+ `UPDATE ${collectionTableSql}
901
+ SET metadata = ?
902
+ WHERE key = ?`,
903
+ [
904
+ rowMetadataMutation.value === void 0 ? null : serializePersistedRowValue(rowMetadataMutation.value),
905
+ encodedKey
906
+ ]
907
+ );
908
+ }
909
+ }
910
+ for (const metadataMutation of tx.collectionMetadataMutations ?? []) {
911
+ if (metadataMutation.type === `delete`) {
912
+ await transactionDriver.run(
913
+ `DELETE FROM collection_metadata
914
+ WHERE collection_id = ? AND key = ?`,
915
+ [collectionId, metadataMutation.key]
916
+ );
917
+ } else {
918
+ await transactionDriver.run(
919
+ `INSERT INTO collection_metadata (collection_id, key, value, updated_at)
920
+ VALUES (?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
921
+ ON CONFLICT(collection_id, key) DO UPDATE SET
922
+ value = excluded.value,
923
+ updated_at = excluded.updated_at`,
924
+ [
925
+ collectionId,
926
+ metadataMutation.key,
927
+ serializePersistedRowValue(metadataMutation.value)
928
+ ]
929
+ );
930
+ }
931
+ }
932
+ await transactionDriver.run(
933
+ `INSERT INTO collection_version (collection_id, latest_row_version)
934
+ VALUES (?, ?)
935
+ ON CONFLICT(collection_id) DO UPDATE SET
936
+ latest_row_version = excluded.latest_row_version`,
937
+ [collectionId, nextRowVersion]
938
+ );
939
+ await transactionDriver.run(
940
+ `INSERT INTO leader_term (collection_id, latest_term)
941
+ VALUES (?, ?)
942
+ ON CONFLICT(collection_id) DO UPDATE SET
943
+ latest_term = CASE
944
+ WHEN leader_term.latest_term > excluded.latest_term
945
+ THEN leader_term.latest_term
946
+ ELSE excluded.latest_term
947
+ END`,
948
+ [collectionId, tx.term]
949
+ );
950
+ await transactionDriver.run(
951
+ `INSERT INTO applied_tx (
952
+ collection_id,
953
+ term,
954
+ seq,
955
+ tx_id,
956
+ row_version,
957
+ replay_json,
958
+ replay_requires_full_reload,
959
+ applied_at
960
+ )
961
+ VALUES (?, ?, ?, ?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))`,
962
+ [
963
+ collectionId,
964
+ tx.term,
965
+ tx.seq,
966
+ tx.txId,
967
+ nextRowVersion,
968
+ replayDelta ? stableStringify(replayDelta) : null,
969
+ tx.truncate ? 1 : 0
970
+ ]
971
+ );
972
+ await this.pruneAppliedTxRows(collectionId, transactionDriver);
973
+ });
974
+ }
975
+ async loadCollectionMetadata(collectionId) {
976
+ const rows = await this.driver.query(
977
+ `SELECT key, value
978
+ FROM collection_metadata
979
+ WHERE collection_id = ?`,
980
+ [collectionId]
981
+ );
982
+ return rows.map((row) => ({
983
+ key: row.key,
984
+ value: deserializePersistedRowValue(row.value)
985
+ }));
986
+ }
987
+ async scanRows(collectionId, options) {
988
+ const tableMapping = await this.ensureCollectionReady(collectionId);
989
+ const collectionTableSql = quoteIdentifier(tableMapping.tableName);
990
+ const storedRows = await this.driver.query(
991
+ options?.metadataOnly ? `SELECT key, value, metadata, row_version
992
+ FROM ${collectionTableSql}
993
+ WHERE metadata IS NOT NULL` : `SELECT key, value, metadata, row_version
994
+ FROM ${collectionTableSql}`
995
+ );
996
+ return decodeStoredSqliteRows(storedRows).map((row) => ({
997
+ key: row.key,
998
+ value: row.value,
999
+ metadata: row.metadata
1000
+ }));
1001
+ }
1002
+ async ensureIndex(collectionId, signature, spec) {
1003
+ const tableMapping = await this.ensureCollectionReady(collectionId);
1004
+ const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1005
+ const indexName = buildIndexName(collectionId, signature);
1006
+ const indexNameSql = quoteIdentifier(indexName);
1007
+ const normalizedExpressionSql = spec.expressionSql.map(
1008
+ (fragment) => normalizeIndexSqlFragment(fragment)
1009
+ );
1010
+ const expressionSql = normalizedExpressionSql.join(`, `);
1011
+ const whereSql = spec.whereSql ? normalizeIndexSqlFragment(spec.whereSql) : void 0;
1012
+ await this.runInTransaction(async (transactionDriver) => {
1013
+ await transactionDriver.run(
1014
+ `INSERT INTO persisted_index_registry (
1015
+ collection_id,
1016
+ signature,
1017
+ index_name,
1018
+ expression_sql,
1019
+ where_sql,
1020
+ removed,
1021
+ created_at,
1022
+ updated_at,
1023
+ last_used_at
1024
+ )
1025
+ VALUES (?, ?, ?, ?, ?, 0,
1026
+ CAST(strftime('%s', 'now') AS INTEGER),
1027
+ CAST(strftime('%s', 'now') AS INTEGER),
1028
+ CAST(strftime('%s', 'now') AS INTEGER))
1029
+ ON CONFLICT(collection_id, signature) DO UPDATE SET
1030
+ index_name = excluded.index_name,
1031
+ expression_sql = excluded.expression_sql,
1032
+ where_sql = excluded.where_sql,
1033
+ removed = 0,
1034
+ updated_at = CAST(strftime('%s', 'now') AS INTEGER),
1035
+ last_used_at = CAST(strftime('%s', 'now') AS INTEGER)`,
1036
+ [
1037
+ collectionId,
1038
+ signature,
1039
+ indexName,
1040
+ JSON.stringify(normalizedExpressionSql),
1041
+ whereSql ?? null
1042
+ ]
1043
+ );
1044
+ const createIndexSql = whereSql ? `CREATE INDEX IF NOT EXISTS ${indexNameSql}
1045
+ ON ${collectionTableSql} (${expressionSql})
1046
+ WHERE ${whereSql}` : `CREATE INDEX IF NOT EXISTS ${indexNameSql}
1047
+ ON ${collectionTableSql} (${expressionSql})`;
1048
+ await transactionDriver.exec(createIndexSql);
1049
+ });
1050
+ }
1051
+ async markIndexRemoved(collectionId, signature) {
1052
+ await this.ensureCollectionReady(collectionId);
1053
+ const rows = await this.driver.query(
1054
+ `SELECT index_name
1055
+ FROM persisted_index_registry
1056
+ WHERE collection_id = ? AND signature = ?
1057
+ LIMIT 1`,
1058
+ [collectionId, signature]
1059
+ );
1060
+ const indexName = rows[0]?.index_name;
1061
+ await this.driver.run(
1062
+ `UPDATE persisted_index_registry
1063
+ SET removed = 1,
1064
+ updated_at = CAST(strftime('%s', 'now') AS INTEGER),
1065
+ last_used_at = CAST(strftime('%s', 'now') AS INTEGER)
1066
+ WHERE collection_id = ? AND signature = ?`,
1067
+ [collectionId, signature]
1068
+ );
1069
+ if (indexName) {
1070
+ await this.driver.exec(
1071
+ `DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`
1072
+ );
1073
+ }
1074
+ }
1075
+ async getStreamPosition(collectionId) {
1076
+ await this.ensureCollectionReady(collectionId);
1077
+ const [termRows, versionRows, seqRows] = await Promise.all([
1078
+ this.driver.query(
1079
+ `SELECT latest_term
1080
+ FROM leader_term
1081
+ WHERE collection_id = ?
1082
+ LIMIT 1`,
1083
+ [collectionId]
1084
+ ),
1085
+ this.driver.query(
1086
+ `SELECT latest_row_version
1087
+ FROM collection_version
1088
+ WHERE collection_id = ?
1089
+ LIMIT 1`,
1090
+ [collectionId]
1091
+ ),
1092
+ this.driver.query(
1093
+ `SELECT MAX(seq) AS max_seq
1094
+ FROM applied_tx
1095
+ WHERE collection_id = ? AND term = (
1096
+ SELECT latest_term FROM leader_term WHERE collection_id = ? LIMIT 1
1097
+ )`,
1098
+ [collectionId, collectionId]
1099
+ )
1100
+ ]);
1101
+ return {
1102
+ latestTerm: termRows[0]?.latest_term ?? 0,
1103
+ latestSeq: seqRows[0]?.max_seq ?? 0,
1104
+ latestRowVersion: versionRows[0]?.latest_row_version ?? 0
1105
+ };
1106
+ }
1107
+ async pullSince(collectionId, fromRowVersion) {
1108
+ const tableMapping = await this.ensureCollectionReady(collectionId);
1109
+ const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1110
+ const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
1111
+ const [changedRows, deletedRows, latestVersionRows, replayRows] = await Promise.all([
1112
+ this.driver.query(
1113
+ `SELECT key
1114
+ FROM ${collectionTableSql}
1115
+ WHERE row_version > ?`,
1116
+ [fromRowVersion]
1117
+ ),
1118
+ this.driver.query(
1119
+ `SELECT key
1120
+ FROM ${tombstoneTableSql}
1121
+ WHERE row_version > ?`,
1122
+ [fromRowVersion]
1123
+ ),
1124
+ this.driver.query(
1125
+ `SELECT latest_row_version
1126
+ FROM collection_version
1127
+ WHERE collection_id = ?
1128
+ LIMIT 1`,
1129
+ [collectionId]
1130
+ ),
1131
+ this.driver.query(
1132
+ `SELECT tx_id, row_version, replay_json, replay_requires_full_reload
1133
+ FROM applied_tx
1134
+ WHERE collection_id = ? AND row_version > ?
1135
+ ORDER BY term ASC, seq ASC`,
1136
+ [collectionId, fromRowVersion]
1137
+ )
1138
+ ]);
1139
+ const latestRowVersion = latestVersionRows[0]?.latest_row_version ?? 0;
1140
+ const changedKeyCount = changedRows.length + deletedRows.length;
1141
+ if (changedKeyCount > this.pullSinceReloadThreshold) {
1142
+ return {
1143
+ latestRowVersion,
1144
+ requiresFullReload: true
1145
+ };
1146
+ }
1147
+ if (replayRows.some(
1148
+ (row) => row.replay_requires_full_reload !== 0 || row.replay_json == null
1149
+ )) {
1150
+ return {
1151
+ latestRowVersion,
1152
+ requiresFullReload: true
1153
+ };
1154
+ }
1155
+ const decodeKey = (encodedKey) => {
1156
+ try {
1157
+ return persisted.decodePersistedStorageKey(encodedKey);
1158
+ } catch (error) {
1159
+ throw new errors.InvalidPersistedStorageKeyEncodingError(
1160
+ `${encodedKey}: ${error.message}`
1161
+ );
1162
+ }
1163
+ };
1164
+ const deltas = replayRows.map((row) => {
1165
+ const parsed = deserializePersistedRowValue(row.replay_json ?? `null`);
1166
+ if (!parsed) {
1167
+ throw new errors.InvalidPersistedCollectionConfigError(
1168
+ `missing replay payload for applied_tx row`
1169
+ );
1170
+ }
1171
+ return parsed;
1172
+ });
1173
+ const replayChangeCount = deltas.reduce(
1174
+ (count, delta) => count + delta.changedRows.length + delta.deletedKeys.length + delta.rowMetadataMutations.length + delta.collectionMetadataMutations.length,
1175
+ 0
1176
+ );
1177
+ if (replayChangeCount > this.pullSinceReloadThreshold) {
1178
+ return {
1179
+ latestRowVersion,
1180
+ requiresFullReload: true
1181
+ };
1182
+ }
1183
+ return {
1184
+ latestRowVersion,
1185
+ requiresFullReload: false,
1186
+ changedKeys: changedRows.map((row) => decodeKey(row.key)),
1187
+ deletedKeys: deletedRows.map((row) => decodeKey(row.key)),
1188
+ deltas
1189
+ };
1190
+ }
1191
+ async loadSubsetInternal(tableMapping, options) {
1192
+ const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1193
+ const whereCompiled = options.where ? compileSqlExpression(options.where) : { supported: true, sql: ``, params: [] };
1194
+ const orderByCompiled = compileOrderByClauses(options.orderBy);
1195
+ const queryParams = [];
1196
+ let sql = `SELECT key, value, metadata, row_version FROM ${collectionTableSql}`;
1197
+ if (options.where && whereCompiled.supported) {
1198
+ sql = `${sql} WHERE ${whereCompiled.sql}`;
1199
+ queryParams.push(...whereCompiled.params);
1200
+ }
1201
+ if (options.orderBy && orderByCompiled.supported) {
1202
+ sql = `${sql} ORDER BY ${orderByCompiled.sql}, key ASC`;
1203
+ queryParams.push(...orderByCompiled.params);
1204
+ }
1205
+ const storedRows = await this.driver.query(
1206
+ sql,
1207
+ queryParams
1208
+ );
1209
+ const parsedRows = decodeStoredSqliteRows(storedRows);
1210
+ const filteredRows = this.applyInMemoryWhere(parsedRows, options.where);
1211
+ const orderedRows = this.applyInMemoryOrderBy(filteredRows, options.orderBy);
1212
+ return this.applyInMemoryPagination(
1213
+ orderedRows,
1214
+ options.limit,
1215
+ options.offset
1216
+ );
1217
+ }
1218
+ applyInMemoryWhere(rows, where) {
1219
+ if (!where) {
1220
+ return rows;
1221
+ }
1222
+ const evaluator = compileRowExpressionEvaluator(where);
1223
+ return rows.filter(
1224
+ (row) => db.toBooleanPredicate(
1225
+ evaluator(row.value)
1226
+ )
1227
+ );
1228
+ }
1229
+ applyInMemoryOrderBy(rows, orderBy) {
1230
+ if (!orderBy || orderBy.length === 0) {
1231
+ return rows;
1232
+ }
1233
+ const compiledClauses = orderBy.map((clause) => ({
1234
+ evaluator: compileRowExpressionEvaluator(clause.expression),
1235
+ compareOptions: clause.compareOptions
1236
+ }));
1237
+ const ordered = [...rows];
1238
+ ordered.sort((left, right) => {
1239
+ for (const clause of compiledClauses) {
1240
+ const leftValue = clause.evaluator(
1241
+ left.value
1242
+ );
1243
+ const rightValue = clause.evaluator(
1244
+ right.value
1245
+ );
1246
+ const comparison = compareOrderByValues(
1247
+ leftValue,
1248
+ rightValue,
1249
+ clause.compareOptions
1250
+ );
1251
+ if (comparison !== 0) {
1252
+ return clause.compareOptions.direction === `desc` ? comparison * -1 : comparison;
1253
+ }
1254
+ }
1255
+ const leftKey = persisted.encodePersistedStorageKey(left.key);
1256
+ const rightKey = persisted.encodePersistedStorageKey(right.key);
1257
+ if (leftKey < rightKey) {
1258
+ return -1;
1259
+ }
1260
+ if (leftKey > rightKey) {
1261
+ return 1;
1262
+ }
1263
+ return 0;
1264
+ });
1265
+ return ordered;
1266
+ }
1267
+ applyInMemoryPagination(rows, limit, offset) {
1268
+ const start = offset ?? 0;
1269
+ if (limit === void 0) {
1270
+ return rows.slice(start);
1271
+ }
1272
+ return rows.slice(start, start + limit);
1273
+ }
1274
+ async touchRequiredIndexes(collectionId, requiredIndexSignatures) {
1275
+ if (!requiredIndexSignatures || requiredIndexSignatures.length === 0) {
1276
+ return;
1277
+ }
1278
+ for (const signature of requiredIndexSignatures) {
1279
+ await this.driver.run(
1280
+ `UPDATE persisted_index_registry
1281
+ SET last_used_at = CAST(strftime('%s', 'now') AS INTEGER),
1282
+ updated_at = CAST(strftime('%s', 'now') AS INTEGER)
1283
+ WHERE collection_id = ? AND signature = ? AND removed = 0`,
1284
+ [collectionId, signature]
1285
+ );
1286
+ }
1287
+ }
1288
+ async pruneAppliedTxRows(collectionId, driver = this.driver) {
1289
+ if (this.appliedTxPruneMaxAgeSeconds !== void 0 && this.appliedTxPruneMaxAgeSeconds > 0) {
1290
+ await driver.run(
1291
+ `DELETE FROM applied_tx
1292
+ WHERE collection_id = ?
1293
+ AND applied_at < (CAST(strftime('%s', 'now') AS INTEGER) - ?)`,
1294
+ [collectionId, this.appliedTxPruneMaxAgeSeconds]
1295
+ );
1296
+ }
1297
+ if (this.appliedTxPruneMaxRows === void 0 || this.appliedTxPruneMaxRows <= 0) {
1298
+ return;
1299
+ }
1300
+ const countRows = await driver.query(
1301
+ `SELECT COUNT(*) AS count
1302
+ FROM applied_tx
1303
+ WHERE collection_id = ?`,
1304
+ [collectionId]
1305
+ );
1306
+ const count = countRows[0]?.count ?? 0;
1307
+ const excessRows = count - this.appliedTxPruneMaxRows;
1308
+ if (excessRows <= 0) {
1309
+ return;
1310
+ }
1311
+ await driver.run(
1312
+ `DELETE FROM applied_tx
1313
+ WHERE rowid IN (
1314
+ SELECT rowid
1315
+ FROM applied_tx
1316
+ WHERE collection_id = ?
1317
+ ORDER BY term ASC, seq ASC
1318
+ LIMIT ?
1319
+ )`,
1320
+ [collectionId, excessRows]
1321
+ );
1322
+ }
1323
+ async ensureCollectionReady(collectionId) {
1324
+ await this.ensureInitialized();
1325
+ const cached = this.collectionTableCache.get(collectionId);
1326
+ if (cached) {
1327
+ return cached;
1328
+ }
1329
+ const inFlight = this.collectionTableLoads.get(collectionId);
1330
+ if (inFlight) {
1331
+ return inFlight;
1332
+ }
1333
+ const loadPromise = this.ensureCollectionReadyInternal(collectionId);
1334
+ this.collectionTableLoads.set(collectionId, loadPromise);
1335
+ try {
1336
+ return await loadPromise;
1337
+ } finally {
1338
+ this.collectionTableLoads.delete(collectionId);
1339
+ }
1340
+ }
1341
+ async ensureCollectionReadyInternal(collectionId) {
1342
+ const existingRows = await this.driver.query(
1343
+ `SELECT table_name, tombstone_table_name, schema_version
1344
+ FROM collection_registry
1345
+ WHERE collection_id = ?
1346
+ LIMIT 1`,
1347
+ [collectionId]
1348
+ );
1349
+ let tableName;
1350
+ let tombstoneTableName;
1351
+ if (existingRows.length > 0) {
1352
+ tableName = existingRows[0].table_name;
1353
+ tombstoneTableName = existingRows[0].tombstone_table_name;
1354
+ if (existingRows[0].schema_version !== this.schemaVersion) {
1355
+ await this.handleSchemaMismatch(
1356
+ collectionId,
1357
+ existingRows[0].schema_version,
1358
+ this.schemaVersion,
1359
+ tableName,
1360
+ tombstoneTableName
1361
+ );
1362
+ }
1363
+ } else {
1364
+ tableName = persisted.createPersistedTableName(collectionId, `c`);
1365
+ tombstoneTableName = persisted.createPersistedTableName(collectionId, `t`);
1366
+ await this.driver.run(
1367
+ `INSERT INTO collection_registry (
1368
+ collection_id,
1369
+ table_name,
1370
+ tombstone_table_name,
1371
+ schema_version,
1372
+ updated_at
1373
+ )
1374
+ VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))`,
1375
+ [collectionId, tableName, tombstoneTableName, this.schemaVersion]
1376
+ );
1377
+ }
1378
+ const collectionTableSql = quoteIdentifier(tableName);
1379
+ const tombstoneTableSql = quoteIdentifier(tombstoneTableName);
1380
+ await this.driver.exec(
1381
+ `CREATE TABLE IF NOT EXISTS ${collectionTableSql} (
1382
+ key TEXT PRIMARY KEY,
1383
+ value TEXT NOT NULL,
1384
+ metadata TEXT,
1385
+ row_version INTEGER NOT NULL
1386
+ )`
1387
+ );
1388
+ await this.driver.exec(
1389
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${tableName}_row_version_idx`)}
1390
+ ON ${collectionTableSql} (row_version)`
1391
+ );
1392
+ await this.driver.exec(
1393
+ `CREATE TABLE IF NOT EXISTS ${tombstoneTableSql} (
1394
+ key TEXT PRIMARY KEY,
1395
+ value TEXT,
1396
+ row_version INTEGER NOT NULL,
1397
+ deleted_at TEXT NOT NULL
1398
+ )`
1399
+ );
1400
+ await this.driver.exec(
1401
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${tombstoneTableName}_row_version_idx`)}
1402
+ ON ${tombstoneTableSql} (row_version)`
1403
+ );
1404
+ await this.driver.run(
1405
+ `INSERT INTO collection_version (collection_id, latest_row_version)
1406
+ VALUES (?, 0)
1407
+ ON CONFLICT(collection_id) DO NOTHING`,
1408
+ [collectionId]
1409
+ );
1410
+ await this.driver.run(
1411
+ `INSERT INTO collection_reset_epoch (collection_id, reset_epoch, updated_at)
1412
+ VALUES (?, 0, CAST(strftime('%s', 'now') AS INTEGER))
1413
+ ON CONFLICT(collection_id) DO NOTHING`,
1414
+ [collectionId]
1415
+ );
1416
+ const mapping = {
1417
+ tableName,
1418
+ tombstoneTableName
1419
+ };
1420
+ this.collectionTableCache.set(collectionId, mapping);
1421
+ return mapping;
1422
+ }
1423
+ async handleSchemaMismatch(collectionId, previousSchemaVersion, nextSchemaVersion, tableName, tombstoneTableName) {
1424
+ if (this.schemaMismatchPolicy === `sync-absent-error`) {
1425
+ throw new errors.InvalidPersistedCollectionConfigError(
1426
+ `Schema version mismatch for collection "${collectionId}": found ${previousSchemaVersion}, expected ${nextSchemaVersion}. Set schemaMismatchPolicy to "sync-present-reset" or "reset" to allow automatic reset.`
1427
+ );
1428
+ }
1429
+ const collectionTableSql = quoteIdentifier(tableName);
1430
+ const tombstoneTableSql = quoteIdentifier(tombstoneTableName);
1431
+ await this.runInTransaction(async (transactionDriver) => {
1432
+ const persistedIndexes = await transactionDriver.query(
1433
+ `SELECT index_name
1434
+ FROM persisted_index_registry
1435
+ WHERE collection_id = ?`,
1436
+ [collectionId]
1437
+ );
1438
+ for (const row of persistedIndexes) {
1439
+ await transactionDriver.exec(
1440
+ `DROP INDEX IF EXISTS ${quoteIdentifier(row.index_name)}`
1441
+ );
1442
+ }
1443
+ await transactionDriver.run(`DELETE FROM ${collectionTableSql}`);
1444
+ await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`);
1445
+ await transactionDriver.run(
1446
+ `DELETE FROM applied_tx
1447
+ WHERE collection_id = ?`,
1448
+ [collectionId]
1449
+ );
1450
+ await transactionDriver.run(
1451
+ `DELETE FROM persisted_index_registry
1452
+ WHERE collection_id = ?`,
1453
+ [collectionId]
1454
+ );
1455
+ await transactionDriver.run(
1456
+ `UPDATE collection_registry
1457
+ SET schema_version = ?,
1458
+ updated_at = CAST(strftime('%s', 'now') AS INTEGER)
1459
+ WHERE collection_id = ?`,
1460
+ [nextSchemaVersion, collectionId]
1461
+ );
1462
+ await transactionDriver.run(
1463
+ `INSERT INTO collection_version (collection_id, latest_row_version)
1464
+ VALUES (?, 0)
1465
+ ON CONFLICT(collection_id) DO UPDATE SET
1466
+ latest_row_version = 0`,
1467
+ [collectionId]
1468
+ );
1469
+ await transactionDriver.run(
1470
+ `INSERT INTO collection_reset_epoch (collection_id, reset_epoch, updated_at)
1471
+ VALUES (?, 1, CAST(strftime('%s', 'now') AS INTEGER))
1472
+ ON CONFLICT(collection_id) DO UPDATE SET
1473
+ reset_epoch = collection_reset_epoch.reset_epoch + 1,
1474
+ updated_at = CAST(strftime('%s', 'now') AS INTEGER)`,
1475
+ [collectionId]
1476
+ );
1477
+ });
1478
+ }
1479
+ async ensureInitialized() {
1480
+ if (this.initialized) {
1481
+ return;
1482
+ }
1483
+ await this.driver.exec(
1484
+ `CREATE TABLE IF NOT EXISTS collection_registry (
1485
+ collection_id TEXT PRIMARY KEY,
1486
+ table_name TEXT NOT NULL UNIQUE,
1487
+ tombstone_table_name TEXT NOT NULL UNIQUE,
1488
+ schema_version INTEGER NOT NULL,
1489
+ updated_at INTEGER NOT NULL
1490
+ )`
1491
+ );
1492
+ await this.driver.exec(
1493
+ `CREATE TABLE IF NOT EXISTS persisted_index_registry (
1494
+ collection_id TEXT NOT NULL,
1495
+ signature TEXT NOT NULL,
1496
+ index_name TEXT NOT NULL,
1497
+ expression_sql TEXT NOT NULL,
1498
+ where_sql TEXT,
1499
+ removed INTEGER NOT NULL DEFAULT 0,
1500
+ created_at INTEGER NOT NULL,
1501
+ updated_at INTEGER NOT NULL,
1502
+ last_used_at INTEGER NOT NULL,
1503
+ PRIMARY KEY (collection_id, signature)
1504
+ )`
1505
+ );
1506
+ await this.driver.exec(
1507
+ `CREATE TABLE IF NOT EXISTS applied_tx (
1508
+ collection_id TEXT NOT NULL,
1509
+ term INTEGER NOT NULL,
1510
+ seq INTEGER NOT NULL,
1511
+ tx_id TEXT NOT NULL,
1512
+ row_version INTEGER NOT NULL,
1513
+ replay_json TEXT,
1514
+ replay_requires_full_reload INTEGER NOT NULL DEFAULT 0,
1515
+ applied_at INTEGER NOT NULL,
1516
+ PRIMARY KEY (collection_id, term, seq)
1517
+ )`
1518
+ );
1519
+ try {
1520
+ await this.driver.exec(
1521
+ `ALTER TABLE applied_tx ADD COLUMN replay_json TEXT`
1522
+ );
1523
+ } catch (error) {
1524
+ if (!isDuplicateColumnAddError(error, `replay_json`)) {
1525
+ throw error;
1526
+ }
1527
+ }
1528
+ try {
1529
+ await this.driver.exec(
1530
+ `ALTER TABLE applied_tx ADD COLUMN replay_requires_full_reload INTEGER NOT NULL DEFAULT 0`
1531
+ );
1532
+ } catch (error) {
1533
+ if (!isDuplicateColumnAddError(error, `replay_requires_full_reload`)) {
1534
+ throw error;
1535
+ }
1536
+ }
1537
+ await this.driver.exec(
1538
+ `CREATE TABLE IF NOT EXISTS collection_version (
1539
+ collection_id TEXT PRIMARY KEY,
1540
+ latest_row_version INTEGER NOT NULL
1541
+ )`
1542
+ );
1543
+ await this.driver.exec(
1544
+ `CREATE TABLE IF NOT EXISTS collection_metadata (
1545
+ collection_id TEXT NOT NULL,
1546
+ key TEXT NOT NULL,
1547
+ value TEXT NOT NULL,
1548
+ updated_at INTEGER NOT NULL,
1549
+ PRIMARY KEY (collection_id, key)
1550
+ )`
1551
+ );
1552
+ await this.driver.exec(
1553
+ `CREATE TABLE IF NOT EXISTS leader_term (
1554
+ collection_id TEXT PRIMARY KEY,
1555
+ latest_term INTEGER NOT NULL
1556
+ )`
1557
+ );
1558
+ await this.driver.exec(
1559
+ `CREATE TABLE IF NOT EXISTS schema_version (
1560
+ scope TEXT PRIMARY KEY,
1561
+ version INTEGER NOT NULL,
1562
+ updated_at INTEGER NOT NULL
1563
+ )`
1564
+ );
1565
+ await this.driver.run(
1566
+ `INSERT INTO schema_version (scope, version, updated_at)
1567
+ VALUES ('global', ?, CAST(strftime('%s', 'now') AS INTEGER))
1568
+ ON CONFLICT(scope) DO UPDATE SET
1569
+ version = excluded.version,
1570
+ updated_at = excluded.updated_at`,
1571
+ [this.schemaVersion]
1572
+ );
1573
+ await this.driver.exec(
1574
+ `CREATE TABLE IF NOT EXISTS collection_reset_epoch (
1575
+ collection_id TEXT PRIMARY KEY,
1576
+ reset_epoch INTEGER NOT NULL,
1577
+ updated_at INTEGER NOT NULL
1578
+ )`
1579
+ );
1580
+ this.initialized = true;
1581
+ }
1582
+ }
1583
+ function createSQLiteCorePersistenceAdapter(options) {
1584
+ return new SQLiteCorePersistenceAdapter(options);
1585
+ }
1586
+ exports.SQLiteCorePersistenceAdapter = SQLiteCorePersistenceAdapter;
1587
+ exports.createSQLiteCorePersistenceAdapter = createSQLiteCorePersistenceAdapter;
1588
+ //# sourceMappingURL=sqlite-core-adapter.cjs.map