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