@tanstack/db-sqlite-persistence-core 0.2.23 → 0.4.1
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.
- package/README.md +258 -7
- package/dist/cjs/broadcast-coordinator.cjs +1689 -0
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -0
- package/dist/cjs/broadcast-coordinator.d.cts +116 -0
- package/dist/cjs/errors.cjs +73 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +44 -0
- package/dist/cjs/index.cjs +19 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/persisted.cjs +1782 -394
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +176 -11
- package/dist/cjs/remote-subset-owner.cjs +20 -0
- package/dist/cjs/remote-subset-owner.cjs.map +1 -0
- package/dist/cjs/remote-subset-owner.d.cts +7 -0
- package/dist/cjs/remote-subset-wire.cjs +681 -0
- package/dist/cjs/remote-subset-wire.cjs.map +1 -0
- package/dist/cjs/remote-subset-wire.d.cts +59 -0
- package/dist/cjs/sqlite-core-adapter.cjs +969 -364
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +45 -1
- package/dist/cjs/sqlite-value.cjs +31 -0
- package/dist/cjs/sqlite-value.cjs.map +1 -0
- package/dist/cjs/sqlite-value.d.cts +10 -0
- package/dist/esm/broadcast-coordinator.d.ts +116 -0
- package/dist/esm/broadcast-coordinator.js +1689 -0
- package/dist/esm/broadcast-coordinator.js.map +1 -0
- package/dist/esm/errors.d.ts +44 -0
- package/dist/esm/errors.js +74 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +21 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/persisted.d.ts +176 -11
- package/dist/esm/persisted.js +1784 -396
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-owner.d.ts +7 -0
- package/dist/esm/remote-subset-owner.js +20 -0
- package/dist/esm/remote-subset-owner.js.map +1 -0
- package/dist/esm/remote-subset-wire.d.ts +59 -0
- package/dist/esm/remote-subset-wire.js +681 -0
- package/dist/esm/remote-subset-wire.js.map +1 -0
- package/dist/esm/sqlite-core-adapter.d.ts +45 -1
- package/dist/esm/sqlite-core-adapter.js +955 -350
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/dist/esm/sqlite-value.d.ts +10 -0
- package/dist/esm/sqlite-value.js +31 -0
- package/dist/esm/sqlite-value.js.map +1 -0
- package/package.json +12 -2
- package/src/broadcast-coordinator.ts +2456 -0
- package/src/errors.ts +120 -0
- package/src/index.ts +2 -0
- package/src/persisted.ts +2617 -442
- package/src/remote-subset-owner.ts +36 -0
- package/src/remote-subset-wire.ts +946 -0
- package/src/sqlite-core-adapter.ts +1297 -410
- package/src/sqlite-value.ts +30 -0
|
@@ -1,6 +1,108 @@
|
|
|
1
1
|
import { IR, toBooleanPredicate, compileSingleRowExpression } from "@tanstack/db";
|
|
2
2
|
import { InvalidPersistedCollectionConfigError, InvalidPersistedStorageKeyEncodingError } from "./errors.js";
|
|
3
|
-
import { encodePersistedStorageKey, createPersistedTableName, decodePersistedStorageKey } from "./persisted.js";
|
|
3
|
+
import { encodePersistedStorageKey, createPersistedTableName, decodePersistedStorageKey, SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY } from "./persisted.js";
|
|
4
|
+
import { PERSISTED_TYPE_TAG, PERSISTED_VALUE_TAG, serializeSQLiteBigInt, assertSQLiteBigIntInRange } from "./sqlite-value.js";
|
|
5
|
+
const REPLACEMENT_BATCH_SIZE = 100;
|
|
6
|
+
class SharedPersistenceScheduler {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.regularQueue = [];
|
|
9
|
+
this.hydrateQueue = [];
|
|
10
|
+
this.running = false;
|
|
11
|
+
}
|
|
12
|
+
runRegular(task) {
|
|
13
|
+
return this.enqueue(`regular`, task);
|
|
14
|
+
}
|
|
15
|
+
runHydrate(task) {
|
|
16
|
+
return this.enqueue(`hydrate`, task);
|
|
17
|
+
}
|
|
18
|
+
adoptRunningHydrate(completion) {
|
|
19
|
+
if (this.running) return;
|
|
20
|
+
this.running = true;
|
|
21
|
+
const finish = () => {
|
|
22
|
+
this.lastCompletedKind = `hydrate`;
|
|
23
|
+
this.running = false;
|
|
24
|
+
this.drain();
|
|
25
|
+
};
|
|
26
|
+
void completion.then(finish, finish);
|
|
27
|
+
}
|
|
28
|
+
enqueue(kind, task) {
|
|
29
|
+
const result = new Promise((resolve, reject) => {
|
|
30
|
+
const operation = {
|
|
31
|
+
kind,
|
|
32
|
+
task,
|
|
33
|
+
resolve,
|
|
34
|
+
reject
|
|
35
|
+
};
|
|
36
|
+
const queue = kind === `hydrate` ? this.hydrateQueue : this.regularQueue;
|
|
37
|
+
queue.push(operation);
|
|
38
|
+
});
|
|
39
|
+
this.drain();
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
drain() {
|
|
43
|
+
if (this.running) return;
|
|
44
|
+
const operation = this.takeNext();
|
|
45
|
+
if (!operation) return;
|
|
46
|
+
this.running = true;
|
|
47
|
+
void this.execute(operation);
|
|
48
|
+
}
|
|
49
|
+
async execute(operation) {
|
|
50
|
+
try {
|
|
51
|
+
operation.resolve(await operation.task());
|
|
52
|
+
} catch (error) {
|
|
53
|
+
operation.reject(error);
|
|
54
|
+
} finally {
|
|
55
|
+
this.lastCompletedKind = operation.kind;
|
|
56
|
+
this.running = false;
|
|
57
|
+
this.drain();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
takeNext() {
|
|
61
|
+
if (this.hydrateQueue.length > 0) {
|
|
62
|
+
if (this.regularQueue.length > 0 && this.lastCompletedKind === `hydrate`) {
|
|
63
|
+
return this.regularQueue.shift();
|
|
64
|
+
}
|
|
65
|
+
return this.hydrateQueue.shift();
|
|
66
|
+
}
|
|
67
|
+
return this.regularQueue.shift();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const sharedPersistenceSchedulers = /* @__PURE__ */ new WeakMap();
|
|
71
|
+
const observedDriverSchedulingKeys = /* @__PURE__ */ new WeakMap();
|
|
72
|
+
function getSharedPersistenceScheduler(key) {
|
|
73
|
+
let scheduler = sharedPersistenceSchedulers.get(key);
|
|
74
|
+
if (!scheduler) {
|
|
75
|
+
scheduler = new SharedPersistenceScheduler();
|
|
76
|
+
sharedPersistenceSchedulers.set(key, scheduler);
|
|
77
|
+
}
|
|
78
|
+
return scheduler;
|
|
79
|
+
}
|
|
80
|
+
function getSharedLogicalSchedulingKey(value) {
|
|
81
|
+
if (typeof value !== `object` && typeof value !== `function` || !value) {
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
const key = value[SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY];
|
|
85
|
+
return key !== null && (typeof key === `object` || typeof key === `function`) ? key : void 0;
|
|
86
|
+
}
|
|
87
|
+
function observeSharedLogicalSchedulingSupport(driver, onSupport) {
|
|
88
|
+
let observationPending = true;
|
|
89
|
+
const observe = (promise) => {
|
|
90
|
+
if (!observationPending) return promise;
|
|
91
|
+
observationPending = false;
|
|
92
|
+
const key = getSharedLogicalSchedulingKey(promise);
|
|
93
|
+
if (key) onSupport(key);
|
|
94
|
+
return promise;
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
exec: (sql) => observe(driver.exec(sql)),
|
|
98
|
+
query: (sql, params = []) => observe(driver.query(sql, params)),
|
|
99
|
+
run: (sql, params = []) => observe(driver.run(sql, params)),
|
|
100
|
+
transaction: (fn) => observe(driver.transaction(fn)),
|
|
101
|
+
transactionWithDriver: (fn) => observe(
|
|
102
|
+
driver.transactionWithDriver ? driver.transactionWithDriver(fn) : driver.transaction(fn)
|
|
103
|
+
)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
4
106
|
const DEFAULT_SCHEMA_VERSION = 1;
|
|
5
107
|
const DEFAULT_PULL_SINCE_RELOAD_THRESHOLD = 128;
|
|
6
108
|
const DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS = 1e3;
|
|
@@ -8,8 +110,6 @@ const DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
|
8
110
|
const SQLITE_MAX_IN_BATCH_SIZE = 900;
|
|
9
111
|
const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
10
112
|
const FORBIDDEN_SQL_FRAGMENT_PATTERN = /(;|--|\/\*)/;
|
|
11
|
-
const PERSISTED_TYPE_TAG = `__tanstack_db_persisted_type__`;
|
|
12
|
-
const PERSISTED_VALUE_TAG = `value`;
|
|
13
113
|
const persistedTaggedValueTypes = /* @__PURE__ */ new Set([
|
|
14
114
|
`bigint`,
|
|
15
115
|
`date`,
|
|
@@ -20,12 +120,11 @@ const persistedTaggedValueTypes = /* @__PURE__ */ new Set([
|
|
|
20
120
|
const orderByObjectIds = /* @__PURE__ */ new WeakMap();
|
|
21
121
|
let nextOrderByObjectId = 1;
|
|
22
122
|
function isDuplicateColumnAddError(error, columnName) {
|
|
23
|
-
if (!(error instanceof Error)) {
|
|
123
|
+
if (typeof error !== `string` && !(error instanceof Error)) {
|
|
24
124
|
return false;
|
|
25
125
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
return message.includes(`duplicate column name`) && message.includes(normalizedColumnName) || message.includes(`already exists`) && message.includes(normalizedColumnName);
|
|
126
|
+
const normalizedMessage = (typeof error === `string` ? error : error.message).toLowerCase();
|
|
127
|
+
return normalizedMessage.includes(columnName.toLowerCase()) && (normalizedMessage.includes(`duplicate column name`) || normalizedMessage.includes(`already exists`));
|
|
29
128
|
}
|
|
30
129
|
function quoteIdentifier(identifier) {
|
|
31
130
|
if (!SAFE_IDENTIFIER_PATTERN.test(identifier)) {
|
|
@@ -52,10 +151,7 @@ function encodePersistedJsonValue(value) {
|
|
|
52
151
|
return value;
|
|
53
152
|
}
|
|
54
153
|
if (typeof value === `bigint`) {
|
|
55
|
-
return
|
|
56
|
-
[PERSISTED_TYPE_TAG]: `bigint`,
|
|
57
|
-
[PERSISTED_VALUE_TAG]: value.toString()
|
|
58
|
-
};
|
|
154
|
+
return serializeSQLiteBigInt(value);
|
|
59
155
|
}
|
|
60
156
|
if (value instanceof Date) {
|
|
61
157
|
return {
|
|
@@ -153,6 +249,7 @@ function toSqliteParameterValue(value) {
|
|
|
153
249
|
return value;
|
|
154
250
|
}
|
|
155
251
|
if (typeof value === `bigint`) {
|
|
252
|
+
assertSQLiteBigIntInRange(value);
|
|
156
253
|
return value.toString();
|
|
157
254
|
}
|
|
158
255
|
if (typeof value === `boolean`) {
|
|
@@ -175,56 +272,11 @@ function toSqliteLiteral(value) {
|
|
|
175
272
|
}
|
|
176
273
|
return `'${value.replace(/'/g, `''`)}'`;
|
|
177
274
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const paramValue = params[index];
|
|
182
|
-
index++;
|
|
183
|
-
return toSqliteLiteral(paramValue ?? null);
|
|
184
|
-
});
|
|
185
|
-
if (index !== params.length) {
|
|
186
|
-
throw new 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
|
-
}
|
|
275
|
+
function toSqliteExpressionLiteral(value) {
|
|
276
|
+
if (typeof value === `bigint`) {
|
|
277
|
+
return assertSQLiteBigIntInRange(value).toString();
|
|
206
278
|
}
|
|
207
|
-
return
|
|
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
|
-
});
|
|
279
|
+
return toSqliteLiteral(toSqliteParameterValue(value));
|
|
228
280
|
}
|
|
229
281
|
function compileRowExpressionEvaluator(expression) {
|
|
230
282
|
let baseEvaluator;
|
|
@@ -235,19 +287,7 @@ function compileRowExpressionEvaluator(expression) {
|
|
|
235
287
|
`Unsupported expression for SQLite adapter fallback evaluator: ${error.message}`
|
|
236
288
|
);
|
|
237
289
|
}
|
|
238
|
-
|
|
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
|
-
};
|
|
290
|
+
return baseEvaluator;
|
|
251
291
|
}
|
|
252
292
|
function getOrderByObjectId(value) {
|
|
253
293
|
const existing = orderByObjectIds.get(value);
|
|
@@ -270,6 +310,9 @@ function compareOrderByValues(left, right, compareOptions) {
|
|
|
270
310
|
return compareOptions.nulls === `first` ? 1 : -1;
|
|
271
311
|
}
|
|
272
312
|
if (typeof left === `string` && typeof right === `string`) {
|
|
313
|
+
if (compareOptions.stringSort === `custom`) {
|
|
314
|
+
return compareOptions.compare(left, right);
|
|
315
|
+
}
|
|
273
316
|
if (compareOptions.stringSort === `locale`) {
|
|
274
317
|
return left.localeCompare(
|
|
275
318
|
right,
|
|
@@ -364,32 +407,34 @@ function resolveComparisonValueKind(leftExpression, rightExpression, leftCompile
|
|
|
364
407
|
}
|
|
365
408
|
return `unknown`;
|
|
366
409
|
}
|
|
367
|
-
function compileComparisonSql(operator, leftSql, rightSql, valueKind) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
return `(${leftSql} ${operator} ${rightSql})`;
|
|
410
|
+
function compileComparisonSql(operator, leftExpression, rightExpression, leftSql, rightSql, valueKind, leftKind, rightKind) {
|
|
411
|
+
const compileOperand = (expression, sql, otherKind) => {
|
|
412
|
+
if (expression.type !== `val`) return sql;
|
|
413
|
+
if (valueKind === `date` && otherKind === `date`) return `date(${sql})`;
|
|
414
|
+
if (valueKind === `datetime` && otherKind === `datetime`) {
|
|
415
|
+
return `datetime(${sql})`;
|
|
416
|
+
}
|
|
417
|
+
return sql;
|
|
418
|
+
};
|
|
419
|
+
return `(${compileOperand(leftExpression, leftSql, rightKind)} ${operator} ${compileOperand(rightExpression, rightSql, leftKind)})`;
|
|
378
420
|
}
|
|
379
421
|
function compileRefExpressionSql(jsonPath) {
|
|
380
422
|
const typePath = `${jsonPath}.${PERSISTED_TYPE_TAG}`;
|
|
381
423
|
const taggedValuePath = `${jsonPath}.${PERSISTED_VALUE_TAG}`;
|
|
424
|
+
const typePathSql = toSqliteLiteral(typePath);
|
|
425
|
+
const taggedValuePathSql = toSqliteLiteral(taggedValuePath);
|
|
426
|
+
const jsonPathSql = toSqliteLiteral(jsonPath);
|
|
382
427
|
return {
|
|
383
428
|
supported: true,
|
|
384
|
-
sql: `(CASE json_extract(value,
|
|
385
|
-
WHEN 'bigint' THEN CAST(json_extract(value,
|
|
386
|
-
WHEN 'date' THEN json_extract(value,
|
|
429
|
+
sql: `(CASE json_extract(value, ${typePathSql})
|
|
430
|
+
WHEN 'bigint' THEN CAST(json_extract(value, ${taggedValuePathSql}) AS NUMERIC)
|
|
431
|
+
WHEN 'date' THEN json_extract(value, ${taggedValuePathSql})
|
|
387
432
|
WHEN 'nan' THEN NULL
|
|
388
433
|
WHEN 'infinity' THEN NULL
|
|
389
434
|
WHEN '-infinity' THEN NULL
|
|
390
|
-
ELSE json_extract(value,
|
|
435
|
+
ELSE json_extract(value, ${jsonPathSql})
|
|
391
436
|
END)`,
|
|
392
|
-
params: [
|
|
437
|
+
params: [],
|
|
393
438
|
valueKind: `unknown`
|
|
394
439
|
};
|
|
395
440
|
}
|
|
@@ -416,18 +461,45 @@ function decodeStoredSqliteRows(storedRows) {
|
|
|
416
461
|
function stableStringify(value) {
|
|
417
462
|
return serializePersistedRowValue(value);
|
|
418
463
|
}
|
|
419
|
-
function
|
|
464
|
+
function argumentCompilationContext(parentName, argumentIndex, argument, parentContext) {
|
|
465
|
+
if (parentContext === `index-expression`) return `index-expression`;
|
|
466
|
+
switch (parentName) {
|
|
467
|
+
case `and`:
|
|
468
|
+
case `or`:
|
|
469
|
+
case `not`:
|
|
470
|
+
return `predicate`;
|
|
471
|
+
case `eq`:
|
|
472
|
+
case `gt`:
|
|
473
|
+
case `gte`:
|
|
474
|
+
case `lt`:
|
|
475
|
+
case `lte`:
|
|
476
|
+
case `like`:
|
|
477
|
+
case `ilike`:
|
|
478
|
+
if (argument.type !== `val`) return `index-expression`;
|
|
479
|
+
return typeof argument.value === `bigint` ? `index-expression` : `predicate`;
|
|
480
|
+
case `in`:
|
|
481
|
+
return argumentIndex === 0 ? `index-expression` : `predicate`;
|
|
482
|
+
case `isNull`:
|
|
483
|
+
case `isUndefined`:
|
|
484
|
+
return `index-expression`;
|
|
485
|
+
default:
|
|
486
|
+
return `index-expression`;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function compileSqlExpression(expression, context = `predicate`) {
|
|
420
490
|
if (expression.type === `val`) {
|
|
421
491
|
const valueKind = getLiteralValueKind(expression.value);
|
|
422
492
|
return {
|
|
423
493
|
supported: true,
|
|
424
|
-
sql: `?`,
|
|
425
|
-
params: [toSqliteParameterValue(expression.value)],
|
|
494
|
+
sql: context === `index-expression` ? toSqliteExpressionLiteral(expression.value) : `?`,
|
|
495
|
+
params: context === `predicate` ? [toSqliteParameterValue(expression.value)] : [],
|
|
426
496
|
valueKind
|
|
427
497
|
};
|
|
428
498
|
}
|
|
429
499
|
if (expression.type === `ref`) {
|
|
430
|
-
const jsonPath = createJsonPath(
|
|
500
|
+
const jsonPath = createJsonPath(
|
|
501
|
+
IR.getPropRefPropertyPath(expression).map(String)
|
|
502
|
+
);
|
|
431
503
|
if (!jsonPath) {
|
|
432
504
|
return {
|
|
433
505
|
supported: false,
|
|
@@ -437,7 +509,12 @@ function compileSqlExpression(expression) {
|
|
|
437
509
|
}
|
|
438
510
|
return compileRefExpressionSql(jsonPath);
|
|
439
511
|
}
|
|
440
|
-
const compiledArgs = expression.args.map(
|
|
512
|
+
const compiledArgs = expression.args.map(
|
|
513
|
+
(arg, index) => compileSqlExpression(
|
|
514
|
+
arg,
|
|
515
|
+
argumentCompilationContext(expression.name, index, arg, context)
|
|
516
|
+
)
|
|
517
|
+
);
|
|
441
518
|
if (compiledArgs.some((arg) => !arg.supported)) {
|
|
442
519
|
return {
|
|
443
520
|
supported: false,
|
|
@@ -473,9 +550,13 @@ function compileSqlExpression(expression) {
|
|
|
473
550
|
supported: true,
|
|
474
551
|
sql: compileComparisonSql(
|
|
475
552
|
operatorByName[expression.name],
|
|
553
|
+
expression.args[0],
|
|
554
|
+
expression.args[1],
|
|
476
555
|
argSql[0],
|
|
477
556
|
argSql[1],
|
|
478
|
-
valueKind
|
|
557
|
+
valueKind,
|
|
558
|
+
getCompiledValueKind(compiledArgs[0]),
|
|
559
|
+
getCompiledValueKind(compiledArgs[1])
|
|
479
560
|
),
|
|
480
561
|
params
|
|
481
562
|
};
|
|
@@ -526,11 +607,14 @@ function compileSqlExpression(expression) {
|
|
|
526
607
|
if (!leftSql) {
|
|
527
608
|
return { supported: false, sql: ``, params: [] };
|
|
528
609
|
}
|
|
610
|
+
if (context === `index-expression`) {
|
|
611
|
+
return {
|
|
612
|
+
supported: true,
|
|
613
|
+
sql: `(${leftSql} IN (${listValue.map((value) => toSqliteExpressionLiteral(value)).join(`, `)}))`,
|
|
614
|
+
params: leftParams
|
|
615
|
+
};
|
|
616
|
+
}
|
|
529
617
|
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
618
|
const chunkClauses = [];
|
|
535
619
|
const batchedParams = [];
|
|
536
620
|
for (let startIndex = 0; startIndex < listValue.length; startIndex += SQLITE_MAX_IN_BATCH_SIZE) {
|
|
@@ -538,13 +622,13 @@ function compileSqlExpression(expression) {
|
|
|
538
622
|
startIndex,
|
|
539
623
|
startIndex + SQLITE_MAX_IN_BATCH_SIZE
|
|
540
624
|
);
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
);
|
|
625
|
+
const chunkParams = [];
|
|
626
|
+
const chunkValueSql = chunkValues.map((value) => {
|
|
627
|
+
chunkParams.push(toSqliteParameterValue(value));
|
|
628
|
+
return typeof value === `bigint` ? `CAST(? AS NUMERIC)` : `?`;
|
|
629
|
+
});
|
|
630
|
+
chunkClauses.push(`(${leftSql} IN (${chunkValueSql.join(`, `)}))`);
|
|
631
|
+
batchedParams.push(...leftParams, ...chunkParams);
|
|
548
632
|
}
|
|
549
633
|
return {
|
|
550
634
|
supported: true,
|
|
@@ -552,18 +636,15 @@ function compileSqlExpression(expression) {
|
|
|
552
636
|
params: batchedParams
|
|
553
637
|
};
|
|
554
638
|
}
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
639
|
+
const listParams = [];
|
|
640
|
+
const listValueSql = listValue.map((value) => {
|
|
641
|
+
listParams.push(toSqliteParameterValue(value));
|
|
642
|
+
return typeof value === `bigint` ? `CAST(? AS NUMERIC)` : `?`;
|
|
643
|
+
});
|
|
560
644
|
return {
|
|
561
645
|
supported: true,
|
|
562
|
-
sql: `(${
|
|
563
|
-
params: [
|
|
564
|
-
...leftParams,
|
|
565
|
-
...listValue.map((value) => toSqliteParameterValue(value))
|
|
566
|
-
]
|
|
646
|
+
sql: `(${leftSql} IN (${listValueSql.join(`, `)}))`,
|
|
647
|
+
params: [...leftParams, ...listParams]
|
|
567
648
|
};
|
|
568
649
|
}
|
|
569
650
|
case `like`:
|
|
@@ -638,7 +719,10 @@ function compileOrderByClauses(orderBy) {
|
|
|
638
719
|
const parts = [];
|
|
639
720
|
const params = [];
|
|
640
721
|
for (const clause of orderBy) {
|
|
641
|
-
const compiledExpression = compileSqlExpression(
|
|
722
|
+
const compiledExpression = compileSqlExpression(
|
|
723
|
+
clause.expression,
|
|
724
|
+
`index-expression`
|
|
725
|
+
);
|
|
642
726
|
if (!compiledExpression.supported) {
|
|
643
727
|
return {
|
|
644
728
|
supported: false,
|
|
@@ -666,7 +750,7 @@ function isExpressionLikeShape(value) {
|
|
|
666
750
|
return Object.prototype.hasOwnProperty.call(candidate, `value`);
|
|
667
751
|
}
|
|
668
752
|
if (candidate.type === `ref`) {
|
|
669
|
-
return Array.isArray(candidate.path);
|
|
753
|
+
return Array.isArray(candidate.path) && (candidate.sourceAlias === void 0 || typeof candidate.sourceAlias === `string` && candidate.path[0] === candidate.sourceAlias);
|
|
670
754
|
}
|
|
671
755
|
if (candidate.type === `func`) {
|
|
672
756
|
if (typeof candidate.name !== `string` || !Array.isArray(candidate.args)) {
|
|
@@ -690,14 +774,20 @@ function normalizeIndexSqlFragment(fragment) {
|
|
|
690
774
|
hasParsedJson = true;
|
|
691
775
|
} catch {
|
|
692
776
|
}
|
|
693
|
-
|
|
694
|
-
|
|
777
|
+
const decodedJson = hasParsedJson ? decodePersistedJsonValue(parsedJson) : void 0;
|
|
778
|
+
if (hasParsedJson && isExpressionLikeShape(decodedJson)) {
|
|
779
|
+
const compiled = compileSqlExpression(decodedJson, `index-expression`);
|
|
695
780
|
if (!compiled.supported) {
|
|
696
781
|
throw new InvalidPersistedCollectionConfigError(
|
|
697
782
|
`Persisted index expression is not supported by the SQLite compiler`
|
|
698
783
|
);
|
|
699
784
|
}
|
|
700
|
-
|
|
785
|
+
if (compiled.params.length !== 0) {
|
|
786
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
787
|
+
`Persisted index expression cannot contain bound parameters`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return compiled.sql;
|
|
701
791
|
}
|
|
702
792
|
return sanitizeExpressionSqlFragment(fragment);
|
|
703
793
|
}
|
|
@@ -719,6 +809,13 @@ class SQLiteCorePersistenceAdapter {
|
|
|
719
809
|
this.initialized = false;
|
|
720
810
|
this.collectionTableCache = /* @__PURE__ */ new Map();
|
|
721
811
|
this.collectionTableLoads = /* @__PURE__ */ new Map();
|
|
812
|
+
const maxBoundParameters = options.driver.maxBoundParameters;
|
|
813
|
+
if (maxBoundParameters !== void 0 && (!Number.isInteger(maxBoundParameters) || maxBoundParameters < 4)) {
|
|
814
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
815
|
+
`SQLite driver maxBoundParameters must be an integer of at least 4`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
this.replacementBatchSize = maxBoundParameters === void 0 ? REPLACEMENT_BATCH_SIZE : Math.min(REPLACEMENT_BATCH_SIZE, Math.floor(maxBoundParameters / 4));
|
|
722
819
|
const schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
723
820
|
if (!Number.isInteger(schemaVersion) || schemaVersion < 0) {
|
|
724
821
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -741,12 +838,67 @@ class SQLiteCorePersistenceAdapter {
|
|
|
741
838
|
`SQLite adapter pullSinceReloadThreshold must be a non-negative integer`
|
|
742
839
|
);
|
|
743
840
|
}
|
|
744
|
-
this.
|
|
841
|
+
this.schedulingIdentitySource = options.driver;
|
|
842
|
+
const schedulingKey = getSharedLogicalSchedulingKey(options.driver) ?? observedDriverSchedulingKeys.get(options.driver);
|
|
843
|
+
this.scheduler = schedulingKey ? getSharedPersistenceScheduler(schedulingKey) : void 0;
|
|
844
|
+
this.driver = schedulingKey ? options.driver : observeSharedLogicalSchedulingSupport(options.driver, (key) => {
|
|
845
|
+
observedDriverSchedulingKeys.set(options.driver, key);
|
|
846
|
+
const scheduler = getSharedPersistenceScheduler(key);
|
|
847
|
+
this.scheduler ??= scheduler;
|
|
848
|
+
if (this.activeUnscheduledHydration) {
|
|
849
|
+
scheduler.adoptRunningHydrate(this.activeUnscheduledHydration);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
745
852
|
this.schemaVersion = schemaVersion;
|
|
746
853
|
this.schemaMismatchPolicy = options.schemaMismatchPolicy ?? `sync-present-reset`;
|
|
747
854
|
this.appliedTxPruneMaxRows = options.appliedTxPruneMaxRows;
|
|
748
855
|
this.appliedTxPruneMaxAgeSeconds = options.appliedTxPruneMaxAgeSeconds;
|
|
749
856
|
this.pullSinceReloadThreshold = pullSinceReloadThreshold;
|
|
857
|
+
this.hydrationAdapter = {
|
|
858
|
+
loadSubset: (collectionId, loadOptions, context) => this.loadSubsetUnscheduled(collectionId, loadOptions, context),
|
|
859
|
+
loadResumeSnapshot: (collectionId, context) => this.loadResumeSnapshotUnscheduled(collectionId, context),
|
|
860
|
+
applyCommittedTx: (collectionId, tx) => this.applyCommittedTxUnscheduled(collectionId, tx),
|
|
861
|
+
loadCollectionMetadata: (collectionId) => this.loadCollectionMetadataUnscheduled(collectionId),
|
|
862
|
+
scanRows: (collectionId, scanOptions) => this.scanRowsUnscheduled(collectionId, scanOptions),
|
|
863
|
+
ensureIndex: (collectionId, signature, spec) => this.ensureIndexUnscheduled(collectionId, signature, spec),
|
|
864
|
+
markIndexRemoved: (collectionId, signature) => this.markIndexRemovedUnscheduled(collectionId, signature),
|
|
865
|
+
getStreamPosition: (collectionId) => this.getStreamPositionUnscheduled(collectionId),
|
|
866
|
+
pullSince: (collectionId, fromRowVersion) => this.pullSinceUnscheduled(collectionId, fromRowVersion),
|
|
867
|
+
runInHydrationScope: async (task) => task(this.hydrationAdapter)
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
runInHydrationScope(task) {
|
|
871
|
+
const scheduler = this.resolveScheduler();
|
|
872
|
+
if (scheduler) {
|
|
873
|
+
return scheduler.runHydrate(() => task(this.hydrationAdapter));
|
|
874
|
+
}
|
|
875
|
+
const hydration = Promise.resolve().then(() => task(this.hydrationAdapter));
|
|
876
|
+
this.activeUnscheduledHydration = hydration;
|
|
877
|
+
const clear = () => {
|
|
878
|
+
if (this.activeUnscheduledHydration === hydration) {
|
|
879
|
+
this.activeUnscheduledHydration = void 0;
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
void hydration.then(clear, clear);
|
|
883
|
+
return hydration;
|
|
884
|
+
}
|
|
885
|
+
runInRegularScope(task) {
|
|
886
|
+
return this.runRegular(() => task(this.hydrationAdapter));
|
|
887
|
+
}
|
|
888
|
+
isHydrationScopeScheduled() {
|
|
889
|
+
return this.resolveScheduler() !== void 0;
|
|
890
|
+
}
|
|
891
|
+
runRegular(task) {
|
|
892
|
+
const scheduler = this.resolveScheduler();
|
|
893
|
+
return scheduler ? scheduler.runRegular(task) : task();
|
|
894
|
+
}
|
|
895
|
+
resolveScheduler() {
|
|
896
|
+
if (this.scheduler) return this.scheduler;
|
|
897
|
+
const key = observedDriverSchedulingKeys.get(this.schedulingIdentitySource);
|
|
898
|
+
if (key) {
|
|
899
|
+
this.scheduler = getSharedPersistenceScheduler(key);
|
|
900
|
+
}
|
|
901
|
+
return this.scheduler;
|
|
750
902
|
}
|
|
751
903
|
runInTransaction(fn) {
|
|
752
904
|
if (typeof this.driver.transactionWithDriver === `function`) {
|
|
@@ -754,69 +906,197 @@ class SQLiteCorePersistenceAdapter {
|
|
|
754
906
|
}
|
|
755
907
|
return this.driver.transaction(fn);
|
|
756
908
|
}
|
|
757
|
-
async
|
|
909
|
+
async assertCurrentSchemaVersion(collectionId, driver, operation) {
|
|
910
|
+
const schemaRows = await driver.query(
|
|
911
|
+
`SELECT schema_version
|
|
912
|
+
FROM collection_registry
|
|
913
|
+
WHERE collection_id = ?
|
|
914
|
+
LIMIT 1`,
|
|
915
|
+
[collectionId]
|
|
916
|
+
);
|
|
917
|
+
const persistedSchemaVersion = schemaRows[0]?.schema_version;
|
|
918
|
+
if (persistedSchemaVersion !== this.schemaVersion) {
|
|
919
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
920
|
+
`Schema version mismatch for collection "${collectionId}": found ${persistedSchemaVersion ?? `missing`}, expected ${this.schemaVersion}. Refusing to ${operation} through a stale cached adapter.`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
loadSubset(collectionId, options, ctx) {
|
|
925
|
+
return this.runRegular(
|
|
926
|
+
() => this.loadSubsetUnscheduled(collectionId, options, ctx)
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
async loadSubsetUnscheduled(collectionId, options, ctx) {
|
|
758
930
|
const tableMapping = await this.ensureCollectionReady(collectionId);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
931
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
932
|
+
await this.assertCurrentSchemaVersion(
|
|
933
|
+
collectionId,
|
|
934
|
+
transactionDriver,
|
|
935
|
+
`load persisted rows`
|
|
936
|
+
);
|
|
937
|
+
await this.touchRequiredIndexes(
|
|
938
|
+
collectionId,
|
|
939
|
+
ctx?.requiredIndexSignatures,
|
|
940
|
+
transactionDriver
|
|
941
|
+
);
|
|
942
|
+
if (options.cursor) {
|
|
943
|
+
const whereCurrentOptions = {
|
|
944
|
+
where: options.where ? new IR.Func(`and`, [options.where, options.cursor.whereCurrent]) : options.cursor.whereCurrent,
|
|
945
|
+
orderBy: options.orderBy
|
|
946
|
+
};
|
|
947
|
+
const whereFromOptions = {
|
|
948
|
+
where: options.where ? new IR.Func(`and`, [options.where, options.cursor.whereFrom]) : options.cursor.whereFrom,
|
|
949
|
+
orderBy: options.orderBy,
|
|
950
|
+
limit: options.limit
|
|
951
|
+
};
|
|
952
|
+
const [whereCurrentRows, whereFromRows] = await Promise.all([
|
|
953
|
+
this.loadSubsetInternal(
|
|
954
|
+
tableMapping,
|
|
955
|
+
whereCurrentOptions,
|
|
956
|
+
transactionDriver
|
|
957
|
+
),
|
|
958
|
+
this.loadSubsetInternal(
|
|
959
|
+
tableMapping,
|
|
960
|
+
whereFromOptions,
|
|
961
|
+
transactionDriver
|
|
962
|
+
)
|
|
963
|
+
]);
|
|
964
|
+
const mergedRows = /* @__PURE__ */ new Map();
|
|
965
|
+
for (const row of [...whereCurrentRows, ...whereFromRows]) {
|
|
966
|
+
mergedRows.set(encodePersistedStorageKey(row.key), row);
|
|
967
|
+
}
|
|
968
|
+
const orderedRows = this.applyInMemoryOrderBy(
|
|
969
|
+
Array.from(mergedRows.values()),
|
|
970
|
+
options.orderBy
|
|
971
|
+
);
|
|
972
|
+
return orderedRows.map((row) => ({
|
|
973
|
+
key: row.key,
|
|
974
|
+
value: row.value,
|
|
975
|
+
metadata: row.metadata
|
|
976
|
+
}));
|
|
777
977
|
}
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
options
|
|
978
|
+
const rows = await this.loadSubsetInternal(
|
|
979
|
+
tableMapping,
|
|
980
|
+
options,
|
|
981
|
+
transactionDriver
|
|
781
982
|
);
|
|
782
|
-
return
|
|
983
|
+
return rows.map((row) => ({
|
|
783
984
|
key: row.key,
|
|
784
985
|
value: row.value,
|
|
785
986
|
metadata: row.metadata
|
|
786
987
|
}));
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
loadResumeSnapshot(collectionId, ctx) {
|
|
991
|
+
return this.runRegular(
|
|
992
|
+
() => this.loadResumeSnapshotUnscheduled(collectionId, ctx)
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
async loadResumeSnapshotUnscheduled(collectionId, ctx) {
|
|
996
|
+
const tableMapping = await this.ensureCollectionReady(collectionId);
|
|
997
|
+
const includeRows = ctx?.includeRows !== false;
|
|
998
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
999
|
+
await this.assertCurrentSchemaVersion(
|
|
1000
|
+
collectionId,
|
|
1001
|
+
transactionDriver,
|
|
1002
|
+
`load a resume snapshot`
|
|
1003
|
+
);
|
|
1004
|
+
if (includeRows) {
|
|
1005
|
+
await this.touchRequiredIndexes(
|
|
1006
|
+
collectionId,
|
|
1007
|
+
ctx?.requiredIndexSignatures,
|
|
1008
|
+
transactionDriver
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
const rows = includeRows ? await this.loadSubsetInternal(tableMapping, {}, transactionDriver) : [];
|
|
1012
|
+
const { latestRowVersion, keySet } = await this.readKeySetEvidence(
|
|
1013
|
+
collectionId,
|
|
1014
|
+
transactionDriver
|
|
1015
|
+
);
|
|
1016
|
+
const collectionMetadataRows = await transactionDriver.query(
|
|
1017
|
+
`SELECT key, value
|
|
1018
|
+
FROM collection_metadata
|
|
1019
|
+
WHERE collection_id = ?`,
|
|
1020
|
+
[collectionId]
|
|
1021
|
+
);
|
|
1022
|
+
const { latestTerm, latestSeq } = await this.readStreamPosition(
|
|
1023
|
+
collectionId,
|
|
1024
|
+
transactionDriver
|
|
1025
|
+
);
|
|
1026
|
+
const resetRows = await transactionDriver.query(
|
|
1027
|
+
`SELECT reset_epoch
|
|
1028
|
+
FROM collection_reset_epoch
|
|
1029
|
+
WHERE collection_id = ?
|
|
1030
|
+
LIMIT 1`,
|
|
1031
|
+
[collectionId]
|
|
1032
|
+
);
|
|
1033
|
+
return {
|
|
1034
|
+
rows: rows.map((row) => ({
|
|
1035
|
+
key: row.key,
|
|
1036
|
+
value: row.value,
|
|
1037
|
+
metadata: row.metadata
|
|
1038
|
+
})),
|
|
1039
|
+
keySet,
|
|
1040
|
+
collectionMetadata: collectionMetadataRows.map((row) => ({
|
|
1041
|
+
key: row.key,
|
|
1042
|
+
value: deserializePersistedRowValue(row.value)
|
|
1043
|
+
})),
|
|
1044
|
+
latestTerm,
|
|
1045
|
+
latestSeq,
|
|
1046
|
+
latestRowVersion,
|
|
1047
|
+
resetEpoch: resetRows[0]?.reset_epoch ?? 0
|
|
1048
|
+
};
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
applyCommittedTx(collectionId, tx) {
|
|
1052
|
+
return this.runRegular(
|
|
1053
|
+
() => this.applyCommittedTxUnscheduled(collectionId, tx)
|
|
1054
|
+
);
|
|
794
1055
|
}
|
|
795
|
-
async
|
|
1056
|
+
async applyCommittedTxUnscheduled(collectionId, tx) {
|
|
796
1057
|
const tableMapping = await this.ensureCollectionReady(collectionId);
|
|
797
1058
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName);
|
|
798
1059
|
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
|
|
799
1060
|
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
1061
|
const versionRows = await transactionDriver.query(
|
|
811
|
-
`SELECT
|
|
1062
|
+
`SELECT
|
|
1063
|
+
latest_row_version,
|
|
1064
|
+
key_set_evidence_available,
|
|
1065
|
+
(
|
|
1066
|
+
SELECT schema_version
|
|
1067
|
+
FROM collection_registry
|
|
1068
|
+
WHERE collection_id = ?
|
|
1069
|
+
LIMIT 1
|
|
1070
|
+
) AS schema_version,
|
|
1071
|
+
EXISTS (
|
|
1072
|
+
SELECT 1
|
|
1073
|
+
FROM applied_tx
|
|
1074
|
+
WHERE collection_id = ? AND term = ? AND seq = ?
|
|
1075
|
+
) AS already_applied
|
|
812
1076
|
FROM collection_version
|
|
813
1077
|
WHERE collection_id = ?
|
|
814
1078
|
LIMIT 1`,
|
|
815
|
-
[collectionId]
|
|
1079
|
+
[collectionId, collectionId, tx.term, tx.seq, collectionId]
|
|
816
1080
|
);
|
|
817
|
-
const
|
|
1081
|
+
const version = versionRows[0];
|
|
1082
|
+
if (!version) {
|
|
1083
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1084
|
+
`Missing persisted version state for collection "${collectionId}"`
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
if (version.schema_version !== this.schemaVersion) {
|
|
1088
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1089
|
+
`Schema version mismatch for collection "${collectionId}": found ${version.schema_version}, expected ${this.schemaVersion}. Refusing to apply a committed transaction through a stale cached adapter.`
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
if (version.already_applied === 1) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const currentRowVersion = version.latest_row_version;
|
|
818
1096
|
const nextRowVersion = Math.max(currentRowVersion + 1, tx.rowVersion);
|
|
819
|
-
const
|
|
1097
|
+
const replacesPersistedBaseline = tx.truncate === true;
|
|
1098
|
+
const tracksPersistedKeySet = version.key_set_evidence_available === 1 || replacesPersistedBaseline;
|
|
1099
|
+
const replayDelta = replacesPersistedBaseline ? null : {
|
|
820
1100
|
txId: tx.txId,
|
|
821
1101
|
latestRowVersion: nextRowVersion,
|
|
822
1102
|
changedRows: tx.mutations.filter((mutation) => mutation.type !== `delete`).map((mutation) => ({
|
|
@@ -827,13 +1107,67 @@ class SQLiteCorePersistenceAdapter {
|
|
|
827
1107
|
rowMetadataMutations: tx.rowMetadataMutations ?? [],
|
|
828
1108
|
collectionMetadataMutations: tx.collectionMetadataMutations ?? []
|
|
829
1109
|
};
|
|
830
|
-
if (
|
|
1110
|
+
if (replacesPersistedBaseline) {
|
|
1111
|
+
await transactionDriver.run(
|
|
1112
|
+
`DELETE FROM collection_expected_keys
|
|
1113
|
+
WHERE collection_id = ?`,
|
|
1114
|
+
[collectionId]
|
|
1115
|
+
);
|
|
831
1116
|
await transactionDriver.run(`DELETE FROM ${collectionTableSql}`);
|
|
832
1117
|
await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`);
|
|
833
1118
|
}
|
|
834
|
-
|
|
1119
|
+
const replacementKeys = replacesPersistedBaseline ? tx.mutations.map(
|
|
1120
|
+
(mutation) => encodePersistedStorageKey(mutation.key)
|
|
1121
|
+
) : [];
|
|
1122
|
+
const batchReplacement = replacesPersistedBaseline && tx.mutations.length > 0 && tx.mutations.every((mutation) => mutation.type !== `delete`) && new Set(replacementKeys).size === replacementKeys.length;
|
|
1123
|
+
if (batchReplacement) {
|
|
1124
|
+
const finalRowMetadata = /* @__PURE__ */ new Map();
|
|
1125
|
+
for (const mutation of tx.rowMetadataMutations ?? []) {
|
|
1126
|
+
finalRowMetadata.set(
|
|
1127
|
+
encodePersistedStorageKey(mutation.key),
|
|
1128
|
+
mutation.type === `delete` ? void 0 : mutation.value
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
for (let start = 0; start < tx.mutations.length; start += this.replacementBatchSize) {
|
|
1132
|
+
const mutations = tx.mutations.slice(
|
|
1133
|
+
start,
|
|
1134
|
+
start + this.replacementBatchSize
|
|
1135
|
+
);
|
|
1136
|
+
const keys = replacementKeys.slice(
|
|
1137
|
+
start,
|
|
1138
|
+
start + this.replacementBatchSize
|
|
1139
|
+
);
|
|
1140
|
+
await transactionDriver.run(
|
|
1141
|
+
`INSERT INTO collection_expected_keys (collection_id, key)
|
|
1142
|
+
VALUES ${keys.map(() => `(?, ?)`).join(`, `)}`,
|
|
1143
|
+
keys.flatMap((key) => [collectionId, key])
|
|
1144
|
+
);
|
|
1145
|
+
await transactionDriver.run(
|
|
1146
|
+
`INSERT INTO ${collectionTableSql} (key, value, metadata, row_version)
|
|
1147
|
+
VALUES ${keys.map(() => `(?, ?, ?, ?)`).join(`, `)}`,
|
|
1148
|
+
mutations.flatMap((mutation, index) => {
|
|
1149
|
+
const key = keys[index];
|
|
1150
|
+
const metadata = finalRowMetadata.has(key) ? finalRowMetadata.get(key) : mutation.type !== `delete` && mutation.metadataChanged === true ? mutation.metadata : void 0;
|
|
1151
|
+
return [
|
|
1152
|
+
key,
|
|
1153
|
+
serializePersistedRowValue(mutation.value),
|
|
1154
|
+
metadata === void 0 ? null : serializePersistedRowValue(metadata),
|
|
1155
|
+
nextRowVersion
|
|
1156
|
+
];
|
|
1157
|
+
})
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
for (const mutation of batchReplacement ? [] : tx.mutations) {
|
|
835
1162
|
const encodedKey = encodePersistedStorageKey(mutation.key);
|
|
836
1163
|
if (mutation.type === `delete`) {
|
|
1164
|
+
if (tracksPersistedKeySet) {
|
|
1165
|
+
await transactionDriver.run(
|
|
1166
|
+
`DELETE FROM collection_expected_keys
|
|
1167
|
+
WHERE collection_id = ? AND key = ?`,
|
|
1168
|
+
[collectionId, encodedKey]
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
837
1171
|
await transactionDriver.run(
|
|
838
1172
|
`DELETE FROM ${collectionTableSql}
|
|
839
1173
|
WHERE key = ?`,
|
|
@@ -866,6 +1200,14 @@ class SQLiteCorePersistenceAdapter {
|
|
|
866
1200
|
const existingMetadata = existingRows[0]?.metadata != null ? deserializePersistedRowValue(existingRows[0].metadata) : void 0;
|
|
867
1201
|
const mergedValue = mutation.type === `update` ? mergeObjectRows(existingValue, mutation.value) : mutation.value;
|
|
868
1202
|
const nextMetadata = mutation.metadataChanged === true ? mutation.metadata : existingMetadata;
|
|
1203
|
+
if (tracksPersistedKeySet) {
|
|
1204
|
+
await transactionDriver.run(
|
|
1205
|
+
`INSERT INTO collection_expected_keys (collection_id, key)
|
|
1206
|
+
VALUES (?, ?)
|
|
1207
|
+
ON CONFLICT(collection_id, key) DO NOTHING`,
|
|
1208
|
+
[collectionId, encodedKey]
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
869
1211
|
await transactionDriver.run(
|
|
870
1212
|
`INSERT INTO ${collectionTableSql} (key, value, metadata, row_version)
|
|
871
1213
|
VALUES (?, ?, ?, ?)
|
|
@@ -886,7 +1228,7 @@ class SQLiteCorePersistenceAdapter {
|
|
|
886
1228
|
[encodedKey]
|
|
887
1229
|
);
|
|
888
1230
|
}
|
|
889
|
-
for (const rowMetadataMutation of tx.rowMetadataMutations ?? []) {
|
|
1231
|
+
for (const rowMetadataMutation of batchReplacement ? [] : tx.rowMetadataMutations ?? []) {
|
|
890
1232
|
const encodedKey = encodePersistedStorageKey(rowMetadataMutation.key);
|
|
891
1233
|
if (rowMetadataMutation.type === `delete`) {
|
|
892
1234
|
await transactionDriver.run(
|
|
@@ -930,11 +1272,23 @@ class SQLiteCorePersistenceAdapter {
|
|
|
930
1272
|
}
|
|
931
1273
|
}
|
|
932
1274
|
await transactionDriver.run(
|
|
933
|
-
`
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1275
|
+
`UPDATE collection_version
|
|
1276
|
+
SET latest_row_version = ?,
|
|
1277
|
+
key_set_evidence_available = CASE
|
|
1278
|
+
WHEN ? = 1 THEN 1
|
|
1279
|
+
ELSE key_set_evidence_available
|
|
1280
|
+
END,
|
|
1281
|
+
key_set_evidence_incompatible = CASE
|
|
1282
|
+
WHEN ? = 1 THEN 0
|
|
1283
|
+
ELSE key_set_evidence_incompatible
|
|
1284
|
+
END
|
|
1285
|
+
WHERE collection_id = ?`,
|
|
1286
|
+
[
|
|
1287
|
+
nextRowVersion,
|
|
1288
|
+
replacesPersistedBaseline ? 1 : 0,
|
|
1289
|
+
replacesPersistedBaseline ? 1 : 0,
|
|
1290
|
+
collectionId
|
|
1291
|
+
]
|
|
938
1292
|
);
|
|
939
1293
|
await transactionDriver.run(
|
|
940
1294
|
`INSERT INTO leader_term (collection_id, latest_term)
|
|
@@ -966,40 +1320,70 @@ class SQLiteCorePersistenceAdapter {
|
|
|
966
1320
|
tx.txId,
|
|
967
1321
|
nextRowVersion,
|
|
968
1322
|
replayDelta ? stableStringify(replayDelta) : null,
|
|
969
|
-
|
|
1323
|
+
replacesPersistedBaseline ? 1 : 0
|
|
970
1324
|
]
|
|
971
1325
|
);
|
|
972
1326
|
await this.pruneAppliedTxRows(collectionId, transactionDriver);
|
|
973
1327
|
});
|
|
974
1328
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
FROM collection_metadata
|
|
979
|
-
WHERE collection_id = ?`,
|
|
980
|
-
[collectionId]
|
|
1329
|
+
loadCollectionMetadata(collectionId) {
|
|
1330
|
+
return this.runRegular(
|
|
1331
|
+
() => this.loadCollectionMetadataUnscheduled(collectionId)
|
|
981
1332
|
);
|
|
982
|
-
return rows.map((row) => ({
|
|
983
|
-
key: row.key,
|
|
984
|
-
value: deserializePersistedRowValue(row.value)
|
|
985
|
-
}));
|
|
986
1333
|
}
|
|
987
|
-
async
|
|
1334
|
+
async loadCollectionMetadataUnscheduled(collectionId) {
|
|
1335
|
+
await this.ensureCollectionReady(collectionId);
|
|
1336
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1337
|
+
await this.assertCurrentSchemaVersion(
|
|
1338
|
+
collectionId,
|
|
1339
|
+
transactionDriver,
|
|
1340
|
+
`load collection metadata`
|
|
1341
|
+
);
|
|
1342
|
+
const rows = await transactionDriver.query(
|
|
1343
|
+
`SELECT key, value
|
|
1344
|
+
FROM collection_metadata
|
|
1345
|
+
WHERE collection_id = ?`,
|
|
1346
|
+
[collectionId]
|
|
1347
|
+
);
|
|
1348
|
+
return rows.map((row) => ({
|
|
1349
|
+
key: row.key,
|
|
1350
|
+
value: deserializePersistedRowValue(row.value)
|
|
1351
|
+
}));
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
scanRows(collectionId, options) {
|
|
1355
|
+
return this.runRegular(
|
|
1356
|
+
() => this.scanRowsUnscheduled(collectionId, options)
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
async scanRowsUnscheduled(collectionId, options) {
|
|
988
1360
|
const tableMapping = await this.ensureCollectionReady(collectionId);
|
|
989
1361
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName);
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1362
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1363
|
+
await this.assertCurrentSchemaVersion(
|
|
1364
|
+
collectionId,
|
|
1365
|
+
transactionDriver,
|
|
1366
|
+
`scan persisted rows`
|
|
1367
|
+
);
|
|
1368
|
+
const storedRows = await transactionDriver.query(
|
|
1369
|
+
options?.metadataOnly ? `SELECT key, value, metadata, row_version
|
|
1370
|
+
FROM ${collectionTableSql}
|
|
1371
|
+
WHERE metadata IS NOT NULL` : `SELECT key, value, metadata, row_version
|
|
1372
|
+
FROM ${collectionTableSql}`
|
|
1373
|
+
);
|
|
1374
|
+
return decodeStoredSqliteRows(storedRows).map((row) => ({
|
|
1375
|
+
key: row.key,
|
|
1376
|
+
value: row.value,
|
|
1377
|
+
metadata: row.metadata
|
|
1378
|
+
}));
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
ensureIndex(collectionId, signature, spec) {
|
|
1382
|
+
return this.runRegular(
|
|
1383
|
+
() => this.ensureIndexUnscheduled(collectionId, signature, spec)
|
|
995
1384
|
);
|
|
996
|
-
return decodeStoredSqliteRows(storedRows).map((row) => ({
|
|
997
|
-
key: row.key,
|
|
998
|
-
value: row.value,
|
|
999
|
-
metadata: row.metadata
|
|
1000
|
-
}));
|
|
1001
1385
|
}
|
|
1002
|
-
async
|
|
1386
|
+
async ensureIndexUnscheduled(collectionId, signature, spec) {
|
|
1003
1387
|
const tableMapping = await this.ensureCollectionReady(collectionId);
|
|
1004
1388
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName);
|
|
1005
1389
|
const indexName = buildIndexName(collectionId, signature);
|
|
@@ -1008,8 +1392,28 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1008
1392
|
(fragment) => normalizeIndexSqlFragment(fragment)
|
|
1009
1393
|
);
|
|
1010
1394
|
const expressionSql = normalizedExpressionSql.join(`, `);
|
|
1395
|
+
const persistedExpressionSql = JSON.stringify(normalizedExpressionSql);
|
|
1011
1396
|
const whereSql = spec.whereSql ? normalizeIndexSqlFragment(spec.whereSql) : void 0;
|
|
1397
|
+
const persistedWhereSql = whereSql ?? null;
|
|
1012
1398
|
await this.runInTransaction(async (transactionDriver) => {
|
|
1399
|
+
await this.assertCurrentSchemaVersion(
|
|
1400
|
+
collectionId,
|
|
1401
|
+
transactionDriver,
|
|
1402
|
+
`create a persisted index`
|
|
1403
|
+
);
|
|
1404
|
+
const existingRows = await transactionDriver.query(
|
|
1405
|
+
`SELECT index_name, expression_sql, where_sql
|
|
1406
|
+
FROM persisted_index_registry
|
|
1407
|
+
WHERE collection_id = ? AND signature = ?
|
|
1408
|
+
LIMIT 1`,
|
|
1409
|
+
[collectionId, signature]
|
|
1410
|
+
);
|
|
1411
|
+
const existing = existingRows[0];
|
|
1412
|
+
if (existing && (existing.index_name !== indexName || existing.expression_sql !== persistedExpressionSql || existing.where_sql !== persistedWhereSql)) {
|
|
1413
|
+
await transactionDriver.exec(
|
|
1414
|
+
`DROP INDEX IF EXISTS ${quoteIdentifier(existing.index_name)}`
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1013
1417
|
await transactionDriver.run(
|
|
1014
1418
|
`INSERT INTO persisted_index_registry (
|
|
1015
1419
|
collection_id,
|
|
@@ -1037,8 +1441,8 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1037
1441
|
collectionId,
|
|
1038
1442
|
signature,
|
|
1039
1443
|
indexName,
|
|
1040
|
-
|
|
1041
|
-
|
|
1444
|
+
persistedExpressionSql,
|
|
1445
|
+
persistedWhereSql
|
|
1042
1446
|
]
|
|
1043
1447
|
);
|
|
1044
1448
|
const createIndexSql = whereSql ? `CREATE INDEX IF NOT EXISTS ${indexNameSql}
|
|
@@ -1048,48 +1452,83 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1048
1452
|
await transactionDriver.exec(createIndexSql);
|
|
1049
1453
|
});
|
|
1050
1454
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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]
|
|
1455
|
+
markIndexRemoved(collectionId, signature) {
|
|
1456
|
+
return this.runRegular(
|
|
1457
|
+
() => this.markIndexRemovedUnscheduled(collectionId, signature)
|
|
1068
1458
|
);
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1459
|
+
}
|
|
1460
|
+
async markIndexRemovedUnscheduled(collectionId, signature) {
|
|
1461
|
+
await this.ensureCollectionReady(collectionId);
|
|
1462
|
+
await this.runInTransaction(async (transactionDriver) => {
|
|
1463
|
+
await this.assertCurrentSchemaVersion(
|
|
1464
|
+
collectionId,
|
|
1465
|
+
transactionDriver,
|
|
1466
|
+
`remove a persisted index`
|
|
1072
1467
|
);
|
|
1073
|
-
|
|
1468
|
+
const rows = await transactionDriver.query(
|
|
1469
|
+
`SELECT index_name
|
|
1470
|
+
FROM persisted_index_registry
|
|
1471
|
+
WHERE collection_id = ? AND signature = ?
|
|
1472
|
+
LIMIT 1`,
|
|
1473
|
+
[collectionId, signature]
|
|
1474
|
+
);
|
|
1475
|
+
const indexName = rows[0]?.index_name;
|
|
1476
|
+
await transactionDriver.run(
|
|
1477
|
+
`UPDATE persisted_index_registry
|
|
1478
|
+
SET removed = 1,
|
|
1479
|
+
updated_at = CAST(strftime('%s', 'now') AS INTEGER),
|
|
1480
|
+
last_used_at = CAST(strftime('%s', 'now') AS INTEGER)
|
|
1481
|
+
WHERE collection_id = ? AND signature = ?`,
|
|
1482
|
+
[collectionId, signature]
|
|
1483
|
+
);
|
|
1484
|
+
if (indexName) {
|
|
1485
|
+
await transactionDriver.exec(
|
|
1486
|
+
`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1074
1490
|
}
|
|
1075
|
-
|
|
1491
|
+
getStreamPosition(collectionId) {
|
|
1492
|
+
return this.getStreamPositionUnscheduled(collectionId);
|
|
1493
|
+
}
|
|
1494
|
+
async getStreamPositionUnscheduled(collectionId) {
|
|
1076
1495
|
await this.ensureCollectionReady(collectionId);
|
|
1077
|
-
|
|
1078
|
-
this.
|
|
1496
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1497
|
+
await this.assertCurrentSchemaVersion(
|
|
1498
|
+
collectionId,
|
|
1499
|
+
transactionDriver,
|
|
1500
|
+
`read the stream position`
|
|
1501
|
+
);
|
|
1502
|
+
const [position, latestRowVersion] = await Promise.all([
|
|
1503
|
+
this.readStreamPosition(collectionId, transactionDriver),
|
|
1504
|
+
this.readLatestRowVersion(collectionId, transactionDriver)
|
|
1505
|
+
]);
|
|
1506
|
+
return {
|
|
1507
|
+
...position,
|
|
1508
|
+
latestRowVersion
|
|
1509
|
+
};
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
async readLatestRowVersion(collectionId, driver) {
|
|
1513
|
+
const versionRows = await driver.query(
|
|
1514
|
+
`SELECT latest_row_version
|
|
1515
|
+
FROM collection_version
|
|
1516
|
+
WHERE collection_id = ?
|
|
1517
|
+
LIMIT 1`,
|
|
1518
|
+
[collectionId]
|
|
1519
|
+
);
|
|
1520
|
+
return versionRows[0]?.latest_row_version ?? 0;
|
|
1521
|
+
}
|
|
1522
|
+
async readStreamPosition(collectionId, driver) {
|
|
1523
|
+
const [termRows, seqRows] = await Promise.all([
|
|
1524
|
+
driver.query(
|
|
1079
1525
|
`SELECT latest_term
|
|
1080
1526
|
FROM leader_term
|
|
1081
1527
|
WHERE collection_id = ?
|
|
1082
1528
|
LIMIT 1`,
|
|
1083
1529
|
[collectionId]
|
|
1084
1530
|
),
|
|
1085
|
-
|
|
1086
|
-
`SELECT latest_row_version
|
|
1087
|
-
FROM collection_version
|
|
1088
|
-
WHERE collection_id = ?
|
|
1089
|
-
LIMIT 1`,
|
|
1090
|
-
[collectionId]
|
|
1091
|
-
),
|
|
1092
|
-
this.driver.query(
|
|
1531
|
+
driver.query(
|
|
1093
1532
|
`SELECT MAX(seq) AS max_seq
|
|
1094
1533
|
FROM applied_tx
|
|
1095
1534
|
WHERE collection_id = ? AND term = (
|
|
@@ -1100,116 +1539,146 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1100
1539
|
]);
|
|
1101
1540
|
return {
|
|
1102
1541
|
latestTerm: termRows[0]?.latest_term ?? 0,
|
|
1103
|
-
latestSeq: seqRows[0]?.max_seq ?? 0
|
|
1104
|
-
|
|
1542
|
+
latestSeq: seqRows[0]?.max_seq ?? 0
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
async readKeySetEvidence(collectionId, driver) {
|
|
1546
|
+
const versionRows = await driver.query(
|
|
1547
|
+
`SELECT
|
|
1548
|
+
latest_row_version,
|
|
1549
|
+
key_set_evidence_available,
|
|
1550
|
+
key_set_evidence_incompatible AS key_set_incompatible
|
|
1551
|
+
FROM collection_version
|
|
1552
|
+
WHERE collection_id = ?
|
|
1553
|
+
LIMIT 1`,
|
|
1554
|
+
[collectionId]
|
|
1555
|
+
);
|
|
1556
|
+
const version = versionRows[0];
|
|
1557
|
+
return {
|
|
1558
|
+
latestRowVersion: version?.latest_row_version ?? 0,
|
|
1559
|
+
keySet: {
|
|
1560
|
+
status: version?.key_set_evidence_available !== 1 ? `unknown` : version.key_set_incompatible === 1 ? `incompatible` : `consistent`
|
|
1561
|
+
}
|
|
1105
1562
|
};
|
|
1106
1563
|
}
|
|
1107
|
-
|
|
1564
|
+
pullSince(collectionId, fromRowVersion) {
|
|
1565
|
+
return this.runRegular(
|
|
1566
|
+
() => this.pullSinceUnscheduled(collectionId, fromRowVersion)
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
async pullSinceUnscheduled(collectionId, fromRowVersion) {
|
|
1108
1570
|
const tableMapping = await this.ensureCollectionReady(collectionId);
|
|
1109
1571
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName);
|
|
1110
1572
|
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1573
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1574
|
+
await this.assertCurrentSchemaVersion(
|
|
1575
|
+
collectionId,
|
|
1576
|
+
transactionDriver,
|
|
1577
|
+
`read persisted transaction deltas`
|
|
1578
|
+
);
|
|
1579
|
+
const [
|
|
1580
|
+
changedRows,
|
|
1581
|
+
deletedRows,
|
|
1582
|
+
latestVersionRows,
|
|
1583
|
+
replayRows,
|
|
1584
|
+
replayAvailabilityRows
|
|
1585
|
+
] = await Promise.all([
|
|
1586
|
+
transactionDriver.query(
|
|
1587
|
+
`SELECT key
|
|
1120
1588
|
FROM ${collectionTableSql}
|
|
1121
1589
|
WHERE row_version > ?`,
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1590
|
+
[fromRowVersion]
|
|
1591
|
+
),
|
|
1592
|
+
transactionDriver.query(
|
|
1593
|
+
`SELECT key
|
|
1126
1594
|
FROM ${tombstoneTableSql}
|
|
1127
1595
|
WHERE row_version > ?`,
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1596
|
+
[fromRowVersion]
|
|
1597
|
+
),
|
|
1598
|
+
transactionDriver.query(
|
|
1599
|
+
`SELECT latest_row_version
|
|
1132
1600
|
FROM collection_version
|
|
1133
1601
|
WHERE collection_id = ?
|
|
1134
1602
|
LIMIT 1`,
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1603
|
+
[collectionId]
|
|
1604
|
+
),
|
|
1605
|
+
transactionDriver.query(
|
|
1606
|
+
`SELECT tx_id, row_version, replay_json, replay_requires_full_reload
|
|
1139
1607
|
FROM applied_tx
|
|
1140
1608
|
WHERE collection_id = ? AND row_version > ?
|
|
1141
1609
|
ORDER BY term ASC, seq ASC`,
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1610
|
+
[collectionId, fromRowVersion]
|
|
1611
|
+
),
|
|
1612
|
+
transactionDriver.query(
|
|
1613
|
+
`SELECT MIN(row_version) AS min_row_version
|
|
1146
1614
|
FROM applied_tx
|
|
1147
1615
|
WHERE collection_id = ?`,
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1616
|
+
[collectionId]
|
|
1617
|
+
)
|
|
1618
|
+
]);
|
|
1619
|
+
const latestRowVersion = latestVersionRows[0]?.latest_row_version ?? 0;
|
|
1620
|
+
const replayFloor = replayAvailabilityRows[0]?.min_row_version;
|
|
1621
|
+
if (latestRowVersion > fromRowVersion && (replayFloor == null || replayFloor > fromRowVersion + 1)) {
|
|
1622
|
+
return {
|
|
1623
|
+
latestRowVersion,
|
|
1624
|
+
requiresFullReload: true
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
const changedKeyCount = changedRows.length + deletedRows.length;
|
|
1628
|
+
if (changedKeyCount > this.pullSinceReloadThreshold) {
|
|
1629
|
+
return {
|
|
1630
|
+
latestRowVersion,
|
|
1631
|
+
requiresFullReload: true
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
if (replayRows.some(
|
|
1635
|
+
(row) => row.replay_requires_full_reload !== 0 || row.replay_json == null
|
|
1636
|
+
)) {
|
|
1637
|
+
return {
|
|
1638
|
+
latestRowVersion,
|
|
1639
|
+
requiresFullReload: true
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
const decodeKey = (encodedKey) => {
|
|
1643
|
+
try {
|
|
1644
|
+
return decodePersistedStorageKey(encodedKey);
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
throw new InvalidPersistedStorageKeyEncodingError(
|
|
1647
|
+
`${encodedKey}: ${error.message}`
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1172
1650
|
};
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
return decodePersistedStorageKey(encodedKey);
|
|
1177
|
-
} catch (error) {
|
|
1178
|
-
throw new InvalidPersistedStorageKeyEncodingError(
|
|
1179
|
-
`${encodedKey}: ${error.message}`
|
|
1651
|
+
const deltas = replayRows.map((row) => {
|
|
1652
|
+
const parsed = deserializePersistedRowValue(
|
|
1653
|
+
row.replay_json ?? `null`
|
|
1180
1654
|
);
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1655
|
+
if (!parsed) {
|
|
1656
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1657
|
+
`missing replay payload for applied_tx row`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
return parsed;
|
|
1661
|
+
});
|
|
1662
|
+
const replayChangeCount = deltas.reduce(
|
|
1663
|
+
(count, delta) => count + delta.changedRows.length + delta.deletedKeys.length + delta.rowMetadataMutations.length + delta.collectionMetadataMutations.length,
|
|
1664
|
+
0
|
|
1186
1665
|
);
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1666
|
+
if (replayChangeCount > this.pullSinceReloadThreshold) {
|
|
1667
|
+
return {
|
|
1668
|
+
latestRowVersion,
|
|
1669
|
+
requiresFullReload: true
|
|
1670
|
+
};
|
|
1191
1671
|
}
|
|
1192
|
-
return parsed;
|
|
1193
|
-
});
|
|
1194
|
-
const replayChangeCount = deltas.reduce(
|
|
1195
|
-
(count, delta) => count + delta.changedRows.length + delta.deletedKeys.length + delta.rowMetadataMutations.length + delta.collectionMetadataMutations.length,
|
|
1196
|
-
0
|
|
1197
|
-
);
|
|
1198
|
-
if (replayChangeCount > this.pullSinceReloadThreshold) {
|
|
1199
1672
|
return {
|
|
1200
1673
|
latestRowVersion,
|
|
1201
|
-
requiresFullReload:
|
|
1674
|
+
requiresFullReload: false,
|
|
1675
|
+
changedKeys: changedRows.map((row) => decodeKey(row.key)),
|
|
1676
|
+
deletedKeys: deletedRows.map((row) => decodeKey(row.key)),
|
|
1677
|
+
deltas
|
|
1202
1678
|
};
|
|
1203
|
-
}
|
|
1204
|
-
return {
|
|
1205
|
-
latestRowVersion,
|
|
1206
|
-
requiresFullReload: false,
|
|
1207
|
-
changedKeys: changedRows.map((row) => decodeKey(row.key)),
|
|
1208
|
-
deletedKeys: deletedRows.map((row) => decodeKey(row.key)),
|
|
1209
|
-
deltas
|
|
1210
|
-
};
|
|
1679
|
+
});
|
|
1211
1680
|
}
|
|
1212
|
-
async loadSubsetInternal(tableMapping, options) {
|
|
1681
|
+
async loadSubsetInternal(tableMapping, options, driver = this.driver) {
|
|
1213
1682
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName);
|
|
1214
1683
|
const whereCompiled = options.where ? compileSqlExpression(options.where) : { supported: true, sql: ``, params: [] };
|
|
1215
1684
|
const orderByCompiled = compileOrderByClauses(options.orderBy);
|
|
@@ -1223,10 +1692,7 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1223
1692
|
sql = `${sql} ORDER BY ${orderByCompiled.sql}, key ASC`;
|
|
1224
1693
|
queryParams.push(...orderByCompiled.params);
|
|
1225
1694
|
}
|
|
1226
|
-
const storedRows = await
|
|
1227
|
-
sql,
|
|
1228
|
-
queryParams
|
|
1229
|
-
);
|
|
1695
|
+
const storedRows = await driver.query(sql, queryParams);
|
|
1230
1696
|
const parsedRows = decodeStoredSqliteRows(storedRows);
|
|
1231
1697
|
const filteredRows = this.applyInMemoryWhere(parsedRows, options.where);
|
|
1232
1698
|
const orderedRows = this.applyInMemoryOrderBy(filteredRows, options.orderBy);
|
|
@@ -1286,12 +1752,12 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1286
1752
|
}
|
|
1287
1753
|
return rows.slice(start, start + limit);
|
|
1288
1754
|
}
|
|
1289
|
-
async touchRequiredIndexes(collectionId, requiredIndexSignatures) {
|
|
1755
|
+
async touchRequiredIndexes(collectionId, requiredIndexSignatures, driver = this.driver) {
|
|
1290
1756
|
if (!requiredIndexSignatures || requiredIndexSignatures.length === 0) {
|
|
1291
1757
|
return;
|
|
1292
1758
|
}
|
|
1293
1759
|
for (const signature of requiredIndexSignatures) {
|
|
1294
|
-
await
|
|
1760
|
+
await driver.run(
|
|
1295
1761
|
`UPDATE persisted_index_registry
|
|
1296
1762
|
SET last_used_at = CAST(strftime('%s', 'now') AS INTEGER),
|
|
1297
1763
|
updated_at = CAST(strftime('%s', 'now') AS INTEGER)
|
|
@@ -1353,31 +1819,21 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1353
1819
|
this.collectionTableLoads.delete(collectionId);
|
|
1354
1820
|
}
|
|
1355
1821
|
}
|
|
1356
|
-
async
|
|
1357
|
-
const
|
|
1822
|
+
async loadCollectionRegistration(collectionId) {
|
|
1823
|
+
const rows = await this.driver.query(
|
|
1358
1824
|
`SELECT table_name, tombstone_table_name, schema_version
|
|
1359
1825
|
FROM collection_registry
|
|
1360
1826
|
WHERE collection_id = ?
|
|
1361
1827
|
LIMIT 1`,
|
|
1362
1828
|
[collectionId]
|
|
1363
1829
|
);
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
collectionId,
|
|
1372
|
-
existingRows[0].schema_version,
|
|
1373
|
-
this.schemaVersion,
|
|
1374
|
-
tableName,
|
|
1375
|
-
tombstoneTableName
|
|
1376
|
-
);
|
|
1377
|
-
}
|
|
1378
|
-
} else {
|
|
1379
|
-
tableName = createPersistedTableName(collectionId, `c`);
|
|
1380
|
-
tombstoneTableName = createPersistedTableName(collectionId, `t`);
|
|
1830
|
+
return rows[0];
|
|
1831
|
+
}
|
|
1832
|
+
async ensureCollectionReadyInternal(collectionId) {
|
|
1833
|
+
let registration = await this.loadCollectionRegistration(collectionId);
|
|
1834
|
+
if (!registration) {
|
|
1835
|
+
const tableName2 = createPersistedTableName(collectionId, `c`);
|
|
1836
|
+
const tombstoneTableName2 = createPersistedTableName(collectionId, `t`);
|
|
1381
1837
|
await this.driver.run(
|
|
1382
1838
|
`INSERT INTO collection_registry (
|
|
1383
1839
|
collection_id,
|
|
@@ -1386,8 +1842,26 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1386
1842
|
schema_version,
|
|
1387
1843
|
updated_at
|
|
1388
1844
|
)
|
|
1389
|
-
VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
|
|
1390
|
-
|
|
1845
|
+
VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
|
|
1846
|
+
ON CONFLICT DO NOTHING`,
|
|
1847
|
+
[collectionId, tableName2, tombstoneTableName2, this.schemaVersion]
|
|
1848
|
+
);
|
|
1849
|
+
registration = await this.loadCollectionRegistration(collectionId);
|
|
1850
|
+
if (!registration) {
|
|
1851
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1852
|
+
`Unable to register persistence tables for collection "${collectionId}"`
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const tableName = registration.table_name;
|
|
1857
|
+
const tombstoneTableName = registration.tombstone_table_name;
|
|
1858
|
+
if (registration.schema_version !== this.schemaVersion) {
|
|
1859
|
+
await this.handleSchemaMismatch(
|
|
1860
|
+
collectionId,
|
|
1861
|
+
registration.schema_version,
|
|
1862
|
+
this.schemaVersion,
|
|
1863
|
+
tableName,
|
|
1864
|
+
tombstoneTableName
|
|
1391
1865
|
);
|
|
1392
1866
|
}
|
|
1393
1867
|
const collectionTableSql = quoteIdentifier(tableName);
|
|
@@ -1417,8 +1891,13 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1417
1891
|
ON ${tombstoneTableSql} (row_version)`
|
|
1418
1892
|
);
|
|
1419
1893
|
await this.driver.run(
|
|
1420
|
-
`INSERT INTO collection_version (
|
|
1421
|
-
|
|
1894
|
+
`INSERT INTO collection_version (
|
|
1895
|
+
collection_id,
|
|
1896
|
+
latest_row_version,
|
|
1897
|
+
key_set_evidence_available,
|
|
1898
|
+
key_set_evidence_incompatible
|
|
1899
|
+
)
|
|
1900
|
+
VALUES (?, 0, 1, 0)
|
|
1422
1901
|
ON CONFLICT(collection_id) DO NOTHING`,
|
|
1423
1902
|
[collectionId]
|
|
1424
1903
|
);
|
|
@@ -1428,6 +1907,7 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1428
1907
|
ON CONFLICT(collection_id) DO NOTHING`,
|
|
1429
1908
|
[collectionId]
|
|
1430
1909
|
);
|
|
1910
|
+
await this.ensureCollectionKeyEvidenceTriggers(collectionId, tableName);
|
|
1431
1911
|
const mapping = {
|
|
1432
1912
|
tableName,
|
|
1433
1913
|
tombstoneTableName
|
|
@@ -1435,6 +1915,68 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1435
1915
|
this.collectionTableCache.set(collectionId, mapping);
|
|
1436
1916
|
return mapping;
|
|
1437
1917
|
}
|
|
1918
|
+
async ensureCollectionKeyEvidenceTriggers(collectionId, tableName) {
|
|
1919
|
+
const collectionTableSql = quoteIdentifier(tableName);
|
|
1920
|
+
const collectionIdLiteral = toSqliteLiteral(collectionId);
|
|
1921
|
+
const insertTriggerSql = quoteIdentifier(`${tableName}_key_evidence_insert`);
|
|
1922
|
+
const deleteTriggerSql = quoteIdentifier(`${tableName}_key_evidence_delete`);
|
|
1923
|
+
const updateTriggerSql = quoteIdentifier(`${tableName}_key_evidence_update`);
|
|
1924
|
+
await this.driver.exec(
|
|
1925
|
+
`CREATE TRIGGER IF NOT EXISTS ${insertTriggerSql}
|
|
1926
|
+
AFTER INSERT ON ${collectionTableSql}
|
|
1927
|
+
WHEN EXISTS (
|
|
1928
|
+
SELECT 1
|
|
1929
|
+
FROM collection_version
|
|
1930
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
1931
|
+
AND key_set_evidence_available = 1
|
|
1932
|
+
) AND NOT EXISTS (
|
|
1933
|
+
SELECT 1
|
|
1934
|
+
FROM collection_expected_keys
|
|
1935
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
1936
|
+
AND key = NEW.key
|
|
1937
|
+
)
|
|
1938
|
+
BEGIN
|
|
1939
|
+
UPDATE collection_version
|
|
1940
|
+
SET key_set_evidence_incompatible = 1
|
|
1941
|
+
WHERE collection_id = ${collectionIdLiteral};
|
|
1942
|
+
END`
|
|
1943
|
+
);
|
|
1944
|
+
await this.driver.exec(
|
|
1945
|
+
`CREATE TRIGGER IF NOT EXISTS ${deleteTriggerSql}
|
|
1946
|
+
AFTER DELETE ON ${collectionTableSql}
|
|
1947
|
+
WHEN EXISTS (
|
|
1948
|
+
SELECT 1
|
|
1949
|
+
FROM collection_version
|
|
1950
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
1951
|
+
AND key_set_evidence_available = 1
|
|
1952
|
+
) AND EXISTS (
|
|
1953
|
+
SELECT 1
|
|
1954
|
+
FROM collection_expected_keys
|
|
1955
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
1956
|
+
AND key = OLD.key
|
|
1957
|
+
)
|
|
1958
|
+
BEGIN
|
|
1959
|
+
UPDATE collection_version
|
|
1960
|
+
SET key_set_evidence_incompatible = 1
|
|
1961
|
+
WHERE collection_id = ${collectionIdLiteral};
|
|
1962
|
+
END`
|
|
1963
|
+
);
|
|
1964
|
+
await this.driver.exec(
|
|
1965
|
+
`CREATE TRIGGER IF NOT EXISTS ${updateTriggerSql}
|
|
1966
|
+
AFTER UPDATE OF key ON ${collectionTableSql}
|
|
1967
|
+
WHEN OLD.key <> NEW.key AND EXISTS (
|
|
1968
|
+
SELECT 1
|
|
1969
|
+
FROM collection_version
|
|
1970
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
1971
|
+
AND key_set_evidence_available = 1
|
|
1972
|
+
)
|
|
1973
|
+
BEGIN
|
|
1974
|
+
UPDATE collection_version
|
|
1975
|
+
SET key_set_evidence_incompatible = 1
|
|
1976
|
+
WHERE collection_id = ${collectionIdLiteral};
|
|
1977
|
+
END`
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1438
1980
|
async handleSchemaMismatch(collectionId, previousSchemaVersion, nextSchemaVersion, tableName, tombstoneTableName) {
|
|
1439
1981
|
if (this.schemaMismatchPolicy === `sync-absent-error`) {
|
|
1440
1982
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -1444,6 +1986,22 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1444
1986
|
const collectionTableSql = quoteIdentifier(tableName);
|
|
1445
1987
|
const tombstoneTableSql = quoteIdentifier(tombstoneTableName);
|
|
1446
1988
|
await this.runInTransaction(async (transactionDriver) => {
|
|
1989
|
+
const currentSchemaRows = await transactionDriver.query(
|
|
1990
|
+
`SELECT schema_version
|
|
1991
|
+
FROM collection_registry
|
|
1992
|
+
WHERE collection_id = ?
|
|
1993
|
+
LIMIT 1`,
|
|
1994
|
+
[collectionId]
|
|
1995
|
+
);
|
|
1996
|
+
const currentSchemaVersion = currentSchemaRows[0]?.schema_version;
|
|
1997
|
+
if (currentSchemaVersion === nextSchemaVersion) {
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (currentSchemaVersion !== previousSchemaVersion) {
|
|
2001
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
2002
|
+
`Schema version changed concurrently for collection "${collectionId}": found ${currentSchemaVersion ?? `no registry entry`} after observing ${previousSchemaVersion}; refusing to reset it to ${nextSchemaVersion}.`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
1447
2005
|
const persistedIndexes = await transactionDriver.query(
|
|
1448
2006
|
`SELECT index_name
|
|
1449
2007
|
FROM persisted_index_registry
|
|
@@ -1455,6 +2013,11 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1455
2013
|
`DROP INDEX IF EXISTS ${quoteIdentifier(row.index_name)}`
|
|
1456
2014
|
);
|
|
1457
2015
|
}
|
|
2016
|
+
await transactionDriver.run(
|
|
2017
|
+
`DELETE FROM collection_expected_keys
|
|
2018
|
+
WHERE collection_id = ?`,
|
|
2019
|
+
[collectionId]
|
|
2020
|
+
);
|
|
1458
2021
|
await transactionDriver.run(`DELETE FROM ${collectionTableSql}`);
|
|
1459
2022
|
await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`);
|
|
1460
2023
|
await transactionDriver.run(
|
|
@@ -1467,6 +2030,11 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1467
2030
|
WHERE collection_id = ?`,
|
|
1468
2031
|
[collectionId]
|
|
1469
2032
|
);
|
|
2033
|
+
await transactionDriver.run(
|
|
2034
|
+
`DELETE FROM collection_metadata
|
|
2035
|
+
WHERE collection_id = ?`,
|
|
2036
|
+
[collectionId]
|
|
2037
|
+
);
|
|
1470
2038
|
await transactionDriver.run(
|
|
1471
2039
|
`UPDATE collection_registry
|
|
1472
2040
|
SET schema_version = ?,
|
|
@@ -1475,10 +2043,17 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1475
2043
|
[nextSchemaVersion, collectionId]
|
|
1476
2044
|
);
|
|
1477
2045
|
await transactionDriver.run(
|
|
1478
|
-
`INSERT INTO collection_version (
|
|
1479
|
-
|
|
2046
|
+
`INSERT INTO collection_version (
|
|
2047
|
+
collection_id,
|
|
2048
|
+
latest_row_version,
|
|
2049
|
+
key_set_evidence_available,
|
|
2050
|
+
key_set_evidence_incompatible
|
|
2051
|
+
)
|
|
2052
|
+
VALUES (?, 0, 1, 0)
|
|
1480
2053
|
ON CONFLICT(collection_id) DO UPDATE SET
|
|
1481
|
-
latest_row_version = 0
|
|
2054
|
+
latest_row_version = 0,
|
|
2055
|
+
key_set_evidence_available = 1,
|
|
2056
|
+
key_set_evidence_incompatible = 0`,
|
|
1482
2057
|
[collectionId]
|
|
1483
2058
|
);
|
|
1484
2059
|
await transactionDriver.run(
|
|
@@ -1552,7 +2127,37 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1552
2127
|
await this.driver.exec(
|
|
1553
2128
|
`CREATE TABLE IF NOT EXISTS collection_version (
|
|
1554
2129
|
collection_id TEXT PRIMARY KEY,
|
|
1555
|
-
latest_row_version INTEGER NOT NULL
|
|
2130
|
+
latest_row_version INTEGER NOT NULL,
|
|
2131
|
+
key_set_evidence_available INTEGER NOT NULL DEFAULT 0,
|
|
2132
|
+
key_set_evidence_incompatible INTEGER NOT NULL DEFAULT 0
|
|
2133
|
+
)`
|
|
2134
|
+
);
|
|
2135
|
+
const collectionVersionColumns = await this.driver.query(
|
|
2136
|
+
`PRAGMA table_info(collection_version)`
|
|
2137
|
+
);
|
|
2138
|
+
const keyEvidenceColumns = [
|
|
2139
|
+
`key_set_evidence_available`,
|
|
2140
|
+
`key_set_evidence_incompatible`
|
|
2141
|
+
];
|
|
2142
|
+
for (const columnName of keyEvidenceColumns) {
|
|
2143
|
+
if (collectionVersionColumns.some(({ name }) => name === columnName)) {
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
try {
|
|
2147
|
+
await this.driver.exec(
|
|
2148
|
+
`ALTER TABLE collection_version ADD COLUMN ${columnName} INTEGER NOT NULL DEFAULT 0`
|
|
2149
|
+
);
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
if (!isDuplicateColumnAddError(error, columnName)) {
|
|
2152
|
+
throw error;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
await this.driver.exec(
|
|
2157
|
+
`CREATE TABLE IF NOT EXISTS collection_expected_keys (
|
|
2158
|
+
collection_id TEXT NOT NULL,
|
|
2159
|
+
key TEXT NOT NULL,
|
|
2160
|
+
PRIMARY KEY (collection_id, key)
|
|
1556
2161
|
)`
|
|
1557
2162
|
);
|
|
1558
2163
|
await this.driver.exec(
|