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