@tanstack/db-sqlite-persistence-core 0.2.23 → 0.4.0
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
|
@@ -8,13 +8,22 @@ import {
|
|
|
8
8
|
InvalidPersistedStorageKeyEncodingError,
|
|
9
9
|
} from './errors'
|
|
10
10
|
import {
|
|
11
|
+
SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY,
|
|
11
12
|
createPersistedTableName,
|
|
12
13
|
decodePersistedStorageKey,
|
|
13
14
|
encodePersistedStorageKey,
|
|
14
15
|
} from './persisted'
|
|
16
|
+
import {
|
|
17
|
+
PERSISTED_TYPE_TAG,
|
|
18
|
+
PERSISTED_VALUE_TAG,
|
|
19
|
+
assertSQLiteBigIntInRange,
|
|
20
|
+
serializeSQLiteBigInt,
|
|
21
|
+
} from './sqlite-value'
|
|
15
22
|
import type { LoadSubsetOptions } from '@tanstack/db'
|
|
16
23
|
import type {
|
|
24
|
+
HydrationPersistenceAdapter,
|
|
17
25
|
PersistedIndexSpec,
|
|
26
|
+
PersistedKeySetEvidence,
|
|
18
27
|
PersistedRowScanOptions,
|
|
19
28
|
PersistedScannedRow,
|
|
20
29
|
PersistedTx,
|
|
@@ -25,6 +34,10 @@ import type {
|
|
|
25
34
|
|
|
26
35
|
type SqliteSupportedValue = null | number | string
|
|
27
36
|
|
|
37
|
+
// The default stays below SQLite's older 999-variable limit. Drivers with a
|
|
38
|
+
// lower binding cap use smaller chunks; each replacement row binds four values.
|
|
39
|
+
const REPLACEMENT_BATCH_SIZE = 100
|
|
40
|
+
|
|
28
41
|
type CollectionTableMapping = {
|
|
29
42
|
tableName: string
|
|
30
43
|
tombstoneTableName: string
|
|
@@ -37,6 +50,8 @@ type CompiledSqlFragment = {
|
|
|
37
50
|
valueKind?: CompiledValueKind
|
|
38
51
|
}
|
|
39
52
|
|
|
53
|
+
type SqlExpressionCompilationContext = `predicate` | `index-expression`
|
|
54
|
+
|
|
40
55
|
type StoredSqliteRow = {
|
|
41
56
|
key: string
|
|
42
57
|
value: string
|
|
@@ -71,6 +86,159 @@ export type SQLitePullSinceResult<TKey extends string | number> =
|
|
|
71
86
|
deltas: Array<ReplayableTxDelta<Record<string, unknown>, TKey>>
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
type ScheduledOperationKind = `regular` | `hydrate`
|
|
90
|
+
|
|
91
|
+
type ScheduledOperation<T> = {
|
|
92
|
+
kind: ScheduledOperationKind
|
|
93
|
+
task: () => Promise<T>
|
|
94
|
+
resolve: (value: T) => void
|
|
95
|
+
reject: (error: unknown) => void
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class SharedPersistenceScheduler {
|
|
99
|
+
private readonly regularQueue: Array<ScheduledOperation<unknown>> = []
|
|
100
|
+
private readonly hydrateQueue: Array<ScheduledOperation<unknown>> = []
|
|
101
|
+
private running = false
|
|
102
|
+
private lastCompletedKind: ScheduledOperationKind | undefined
|
|
103
|
+
|
|
104
|
+
runRegular<T>(task: () => Promise<T>): Promise<T> {
|
|
105
|
+
return this.enqueue(`regular`, task)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
runHydrate<T>(task: () => Promise<T>): Promise<T> {
|
|
109
|
+
return this.enqueue(`hydrate`, task)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
adoptRunningHydrate(completion: Promise<unknown>): void {
|
|
113
|
+
if (this.running) return
|
|
114
|
+
this.running = true
|
|
115
|
+
const finish = () => {
|
|
116
|
+
this.lastCompletedKind = `hydrate`
|
|
117
|
+
this.running = false
|
|
118
|
+
this.drain()
|
|
119
|
+
}
|
|
120
|
+
void completion.then(finish, finish)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private enqueue<T>(
|
|
124
|
+
kind: ScheduledOperationKind,
|
|
125
|
+
task: () => Promise<T>,
|
|
126
|
+
): Promise<T> {
|
|
127
|
+
const result = new Promise<T>((resolve, reject) => {
|
|
128
|
+
const operation: ScheduledOperation<T> = {
|
|
129
|
+
kind,
|
|
130
|
+
task,
|
|
131
|
+
resolve,
|
|
132
|
+
reject,
|
|
133
|
+
}
|
|
134
|
+
const queue = kind === `hydrate` ? this.hydrateQueue : this.regularQueue
|
|
135
|
+
queue.push(operation as ScheduledOperation<unknown>)
|
|
136
|
+
})
|
|
137
|
+
this.drain()
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private drain(): void {
|
|
142
|
+
if (this.running) return
|
|
143
|
+
|
|
144
|
+
const operation = this.takeNext()
|
|
145
|
+
if (!operation) return
|
|
146
|
+
|
|
147
|
+
this.running = true
|
|
148
|
+
void this.execute(operation)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async execute(operation: ScheduledOperation<unknown>): Promise<void> {
|
|
152
|
+
try {
|
|
153
|
+
operation.resolve(await operation.task())
|
|
154
|
+
} catch (error) {
|
|
155
|
+
operation.reject(error)
|
|
156
|
+
} finally {
|
|
157
|
+
this.lastCompletedKind = operation.kind
|
|
158
|
+
this.running = false
|
|
159
|
+
this.drain()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private takeNext(): ScheduledOperation<unknown> | undefined {
|
|
164
|
+
// Hydrates get priority after the currently running non-preemptible unit.
|
|
165
|
+
// While both lanes remain queued, alternate one regular operation after
|
|
166
|
+
// each hydrate (K=1), preserving FIFO order within each lane.
|
|
167
|
+
if (this.hydrateQueue.length > 0) {
|
|
168
|
+
if (
|
|
169
|
+
this.regularQueue.length > 0 &&
|
|
170
|
+
this.lastCompletedKind === `hydrate`
|
|
171
|
+
) {
|
|
172
|
+
return this.regularQueue.shift()
|
|
173
|
+
}
|
|
174
|
+
return this.hydrateQueue.shift()
|
|
175
|
+
}
|
|
176
|
+
return this.regularQueue.shift()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sharedPersistenceSchedulers = new WeakMap<
|
|
181
|
+
object,
|
|
182
|
+
SharedPersistenceScheduler
|
|
183
|
+
>()
|
|
184
|
+
const observedDriverSchedulingKeys = new WeakMap<object, object>()
|
|
185
|
+
|
|
186
|
+
function getSharedPersistenceScheduler(
|
|
187
|
+
key: object,
|
|
188
|
+
): SharedPersistenceScheduler {
|
|
189
|
+
let scheduler = sharedPersistenceSchedulers.get(key)
|
|
190
|
+
if (!scheduler) {
|
|
191
|
+
scheduler = new SharedPersistenceScheduler()
|
|
192
|
+
sharedPersistenceSchedulers.set(key, scheduler)
|
|
193
|
+
}
|
|
194
|
+
return scheduler
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getSharedLogicalSchedulingKey(value: unknown): object | undefined {
|
|
198
|
+
if ((typeof value !== `object` && typeof value !== `function`) || !value) {
|
|
199
|
+
return undefined
|
|
200
|
+
}
|
|
201
|
+
const key = (
|
|
202
|
+
value as {
|
|
203
|
+
[SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY]?: unknown
|
|
204
|
+
}
|
|
205
|
+
)[SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY]
|
|
206
|
+
return key !== null && (typeof key === `object` || typeof key === `function`)
|
|
207
|
+
? key
|
|
208
|
+
: undefined
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function observeSharedLogicalSchedulingSupport(
|
|
212
|
+
driver: SQLiteDriver,
|
|
213
|
+
onSupport: (key: object) => void,
|
|
214
|
+
): SQLiteDriver {
|
|
215
|
+
let observationPending = true
|
|
216
|
+
const observe = <T>(promise: Promise<T>): Promise<T> => {
|
|
217
|
+
if (!observationPending) return promise
|
|
218
|
+
observationPending = false
|
|
219
|
+
const key = getSharedLogicalSchedulingKey(promise)
|
|
220
|
+
if (key) onSupport(key)
|
|
221
|
+
return promise
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
exec: (sql) => observe(driver.exec(sql)),
|
|
226
|
+
query: <T>(sql: string, params: ReadonlyArray<unknown> = []) =>
|
|
227
|
+
observe(driver.query<T>(sql, params)),
|
|
228
|
+
run: (sql, params = []) => observe(driver.run(sql, params)),
|
|
229
|
+
transaction: <T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>) =>
|
|
230
|
+
observe(driver.transaction(fn)),
|
|
231
|
+
transactionWithDriver: <T>(
|
|
232
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<T>,
|
|
233
|
+
) =>
|
|
234
|
+
observe(
|
|
235
|
+
driver.transactionWithDriver
|
|
236
|
+
? driver.transactionWithDriver(fn)
|
|
237
|
+
: driver.transaction(fn),
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
74
242
|
const DEFAULT_SCHEMA_VERSION = 1
|
|
75
243
|
const DEFAULT_PULL_SINCE_RELOAD_THRESHOLD = 128
|
|
76
244
|
|
|
@@ -91,9 +259,6 @@ export const DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = 24 * 60 * 60
|
|
|
91
259
|
const SQLITE_MAX_IN_BATCH_SIZE = 900
|
|
92
260
|
const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
|
93
261
|
const FORBIDDEN_SQL_FRAGMENT_PATTERN = /(;|--|\/\*)/
|
|
94
|
-
const PERSISTED_TYPE_TAG = `__tanstack_db_persisted_type__`
|
|
95
|
-
const PERSISTED_VALUE_TAG = `value`
|
|
96
|
-
|
|
97
262
|
type CompiledValueKind = `unknown` | `bigint` | `date` | `datetime`
|
|
98
263
|
type PersistedTaggedValueType =
|
|
99
264
|
| `bigint`
|
|
@@ -121,17 +286,17 @@ function isDuplicateColumnAddError(
|
|
|
121
286
|
error: unknown,
|
|
122
287
|
columnName: string,
|
|
123
288
|
): boolean {
|
|
124
|
-
if (!(error instanceof Error)) {
|
|
289
|
+
if (typeof error !== `string` && !(error instanceof Error)) {
|
|
125
290
|
return false
|
|
126
291
|
}
|
|
127
292
|
|
|
128
|
-
const
|
|
129
|
-
|
|
293
|
+
const normalizedMessage = (
|
|
294
|
+
typeof error === `string` ? error : error.message
|
|
295
|
+
).toLowerCase()
|
|
130
296
|
return (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
message.includes(normalizedColumnName))
|
|
297
|
+
normalizedMessage.includes(columnName.toLowerCase()) &&
|
|
298
|
+
(normalizedMessage.includes(`duplicate column name`) ||
|
|
299
|
+
normalizedMessage.includes(`already exists`))
|
|
135
300
|
)
|
|
136
301
|
}
|
|
137
302
|
|
|
@@ -171,10 +336,7 @@ function encodePersistedJsonValue(value: unknown): unknown {
|
|
|
171
336
|
}
|
|
172
337
|
|
|
173
338
|
if (typeof value === `bigint`) {
|
|
174
|
-
return
|
|
175
|
-
[PERSISTED_TYPE_TAG]: `bigint`,
|
|
176
|
-
[PERSISTED_VALUE_TAG]: value.toString(),
|
|
177
|
-
} satisfies PersistedTaggedValue
|
|
339
|
+
return serializeSQLiteBigInt(value) satisfies PersistedTaggedValue
|
|
178
340
|
}
|
|
179
341
|
|
|
180
342
|
if (value instanceof Date) {
|
|
@@ -287,6 +449,7 @@ function toSqliteParameterValue(value: unknown): SqliteSupportedValue {
|
|
|
287
449
|
}
|
|
288
450
|
|
|
289
451
|
if (typeof value === `bigint`) {
|
|
452
|
+
assertSQLiteBigIntInRange(value)
|
|
290
453
|
return value.toString()
|
|
291
454
|
}
|
|
292
455
|
|
|
@@ -317,79 +480,15 @@ function toSqliteLiteral(value: SqliteSupportedValue): string {
|
|
|
317
480
|
return `'${value.replace(/'/g, `''`)}'`
|
|
318
481
|
}
|
|
319
482
|
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
): string {
|
|
324
|
-
let index = 0
|
|
325
|
-
const inlinedSql = sql.replace(/\?/g, () => {
|
|
326
|
-
const paramValue = params[index]
|
|
327
|
-
index++
|
|
328
|
-
return toSqliteLiteral(paramValue ?? null)
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
if (index !== params.length) {
|
|
332
|
-
throw new InvalidPersistedCollectionConfigError(
|
|
333
|
-
`Unable to inline SQL params; placeholder count did not match provided params`,
|
|
334
|
-
)
|
|
483
|
+
function toSqliteExpressionLiteral(value: unknown): string {
|
|
484
|
+
if (typeof value === `bigint`) {
|
|
485
|
+
return assertSQLiteBigIntInRange(value).toString()
|
|
335
486
|
}
|
|
336
|
-
|
|
337
|
-
return inlinedSql
|
|
487
|
+
return toSqliteLiteral(toSqliteParameterValue(value))
|
|
338
488
|
}
|
|
339
489
|
|
|
340
490
|
type CompiledRowExpressionEvaluator = (row: Record<string, unknown>) => unknown
|
|
341
491
|
|
|
342
|
-
function collectAliasQualifiedRefSegments(
|
|
343
|
-
expression: IR.BasicExpression,
|
|
344
|
-
segments: Set<string> = new Set<string>(),
|
|
345
|
-
): Set<string> {
|
|
346
|
-
if (expression.type === `ref`) {
|
|
347
|
-
if (expression.path.length > 1) {
|
|
348
|
-
const rootSegment = String(expression.path[0])
|
|
349
|
-
if (rootSegment.length > 0) {
|
|
350
|
-
segments.add(rootSegment)
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return segments
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (expression.type === `func`) {
|
|
357
|
-
for (const arg of expression.args) {
|
|
358
|
-
collectAliasQualifiedRefSegments(arg, segments)
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return segments
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function createAliasAwareRowProxy(
|
|
366
|
-
row: Record<string, unknown>,
|
|
367
|
-
aliasSegments: ReadonlySet<string>,
|
|
368
|
-
): Record<string, unknown> {
|
|
369
|
-
return new Proxy(row, {
|
|
370
|
-
get(target, prop, receiver) {
|
|
371
|
-
if (typeof prop !== `string`) {
|
|
372
|
-
return Reflect.get(target, prop, receiver)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (Object.prototype.hasOwnProperty.call(target, prop)) {
|
|
376
|
-
const value = Reflect.get(target, prop, receiver)
|
|
377
|
-
if (value !== undefined || !aliasSegments.has(prop)) {
|
|
378
|
-
return value
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return target
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (aliasSegments.has(prop)) {
|
|
385
|
-
return target
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return undefined
|
|
389
|
-
},
|
|
390
|
-
})
|
|
391
|
-
}
|
|
392
|
-
|
|
393
492
|
function compileRowExpressionEvaluator(
|
|
394
493
|
expression: IR.BasicExpression,
|
|
395
494
|
): CompiledRowExpressionEvaluator {
|
|
@@ -401,24 +500,7 @@ function compileRowExpressionEvaluator(
|
|
|
401
500
|
`Unsupported expression for SQLite adapter fallback evaluator: ${(error as Error).message}`,
|
|
402
501
|
)
|
|
403
502
|
}
|
|
404
|
-
|
|
405
|
-
const aliasSegments = collectAliasQualifiedRefSegments(expression)
|
|
406
|
-
if (aliasSegments.size === 0) {
|
|
407
|
-
return (row) => baseEvaluator(row)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const proxyCache = new WeakMap<
|
|
411
|
-
Record<string, unknown>,
|
|
412
|
-
Record<string, unknown>
|
|
413
|
-
>()
|
|
414
|
-
return (row) => {
|
|
415
|
-
let proxy = proxyCache.get(row)
|
|
416
|
-
if (!proxy) {
|
|
417
|
-
proxy = createAliasAwareRowProxy(row, aliasSegments)
|
|
418
|
-
proxyCache.set(row, proxy)
|
|
419
|
-
}
|
|
420
|
-
return baseEvaluator(proxy)
|
|
421
|
-
}
|
|
503
|
+
return baseEvaluator
|
|
422
504
|
}
|
|
423
505
|
|
|
424
506
|
function getOrderByObjectId(value: object): number {
|
|
@@ -449,6 +531,9 @@ function compareOrderByValues(
|
|
|
449
531
|
}
|
|
450
532
|
|
|
451
533
|
if (typeof left === `string` && typeof right === `string`) {
|
|
534
|
+
if (compareOptions.stringSort === `custom`) {
|
|
535
|
+
return compareOptions.compare(left, right)
|
|
536
|
+
}
|
|
452
537
|
if (compareOptions.stringSort === `locale`) {
|
|
453
538
|
return left.localeCompare(
|
|
454
539
|
right,
|
|
@@ -575,37 +660,50 @@ function resolveComparisonValueKind(
|
|
|
575
660
|
|
|
576
661
|
function compileComparisonSql(
|
|
577
662
|
operator: `=` | `>` | `>=` | `<` | `<=`,
|
|
663
|
+
leftExpression: IR.BasicExpression,
|
|
664
|
+
rightExpression: IR.BasicExpression,
|
|
578
665
|
leftSql: string,
|
|
579
666
|
rightSql: string,
|
|
580
667
|
valueKind: CompiledValueKind,
|
|
668
|
+
leftKind: CompiledValueKind,
|
|
669
|
+
rightKind: CompiledValueKind,
|
|
581
670
|
): string {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
671
|
+
const compileOperand = (
|
|
672
|
+
expression: IR.BasicExpression,
|
|
673
|
+
sql: string,
|
|
674
|
+
otherKind: CompiledValueKind,
|
|
675
|
+
): string => {
|
|
676
|
+
if (expression.type !== `val`) return sql
|
|
677
|
+
if (valueKind === `date` && otherKind === `date`) return `date(${sql})`
|
|
678
|
+
if (valueKind === `datetime` && otherKind === `datetime`) {
|
|
679
|
+
return `datetime(${sql})`
|
|
680
|
+
}
|
|
681
|
+
return sql
|
|
590
682
|
}
|
|
591
|
-
|
|
683
|
+
|
|
684
|
+
return `(${compileOperand(leftExpression, leftSql, rightKind)} ${operator} ${compileOperand(rightExpression, rightSql, leftKind)})`
|
|
592
685
|
}
|
|
593
686
|
|
|
594
687
|
function compileRefExpressionSql(jsonPath: string): CompiledSqlFragment {
|
|
595
688
|
const typePath = `${jsonPath}.${PERSISTED_TYPE_TAG}`
|
|
596
689
|
const taggedValuePath = `${jsonPath}.${PERSISTED_VALUE_TAG}`
|
|
690
|
+
// createJsonPath has already validated every segment. Keep these paths as
|
|
691
|
+
// canonical SQL literals so runtime refs match persisted expression indexes.
|
|
692
|
+
const typePathSql = toSqliteLiteral(typePath)
|
|
693
|
+
const taggedValuePathSql = toSqliteLiteral(taggedValuePath)
|
|
694
|
+
const jsonPathSql = toSqliteLiteral(jsonPath)
|
|
597
695
|
|
|
598
696
|
return {
|
|
599
697
|
supported: true,
|
|
600
|
-
sql: `(CASE json_extract(value,
|
|
601
|
-
WHEN 'bigint' THEN CAST(json_extract(value,
|
|
602
|
-
WHEN 'date' THEN json_extract(value,
|
|
698
|
+
sql: `(CASE json_extract(value, ${typePathSql})
|
|
699
|
+
WHEN 'bigint' THEN CAST(json_extract(value, ${taggedValuePathSql}) AS NUMERIC)
|
|
700
|
+
WHEN 'date' THEN json_extract(value, ${taggedValuePathSql})
|
|
603
701
|
WHEN 'nan' THEN NULL
|
|
604
702
|
WHEN 'infinity' THEN NULL
|
|
605
703
|
WHEN '-infinity' THEN NULL
|
|
606
|
-
ELSE json_extract(value,
|
|
704
|
+
ELSE json_extract(value, ${jsonPathSql})
|
|
607
705
|
END)`,
|
|
608
|
-
params: [
|
|
706
|
+
params: [],
|
|
609
707
|
valueKind: `unknown`,
|
|
610
708
|
}
|
|
611
709
|
}
|
|
@@ -656,21 +754,64 @@ function stableStringify(value: unknown): string {
|
|
|
656
754
|
return serializePersistedRowValue(value)
|
|
657
755
|
}
|
|
658
756
|
|
|
757
|
+
function argumentCompilationContext(
|
|
758
|
+
parentName: string,
|
|
759
|
+
argumentIndex: number,
|
|
760
|
+
argument: IR.BasicExpression,
|
|
761
|
+
parentContext: SqlExpressionCompilationContext,
|
|
762
|
+
): SqlExpressionCompilationContext {
|
|
763
|
+
if (parentContext === `index-expression`) return `index-expression`
|
|
764
|
+
|
|
765
|
+
switch (parentName) {
|
|
766
|
+
case `and`:
|
|
767
|
+
case `or`:
|
|
768
|
+
case `not`:
|
|
769
|
+
return `predicate`
|
|
770
|
+
case `eq`:
|
|
771
|
+
case `gt`:
|
|
772
|
+
case `gte`:
|
|
773
|
+
case `lt`:
|
|
774
|
+
case `lte`:
|
|
775
|
+
case `like`:
|
|
776
|
+
case `ilike`:
|
|
777
|
+
if (argument.type !== `val`) return `index-expression`
|
|
778
|
+
return typeof argument.value === `bigint`
|
|
779
|
+
? `index-expression`
|
|
780
|
+
: `predicate`
|
|
781
|
+
case `in`:
|
|
782
|
+
return argumentIndex === 0 ? `index-expression` : `predicate`
|
|
783
|
+
case `isNull`:
|
|
784
|
+
case `isUndefined`:
|
|
785
|
+
return `index-expression`
|
|
786
|
+
default:
|
|
787
|
+
return `index-expression`
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
659
791
|
function compileSqlExpression(
|
|
660
792
|
expression: IR.BasicExpression,
|
|
793
|
+
context: SqlExpressionCompilationContext = `predicate`,
|
|
661
794
|
): CompiledSqlFragment {
|
|
662
795
|
if (expression.type === `val`) {
|
|
663
796
|
const valueKind = getLiteralValueKind(expression.value)
|
|
664
797
|
return {
|
|
665
798
|
supported: true,
|
|
666
|
-
sql:
|
|
667
|
-
|
|
799
|
+
sql:
|
|
800
|
+
context === `index-expression`
|
|
801
|
+
? toSqliteExpressionLiteral(expression.value)
|
|
802
|
+
: `?`,
|
|
803
|
+
params:
|
|
804
|
+
context === `predicate`
|
|
805
|
+
? [toSqliteParameterValue(expression.value)]
|
|
806
|
+
: [],
|
|
668
807
|
valueKind,
|
|
669
808
|
}
|
|
670
809
|
}
|
|
671
810
|
|
|
672
811
|
if (expression.type === `ref`) {
|
|
673
|
-
const jsonPath = createJsonPath(
|
|
812
|
+
const jsonPath = createJsonPath(
|
|
813
|
+
IR.getPropRefPropertyPath(expression).map(String),
|
|
814
|
+
)
|
|
674
815
|
if (!jsonPath) {
|
|
675
816
|
return {
|
|
676
817
|
supported: false,
|
|
@@ -682,7 +823,12 @@ function compileSqlExpression(
|
|
|
682
823
|
return compileRefExpressionSql(jsonPath)
|
|
683
824
|
}
|
|
684
825
|
|
|
685
|
-
const compiledArgs = expression.args.map((arg) =>
|
|
826
|
+
const compiledArgs = expression.args.map((arg, index) =>
|
|
827
|
+
compileSqlExpression(
|
|
828
|
+
arg,
|
|
829
|
+
argumentCompilationContext(expression.name, index, arg, context),
|
|
830
|
+
),
|
|
831
|
+
)
|
|
686
832
|
if (compiledArgs.some((arg) => !arg.supported)) {
|
|
687
833
|
return {
|
|
688
834
|
supported: false,
|
|
@@ -731,9 +877,13 @@ function compileSqlExpression(
|
|
|
731
877
|
supported: true,
|
|
732
878
|
sql: compileComparisonSql(
|
|
733
879
|
operatorByName[expression.name],
|
|
880
|
+
expression.args[0]!,
|
|
881
|
+
expression.args[1]!,
|
|
734
882
|
argSql[0],
|
|
735
883
|
argSql[1],
|
|
736
884
|
valueKind,
|
|
885
|
+
getCompiledValueKind(compiledArgs[0]),
|
|
886
|
+
getCompiledValueKind(compiledArgs[1]),
|
|
737
887
|
),
|
|
738
888
|
params,
|
|
739
889
|
}
|
|
@@ -788,13 +938,17 @@ function compileSqlExpression(
|
|
|
788
938
|
return { supported: false, sql: ``, params: [] }
|
|
789
939
|
}
|
|
790
940
|
|
|
941
|
+
if (context === `index-expression`) {
|
|
942
|
+
return {
|
|
943
|
+
supported: true,
|
|
944
|
+
sql: `(${leftSql} IN (${listValue
|
|
945
|
+
.map((value) => toSqliteExpressionLiteral(value))
|
|
946
|
+
.join(`, `)}))`,
|
|
947
|
+
params: leftParams,
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
791
951
|
if (listValue.length > SQLITE_MAX_IN_BATCH_SIZE) {
|
|
792
|
-
const hasBigIntValues = listValue.some(
|
|
793
|
-
(value) => typeof value === `bigint`,
|
|
794
|
-
)
|
|
795
|
-
const inLeftSql = hasBigIntValues
|
|
796
|
-
? `CAST(${leftSql} AS NUMERIC)`
|
|
797
|
-
: leftSql
|
|
798
952
|
const chunkClauses: Array<string> = []
|
|
799
953
|
const batchedParams: Array<SqliteSupportedValue> = []
|
|
800
954
|
|
|
@@ -807,13 +961,13 @@ function compileSqlExpression(
|
|
|
807
961
|
startIndex,
|
|
808
962
|
startIndex + SQLITE_MAX_IN_BATCH_SIZE,
|
|
809
963
|
)
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
)
|
|
964
|
+
const chunkParams: Array<SqliteSupportedValue> = []
|
|
965
|
+
const chunkValueSql = chunkValues.map((value) => {
|
|
966
|
+
chunkParams.push(toSqliteParameterValue(value))
|
|
967
|
+
return typeof value === `bigint` ? `CAST(? AS NUMERIC)` : `?`
|
|
968
|
+
})
|
|
969
|
+
chunkClauses.push(`(${leftSql} IN (${chunkValueSql.join(`, `)}))`)
|
|
970
|
+
batchedParams.push(...leftParams, ...chunkParams)
|
|
817
971
|
}
|
|
818
972
|
|
|
819
973
|
return {
|
|
@@ -823,20 +977,15 @@ function compileSqlExpression(
|
|
|
823
977
|
}
|
|
824
978
|
}
|
|
825
979
|
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
: leftSql
|
|
832
|
-
const listPlaceholders = listValue.map(() => `?`).join(`, `)
|
|
980
|
+
const listParams: Array<SqliteSupportedValue> = []
|
|
981
|
+
const listValueSql = listValue.map((value) => {
|
|
982
|
+
listParams.push(toSqliteParameterValue(value))
|
|
983
|
+
return typeof value === `bigint` ? `CAST(? AS NUMERIC)` : `?`
|
|
984
|
+
})
|
|
833
985
|
return {
|
|
834
986
|
supported: true,
|
|
835
|
-
sql: `(${
|
|
836
|
-
params: [
|
|
837
|
-
...leftParams,
|
|
838
|
-
...listValue.map((value) => toSqliteParameterValue(value)),
|
|
839
|
-
],
|
|
987
|
+
sql: `(${leftSql} IN (${listValueSql.join(`, `)}))`,
|
|
988
|
+
params: [...leftParams, ...listParams],
|
|
840
989
|
}
|
|
841
990
|
}
|
|
842
991
|
case `like`:
|
|
@@ -916,7 +1065,10 @@ function compileOrderByClauses(
|
|
|
916
1065
|
const params: Array<SqliteSupportedValue> = []
|
|
917
1066
|
|
|
918
1067
|
for (const clause of orderBy) {
|
|
919
|
-
const compiledExpression = compileSqlExpression(
|
|
1068
|
+
const compiledExpression = compileSqlExpression(
|
|
1069
|
+
clause.expression,
|
|
1070
|
+
`index-expression`,
|
|
1071
|
+
)
|
|
920
1072
|
if (!compiledExpression.supported) {
|
|
921
1073
|
return {
|
|
922
1074
|
supported: false,
|
|
@@ -952,6 +1104,7 @@ function isExpressionLikeShape(value: unknown): value is IR.BasicExpression {
|
|
|
952
1104
|
path?: unknown
|
|
953
1105
|
name?: unknown
|
|
954
1106
|
args?: unknown
|
|
1107
|
+
sourceAlias?: unknown
|
|
955
1108
|
}
|
|
956
1109
|
|
|
957
1110
|
if (candidate.type === `val`) {
|
|
@@ -959,7 +1112,12 @@ function isExpressionLikeShape(value: unknown): value is IR.BasicExpression {
|
|
|
959
1112
|
}
|
|
960
1113
|
|
|
961
1114
|
if (candidate.type === `ref`) {
|
|
962
|
-
return
|
|
1115
|
+
return (
|
|
1116
|
+
Array.isArray(candidate.path) &&
|
|
1117
|
+
(candidate.sourceAlias === undefined ||
|
|
1118
|
+
(typeof candidate.sourceAlias === `string` &&
|
|
1119
|
+
candidate.path[0] === candidate.sourceAlias))
|
|
1120
|
+
)
|
|
963
1121
|
}
|
|
964
1122
|
|
|
965
1123
|
if (candidate.type === `func`) {
|
|
@@ -989,14 +1147,22 @@ function normalizeIndexSqlFragment(fragment: string): string {
|
|
|
989
1147
|
// Non-JSON strings are treated as raw SQL fragments below.
|
|
990
1148
|
}
|
|
991
1149
|
|
|
992
|
-
|
|
993
|
-
|
|
1150
|
+
const decodedJson = hasParsedJson
|
|
1151
|
+
? decodePersistedJsonValue(parsedJson)
|
|
1152
|
+
: undefined
|
|
1153
|
+
if (hasParsedJson && isExpressionLikeShape(decodedJson)) {
|
|
1154
|
+
const compiled = compileSqlExpression(decodedJson, `index-expression`)
|
|
994
1155
|
if (!compiled.supported) {
|
|
995
1156
|
throw new InvalidPersistedCollectionConfigError(
|
|
996
1157
|
`Persisted index expression is not supported by the SQLite compiler`,
|
|
997
1158
|
)
|
|
998
1159
|
}
|
|
999
|
-
|
|
1160
|
+
if (compiled.params.length !== 0) {
|
|
1161
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1162
|
+
`Persisted index expression cannot contain bound parameters`,
|
|
1163
|
+
)
|
|
1164
|
+
}
|
|
1165
|
+
return compiled.sql
|
|
1000
1166
|
}
|
|
1001
1167
|
|
|
1002
1168
|
return sanitizeExpressionSqlFragment(fragment)
|
|
@@ -1023,11 +1189,16 @@ function buildIndexName(collectionId: string, signature: string): string {
|
|
|
1023
1189
|
|
|
1024
1190
|
export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
1025
1191
|
private readonly driver: SQLiteDriver
|
|
1192
|
+
private readonly schedulingIdentitySource: SQLiteDriver
|
|
1193
|
+
private scheduler: SharedPersistenceScheduler | undefined
|
|
1194
|
+
private activeUnscheduledHydration: Promise<unknown> | undefined
|
|
1195
|
+
private readonly hydrationAdapter: HydrationPersistenceAdapter
|
|
1026
1196
|
private readonly schemaVersion: number
|
|
1027
1197
|
private readonly schemaMismatchPolicy: SQLiteCoreAdapterSchemaMismatchPolicy
|
|
1028
1198
|
private readonly appliedTxPruneMaxRows: number | undefined
|
|
1029
1199
|
private readonly appliedTxPruneMaxAgeSeconds: number | undefined
|
|
1030
1200
|
private readonly pullSinceReloadThreshold: number
|
|
1201
|
+
private readonly replacementBatchSize: number
|
|
1031
1202
|
|
|
1032
1203
|
private initialized = false
|
|
1033
1204
|
private readonly collectionTableCache = new Map<
|
|
@@ -1040,6 +1211,20 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1040
1211
|
>()
|
|
1041
1212
|
|
|
1042
1213
|
constructor(options: SQLiteCoreAdapterOptions) {
|
|
1214
|
+
const maxBoundParameters = options.driver.maxBoundParameters
|
|
1215
|
+
if (
|
|
1216
|
+
maxBoundParameters !== undefined &&
|
|
1217
|
+
(!Number.isInteger(maxBoundParameters) || maxBoundParameters < 4)
|
|
1218
|
+
) {
|
|
1219
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1220
|
+
`SQLite driver maxBoundParameters must be an integer of at least 4`,
|
|
1221
|
+
)
|
|
1222
|
+
}
|
|
1223
|
+
this.replacementBatchSize =
|
|
1224
|
+
maxBoundParameters === undefined
|
|
1225
|
+
? REPLACEMENT_BATCH_SIZE
|
|
1226
|
+
: Math.min(REPLACEMENT_BATCH_SIZE, Math.floor(maxBoundParameters / 4))
|
|
1227
|
+
|
|
1043
1228
|
const schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION
|
|
1044
1229
|
if (!Number.isInteger(schemaVersion) || schemaVersion < 0) {
|
|
1045
1230
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -1078,13 +1263,93 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1078
1263
|
)
|
|
1079
1264
|
}
|
|
1080
1265
|
|
|
1081
|
-
this.
|
|
1266
|
+
this.schedulingIdentitySource = options.driver
|
|
1267
|
+
const schedulingKey =
|
|
1268
|
+
getSharedLogicalSchedulingKey(options.driver) ??
|
|
1269
|
+
observedDriverSchedulingKeys.get(options.driver)
|
|
1270
|
+
this.scheduler = schedulingKey
|
|
1271
|
+
? getSharedPersistenceScheduler(schedulingKey)
|
|
1272
|
+
: undefined
|
|
1273
|
+
this.driver = schedulingKey
|
|
1274
|
+
? options.driver
|
|
1275
|
+
: observeSharedLogicalSchedulingSupport(options.driver, (key) => {
|
|
1276
|
+
observedDriverSchedulingKeys.set(options.driver, key)
|
|
1277
|
+
const scheduler = getSharedPersistenceScheduler(key)
|
|
1278
|
+
this.scheduler ??= scheduler
|
|
1279
|
+
if (this.activeUnscheduledHydration) {
|
|
1280
|
+
scheduler.adoptRunningHydrate(this.activeUnscheduledHydration)
|
|
1281
|
+
}
|
|
1282
|
+
})
|
|
1082
1283
|
this.schemaVersion = schemaVersion
|
|
1083
1284
|
this.schemaMismatchPolicy =
|
|
1084
1285
|
options.schemaMismatchPolicy ?? `sync-present-reset`
|
|
1085
1286
|
this.appliedTxPruneMaxRows = options.appliedTxPruneMaxRows
|
|
1086
1287
|
this.appliedTxPruneMaxAgeSeconds = options.appliedTxPruneMaxAgeSeconds
|
|
1087
1288
|
this.pullSinceReloadThreshold = pullSinceReloadThreshold
|
|
1289
|
+
this.hydrationAdapter = {
|
|
1290
|
+
loadSubset: (collectionId, loadOptions, context) =>
|
|
1291
|
+
this.loadSubsetUnscheduled(collectionId, loadOptions, context),
|
|
1292
|
+
loadResumeSnapshot: (collectionId, context) =>
|
|
1293
|
+
this.loadResumeSnapshotUnscheduled(collectionId, context),
|
|
1294
|
+
applyCommittedTx: (collectionId, tx) =>
|
|
1295
|
+
this.applyCommittedTxUnscheduled(collectionId, tx),
|
|
1296
|
+
loadCollectionMetadata: (collectionId) =>
|
|
1297
|
+
this.loadCollectionMetadataUnscheduled(collectionId),
|
|
1298
|
+
scanRows: (collectionId, scanOptions) =>
|
|
1299
|
+
this.scanRowsUnscheduled(collectionId, scanOptions),
|
|
1300
|
+
ensureIndex: (collectionId, signature, spec) =>
|
|
1301
|
+
this.ensureIndexUnscheduled(collectionId, signature, spec),
|
|
1302
|
+
markIndexRemoved: (collectionId, signature) =>
|
|
1303
|
+
this.markIndexRemovedUnscheduled(collectionId, signature),
|
|
1304
|
+
getStreamPosition: (collectionId) =>
|
|
1305
|
+
this.getStreamPositionUnscheduled(collectionId),
|
|
1306
|
+
pullSince: (collectionId, fromRowVersion) =>
|
|
1307
|
+
this.pullSinceUnscheduled(collectionId, fromRowVersion),
|
|
1308
|
+
runInHydrationScope: async (task) => task(this.hydrationAdapter),
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
runInHydrationScope<T>(
|
|
1313
|
+
task: (adapter: HydrationPersistenceAdapter) => Promise<T>,
|
|
1314
|
+
): Promise<T> {
|
|
1315
|
+
const scheduler = this.resolveScheduler()
|
|
1316
|
+
if (scheduler) {
|
|
1317
|
+
return scheduler.runHydrate(() => task(this.hydrationAdapter))
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const hydration = Promise.resolve().then(() => task(this.hydrationAdapter))
|
|
1321
|
+
this.activeUnscheduledHydration = hydration
|
|
1322
|
+
const clear = () => {
|
|
1323
|
+
if (this.activeUnscheduledHydration === hydration) {
|
|
1324
|
+
this.activeUnscheduledHydration = undefined
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
void hydration.then(clear, clear)
|
|
1328
|
+
return hydration
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
runInRegularScope<T>(
|
|
1332
|
+
task: (adapter: HydrationPersistenceAdapter) => Promise<T>,
|
|
1333
|
+
): Promise<T> {
|
|
1334
|
+
return this.runRegular(() => task(this.hydrationAdapter))
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
isHydrationScopeScheduled(): boolean {
|
|
1338
|
+
return this.resolveScheduler() !== undefined
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
private runRegular<T>(task: () => Promise<T>): Promise<T> {
|
|
1342
|
+
const scheduler = this.resolveScheduler()
|
|
1343
|
+
return scheduler ? scheduler.runRegular(task) : task()
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
private resolveScheduler(): SharedPersistenceScheduler | undefined {
|
|
1347
|
+
if (this.scheduler) return this.scheduler
|
|
1348
|
+
const key = observedDriverSchedulingKeys.get(this.schedulingIdentitySource)
|
|
1349
|
+
if (key) {
|
|
1350
|
+
this.scheduler = getSharedPersistenceScheduler(key)
|
|
1351
|
+
}
|
|
1352
|
+
return this.scheduler
|
|
1088
1353
|
}
|
|
1089
1354
|
|
|
1090
1355
|
private runInTransaction<TResult>(
|
|
@@ -1097,7 +1362,45 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1097
1362
|
return this.driver.transaction(fn)
|
|
1098
1363
|
}
|
|
1099
1364
|
|
|
1100
|
-
async
|
|
1365
|
+
private async assertCurrentSchemaVersion(
|
|
1366
|
+
collectionId: string,
|
|
1367
|
+
driver: SQLiteDriver,
|
|
1368
|
+
operation: string,
|
|
1369
|
+
): Promise<void> {
|
|
1370
|
+
const schemaRows = await driver.query<{ schema_version: number }>(
|
|
1371
|
+
`SELECT schema_version
|
|
1372
|
+
FROM collection_registry
|
|
1373
|
+
WHERE collection_id = ?
|
|
1374
|
+
LIMIT 1`,
|
|
1375
|
+
[collectionId],
|
|
1376
|
+
)
|
|
1377
|
+
const persistedSchemaVersion = schemaRows[0]?.schema_version
|
|
1378
|
+
if (persistedSchemaVersion !== this.schemaVersion) {
|
|
1379
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1380
|
+
`Schema version mismatch for collection "${collectionId}": ` +
|
|
1381
|
+
`found ${persistedSchemaVersion ?? `missing`}, expected ${this.schemaVersion}. ` +
|
|
1382
|
+
`Refusing to ${operation} through a stale cached adapter.`,
|
|
1383
|
+
)
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
loadSubset(
|
|
1388
|
+
collectionId: string,
|
|
1389
|
+
options: LoadSubsetOptions,
|
|
1390
|
+
ctx?: { requiredIndexSignatures?: ReadonlyArray<string> },
|
|
1391
|
+
): Promise<
|
|
1392
|
+
Array<{
|
|
1393
|
+
key: string | number
|
|
1394
|
+
value: Record<string, unknown>
|
|
1395
|
+
metadata?: unknown
|
|
1396
|
+
}>
|
|
1397
|
+
> {
|
|
1398
|
+
return this.runRegular(() =>
|
|
1399
|
+
this.loadSubsetUnscheduled(collectionId, options, ctx),
|
|
1400
|
+
)
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
private async loadSubsetUnscheduled(
|
|
1101
1404
|
collectionId: string,
|
|
1102
1405
|
options: LoadSubsetOptions,
|
|
1103
1406
|
ctx?: { requiredIndexSignatures?: ReadonlyArray<string> },
|
|
@@ -1109,86 +1412,239 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1109
1412
|
}>
|
|
1110
1413
|
> {
|
|
1111
1414
|
const tableMapping = await this.ensureCollectionReady(collectionId)
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
? new IR.Func(`and`, [options.where, options.cursor.whereFrom])
|
|
1124
|
-
: options.cursor.whereFrom,
|
|
1125
|
-
orderBy: options.orderBy,
|
|
1126
|
-
limit: options.limit,
|
|
1127
|
-
}
|
|
1415
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1416
|
+
await this.assertCurrentSchemaVersion(
|
|
1417
|
+
collectionId,
|
|
1418
|
+
transactionDriver,
|
|
1419
|
+
`load persisted rows`,
|
|
1420
|
+
)
|
|
1421
|
+
await this.touchRequiredIndexes(
|
|
1422
|
+
collectionId,
|
|
1423
|
+
ctx?.requiredIndexSignatures,
|
|
1424
|
+
transactionDriver,
|
|
1425
|
+
)
|
|
1128
1426
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1427
|
+
if (options.cursor) {
|
|
1428
|
+
const whereCurrentOptions: LoadSubsetOptions = {
|
|
1429
|
+
where: options.where
|
|
1430
|
+
? new IR.Func(`and`, [options.where, options.cursor.whereCurrent])
|
|
1431
|
+
: options.cursor.whereCurrent,
|
|
1432
|
+
orderBy: options.orderBy,
|
|
1433
|
+
}
|
|
1434
|
+
const whereFromOptions: LoadSubsetOptions = {
|
|
1435
|
+
where: options.where
|
|
1436
|
+
? new IR.Func(`and`, [options.where, options.cursor.whereFrom])
|
|
1437
|
+
: options.cursor.whereFrom,
|
|
1438
|
+
orderBy: options.orderBy,
|
|
1439
|
+
limit: options.limit,
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const [whereCurrentRows, whereFromRows] = await Promise.all([
|
|
1443
|
+
this.loadSubsetInternal(
|
|
1444
|
+
tableMapping,
|
|
1445
|
+
whereCurrentOptions,
|
|
1446
|
+
transactionDriver,
|
|
1447
|
+
),
|
|
1448
|
+
this.loadSubsetInternal(
|
|
1449
|
+
tableMapping,
|
|
1450
|
+
whereFromOptions,
|
|
1451
|
+
transactionDriver,
|
|
1452
|
+
),
|
|
1453
|
+
])
|
|
1454
|
+
|
|
1455
|
+
const mergedRows = new Map<
|
|
1456
|
+
string,
|
|
1457
|
+
InMemoryRow<string | number, Record<string, unknown>>
|
|
1458
|
+
>()
|
|
1459
|
+
for (const row of [...whereCurrentRows, ...whereFromRows]) {
|
|
1460
|
+
mergedRows.set(encodePersistedStorageKey(row.key), row)
|
|
1461
|
+
}
|
|
1133
1462
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1463
|
+
const orderedRows = this.applyInMemoryOrderBy(
|
|
1464
|
+
Array.from(mergedRows.values()),
|
|
1465
|
+
options.orderBy,
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
return orderedRows.map((row) => ({
|
|
1469
|
+
key: row.key,
|
|
1470
|
+
value: row.value,
|
|
1471
|
+
metadata: row.metadata,
|
|
1472
|
+
}))
|
|
1140
1473
|
}
|
|
1141
1474
|
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
options
|
|
1475
|
+
const rows = await this.loadSubsetInternal(
|
|
1476
|
+
tableMapping,
|
|
1477
|
+
options,
|
|
1478
|
+
transactionDriver,
|
|
1145
1479
|
)
|
|
1146
|
-
|
|
1147
|
-
return orderedRows.map((row) => ({
|
|
1480
|
+
return rows.map((row) => ({
|
|
1148
1481
|
key: row.key,
|
|
1149
1482
|
value: row.value,
|
|
1150
1483
|
metadata: row.metadata,
|
|
1151
1484
|
}))
|
|
1152
|
-
}
|
|
1485
|
+
})
|
|
1486
|
+
}
|
|
1153
1487
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
}
|
|
1488
|
+
loadResumeSnapshot(
|
|
1489
|
+
collectionId: string,
|
|
1490
|
+
ctx?: {
|
|
1491
|
+
requiredIndexSignatures?: ReadonlyArray<string>
|
|
1492
|
+
includeRows?: boolean
|
|
1493
|
+
},
|
|
1494
|
+
) {
|
|
1495
|
+
return this.runRegular(() =>
|
|
1496
|
+
this.loadResumeSnapshotUnscheduled(collectionId, ctx),
|
|
1497
|
+
)
|
|
1160
1498
|
}
|
|
1161
1499
|
|
|
1162
|
-
async
|
|
1500
|
+
private async loadResumeSnapshotUnscheduled(
|
|
1501
|
+
collectionId: string,
|
|
1502
|
+
ctx?: {
|
|
1503
|
+
requiredIndexSignatures?: ReadonlyArray<string>
|
|
1504
|
+
includeRows?: boolean
|
|
1505
|
+
},
|
|
1506
|
+
): Promise<{
|
|
1507
|
+
rows: Array<{
|
|
1508
|
+
key: string | number
|
|
1509
|
+
value: Record<string, unknown>
|
|
1510
|
+
metadata?: unknown
|
|
1511
|
+
}>
|
|
1512
|
+
keySet: PersistedKeySetEvidence
|
|
1513
|
+
collectionMetadata: Array<{ key: string; value: unknown }>
|
|
1514
|
+
latestTerm: number
|
|
1515
|
+
latestSeq: number
|
|
1516
|
+
latestRowVersion: number
|
|
1517
|
+
resetEpoch: number
|
|
1518
|
+
}> {
|
|
1163
1519
|
const tableMapping = await this.ensureCollectionReady(collectionId)
|
|
1164
|
-
const
|
|
1165
|
-
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName)
|
|
1520
|
+
const includeRows = ctx?.includeRows !== false
|
|
1166
1521
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1522
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1523
|
+
await this.assertCurrentSchemaVersion(
|
|
1524
|
+
collectionId,
|
|
1525
|
+
transactionDriver,
|
|
1526
|
+
`load a resume snapshot`,
|
|
1527
|
+
)
|
|
1528
|
+
if (includeRows) {
|
|
1529
|
+
await this.touchRequiredIndexes(
|
|
1530
|
+
collectionId,
|
|
1531
|
+
ctx?.requiredIndexSignatures,
|
|
1532
|
+
transactionDriver,
|
|
1533
|
+
)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const rows = includeRows
|
|
1537
|
+
? await this.loadSubsetInternal(tableMapping, {}, transactionDriver)
|
|
1538
|
+
: []
|
|
1539
|
+
const { latestRowVersion, keySet } = await this.readKeySetEvidence(
|
|
1540
|
+
collectionId,
|
|
1541
|
+
transactionDriver,
|
|
1542
|
+
)
|
|
1543
|
+
const collectionMetadataRows = await transactionDriver.query<{
|
|
1544
|
+
key: string
|
|
1545
|
+
value: string
|
|
1546
|
+
}>(
|
|
1547
|
+
`SELECT key, value
|
|
1548
|
+
FROM collection_metadata
|
|
1549
|
+
WHERE collection_id = ?`,
|
|
1550
|
+
[collectionId],
|
|
1551
|
+
)
|
|
1552
|
+
const { latestTerm, latestSeq } = await this.readStreamPosition(
|
|
1553
|
+
collectionId,
|
|
1554
|
+
transactionDriver,
|
|
1555
|
+
)
|
|
1556
|
+
const resetRows = await transactionDriver.query<{ reset_epoch: number }>(
|
|
1557
|
+
`SELECT reset_epoch
|
|
1558
|
+
FROM collection_reset_epoch
|
|
1559
|
+
WHERE collection_id = ?
|
|
1172
1560
|
LIMIT 1`,
|
|
1173
|
-
[collectionId
|
|
1561
|
+
[collectionId],
|
|
1174
1562
|
)
|
|
1175
1563
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1564
|
+
return {
|
|
1565
|
+
rows: rows.map((row) => ({
|
|
1566
|
+
key: row.key,
|
|
1567
|
+
value: row.value,
|
|
1568
|
+
metadata: row.metadata,
|
|
1569
|
+
})),
|
|
1570
|
+
keySet,
|
|
1571
|
+
collectionMetadata: collectionMetadataRows.map((row) => ({
|
|
1572
|
+
key: row.key,
|
|
1573
|
+
value: deserializePersistedRowValue(row.value),
|
|
1574
|
+
})),
|
|
1575
|
+
latestTerm,
|
|
1576
|
+
latestSeq,
|
|
1577
|
+
latestRowVersion,
|
|
1578
|
+
resetEpoch: resetRows[0]?.reset_epoch ?? 0,
|
|
1178
1579
|
}
|
|
1580
|
+
})
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
applyCommittedTx(collectionId: string, tx: PersistedTx): Promise<void> {
|
|
1584
|
+
return this.runRegular(() =>
|
|
1585
|
+
this.applyCommittedTxUnscheduled(collectionId, tx),
|
|
1586
|
+
)
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private async applyCommittedTxUnscheduled(
|
|
1590
|
+
collectionId: string,
|
|
1591
|
+
tx: PersistedTx,
|
|
1592
|
+
): Promise<void> {
|
|
1593
|
+
const tableMapping = await this.ensureCollectionReady(collectionId)
|
|
1594
|
+
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
1595
|
+
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName)
|
|
1179
1596
|
|
|
1597
|
+
await this.runInTransaction(async (transactionDriver) => {
|
|
1180
1598
|
const versionRows = await transactionDriver.query<{
|
|
1181
1599
|
latest_row_version: number
|
|
1600
|
+
key_set_evidence_available: number
|
|
1601
|
+
schema_version: number
|
|
1602
|
+
already_applied: number
|
|
1182
1603
|
}>(
|
|
1183
|
-
`SELECT
|
|
1604
|
+
`SELECT
|
|
1605
|
+
latest_row_version,
|
|
1606
|
+
key_set_evidence_available,
|
|
1607
|
+
(
|
|
1608
|
+
SELECT schema_version
|
|
1609
|
+
FROM collection_registry
|
|
1610
|
+
WHERE collection_id = ?
|
|
1611
|
+
LIMIT 1
|
|
1612
|
+
) AS schema_version,
|
|
1613
|
+
EXISTS (
|
|
1614
|
+
SELECT 1
|
|
1615
|
+
FROM applied_tx
|
|
1616
|
+
WHERE collection_id = ? AND term = ? AND seq = ?
|
|
1617
|
+
) AS already_applied
|
|
1184
1618
|
FROM collection_version
|
|
1185
1619
|
WHERE collection_id = ?
|
|
1186
1620
|
LIMIT 1`,
|
|
1187
|
-
[collectionId],
|
|
1621
|
+
[collectionId, collectionId, tx.term, tx.seq, collectionId],
|
|
1188
1622
|
)
|
|
1189
|
-
const
|
|
1623
|
+
const version = versionRows[0]
|
|
1624
|
+
|
|
1625
|
+
if (!version) {
|
|
1626
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1627
|
+
`Missing persisted version state for collection "${collectionId}"`,
|
|
1628
|
+
)
|
|
1629
|
+
}
|
|
1630
|
+
if (version.schema_version !== this.schemaVersion) {
|
|
1631
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1632
|
+
`Schema version mismatch for collection "${collectionId}": ` +
|
|
1633
|
+
`found ${version.schema_version}, expected ${this.schemaVersion}. ` +
|
|
1634
|
+
`Refusing to apply a committed transaction through a stale cached adapter.`,
|
|
1635
|
+
)
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (version.already_applied === 1) {
|
|
1639
|
+
return
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const currentRowVersion = version.latest_row_version
|
|
1190
1643
|
const nextRowVersion = Math.max(currentRowVersion + 1, tx.rowVersion)
|
|
1191
|
-
const
|
|
1644
|
+
const replacesPersistedBaseline = tx.truncate === true
|
|
1645
|
+
const tracksPersistedKeySet =
|
|
1646
|
+
version.key_set_evidence_available === 1 || replacesPersistedBaseline
|
|
1647
|
+
const replayDelta: ReplayableTxDelta | null = replacesPersistedBaseline
|
|
1192
1648
|
? null
|
|
1193
1649
|
: {
|
|
1194
1650
|
txId: tx.txId,
|
|
@@ -1206,18 +1662,92 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1206
1662
|
collectionMetadataMutations: tx.collectionMetadataMutations ?? [],
|
|
1207
1663
|
}
|
|
1208
1664
|
|
|
1209
|
-
if (
|
|
1665
|
+
if (replacesPersistedBaseline) {
|
|
1666
|
+
await transactionDriver.run(
|
|
1667
|
+
`DELETE FROM collection_expected_keys
|
|
1668
|
+
WHERE collection_id = ?`,
|
|
1669
|
+
[collectionId],
|
|
1670
|
+
)
|
|
1210
1671
|
await transactionDriver.run(`DELETE FROM ${collectionTableSql}`)
|
|
1211
1672
|
await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`)
|
|
1212
1673
|
}
|
|
1213
1674
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1675
|
+
const replacementKeys = replacesPersistedBaseline
|
|
1676
|
+
? tx.mutations.map((mutation) =>
|
|
1677
|
+
encodePersistedStorageKey(mutation.key),
|
|
1678
|
+
)
|
|
1679
|
+
: []
|
|
1680
|
+
const batchReplacement =
|
|
1681
|
+
replacesPersistedBaseline &&
|
|
1682
|
+
tx.mutations.length > 0 &&
|
|
1683
|
+
tx.mutations.every((mutation) => mutation.type !== `delete`) &&
|
|
1684
|
+
new Set(replacementKeys).size === replacementKeys.length
|
|
1685
|
+
|
|
1686
|
+
if (batchReplacement) {
|
|
1687
|
+
const finalRowMetadata = new Map<string, unknown>()
|
|
1688
|
+
for (const mutation of tx.rowMetadataMutations ?? []) {
|
|
1689
|
+
finalRowMetadata.set(
|
|
1690
|
+
encodePersistedStorageKey(mutation.key),
|
|
1691
|
+
mutation.type === `delete` ? undefined : mutation.value,
|
|
1692
|
+
)
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
for (
|
|
1696
|
+
let start = 0;
|
|
1697
|
+
start < tx.mutations.length;
|
|
1698
|
+
start += this.replacementBatchSize
|
|
1699
|
+
) {
|
|
1700
|
+
const mutations = tx.mutations.slice(
|
|
1701
|
+
start,
|
|
1702
|
+
start + this.replacementBatchSize,
|
|
1703
|
+
)
|
|
1704
|
+
const keys = replacementKeys.slice(
|
|
1705
|
+
start,
|
|
1706
|
+
start + this.replacementBatchSize,
|
|
1707
|
+
)
|
|
1217
1708
|
await transactionDriver.run(
|
|
1218
|
-
`
|
|
1219
|
-
|
|
1220
|
-
[
|
|
1709
|
+
`INSERT INTO collection_expected_keys (collection_id, key)
|
|
1710
|
+
VALUES ${keys.map(() => `(?, ?)`).join(`, `)}`,
|
|
1711
|
+
keys.flatMap((key) => [collectionId, key]),
|
|
1712
|
+
)
|
|
1713
|
+
await transactionDriver.run(
|
|
1714
|
+
`INSERT INTO ${collectionTableSql} (key, value, metadata, row_version)
|
|
1715
|
+
VALUES ${keys.map(() => `(?, ?, ?, ?)`).join(`, `)}`,
|
|
1716
|
+
mutations.flatMap((mutation, index) => {
|
|
1717
|
+
const key = keys[index]!
|
|
1718
|
+
const metadata = finalRowMetadata.has(key)
|
|
1719
|
+
? finalRowMetadata.get(key)
|
|
1720
|
+
: mutation.type !== `delete` &&
|
|
1721
|
+
mutation.metadataChanged === true
|
|
1722
|
+
? mutation.metadata
|
|
1723
|
+
: undefined
|
|
1724
|
+
return [
|
|
1725
|
+
key,
|
|
1726
|
+
serializePersistedRowValue(mutation.value),
|
|
1727
|
+
metadata === undefined
|
|
1728
|
+
? null
|
|
1729
|
+
: serializePersistedRowValue(metadata),
|
|
1730
|
+
nextRowVersion,
|
|
1731
|
+
]
|
|
1732
|
+
}),
|
|
1733
|
+
)
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
for (const mutation of batchReplacement ? [] : tx.mutations) {
|
|
1738
|
+
const encodedKey = encodePersistedStorageKey(mutation.key)
|
|
1739
|
+
if (mutation.type === `delete`) {
|
|
1740
|
+
if (tracksPersistedKeySet) {
|
|
1741
|
+
await transactionDriver.run(
|
|
1742
|
+
`DELETE FROM collection_expected_keys
|
|
1743
|
+
WHERE collection_id = ? AND key = ?`,
|
|
1744
|
+
[collectionId, encodedKey],
|
|
1745
|
+
)
|
|
1746
|
+
}
|
|
1747
|
+
await transactionDriver.run(
|
|
1748
|
+
`DELETE FROM ${collectionTableSql}
|
|
1749
|
+
WHERE key = ?`,
|
|
1750
|
+
[encodedKey],
|
|
1221
1751
|
)
|
|
1222
1752
|
await transactionDriver.run(
|
|
1223
1753
|
`INSERT INTO ${tombstoneTableSql} (key, value, row_version, deleted_at)
|
|
@@ -1262,6 +1792,14 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1262
1792
|
? mutation.metadata
|
|
1263
1793
|
: existingMetadata
|
|
1264
1794
|
|
|
1795
|
+
if (tracksPersistedKeySet) {
|
|
1796
|
+
await transactionDriver.run(
|
|
1797
|
+
`INSERT INTO collection_expected_keys (collection_id, key)
|
|
1798
|
+
VALUES (?, ?)
|
|
1799
|
+
ON CONFLICT(collection_id, key) DO NOTHING`,
|
|
1800
|
+
[collectionId, encodedKey],
|
|
1801
|
+
)
|
|
1802
|
+
}
|
|
1265
1803
|
await transactionDriver.run(
|
|
1266
1804
|
`INSERT INTO ${collectionTableSql} (key, value, metadata, row_version)
|
|
1267
1805
|
VALUES (?, ?, ?, ?)
|
|
@@ -1285,7 +1823,9 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1285
1823
|
)
|
|
1286
1824
|
}
|
|
1287
1825
|
|
|
1288
|
-
for (const rowMetadataMutation of
|
|
1826
|
+
for (const rowMetadataMutation of batchReplacement
|
|
1827
|
+
? []
|
|
1828
|
+
: (tx.rowMetadataMutations ?? [])) {
|
|
1289
1829
|
const encodedKey = encodePersistedStorageKey(rowMetadataMutation.key)
|
|
1290
1830
|
if (rowMetadataMutation.type === `delete`) {
|
|
1291
1831
|
await transactionDriver.run(
|
|
@@ -1333,11 +1873,23 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1333
1873
|
}
|
|
1334
1874
|
|
|
1335
1875
|
await transactionDriver.run(
|
|
1336
|
-
`
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1876
|
+
`UPDATE collection_version
|
|
1877
|
+
SET latest_row_version = ?,
|
|
1878
|
+
key_set_evidence_available = CASE
|
|
1879
|
+
WHEN ? = 1 THEN 1
|
|
1880
|
+
ELSE key_set_evidence_available
|
|
1881
|
+
END,
|
|
1882
|
+
key_set_evidence_incompatible = CASE
|
|
1883
|
+
WHEN ? = 1 THEN 0
|
|
1884
|
+
ELSE key_set_evidence_incompatible
|
|
1885
|
+
END
|
|
1886
|
+
WHERE collection_id = ?`,
|
|
1887
|
+
[
|
|
1888
|
+
nextRowVersion,
|
|
1889
|
+
replacesPersistedBaseline ? 1 : 0,
|
|
1890
|
+
replacesPersistedBaseline ? 1 : 0,
|
|
1891
|
+
collectionId,
|
|
1892
|
+
],
|
|
1341
1893
|
)
|
|
1342
1894
|
|
|
1343
1895
|
await transactionDriver.run(
|
|
@@ -1371,7 +1923,7 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1371
1923
|
tx.txId,
|
|
1372
1924
|
nextRowVersion,
|
|
1373
1925
|
replayDelta ? stableStringify(replayDelta) : null,
|
|
1374
|
-
|
|
1926
|
+
replacesPersistedBaseline ? 1 : 0,
|
|
1375
1927
|
],
|
|
1376
1928
|
)
|
|
1377
1929
|
|
|
@@ -1379,46 +1931,90 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1379
1931
|
})
|
|
1380
1932
|
}
|
|
1381
1933
|
|
|
1382
|
-
|
|
1934
|
+
loadCollectionMetadata(
|
|
1383
1935
|
collectionId: string,
|
|
1384
1936
|
): Promise<Array<{ key: string; value: unknown }>> {
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
FROM collection_metadata
|
|
1388
|
-
WHERE collection_id = ?`,
|
|
1389
|
-
[collectionId],
|
|
1937
|
+
return this.runRegular(() =>
|
|
1938
|
+
this.loadCollectionMetadataUnscheduled(collectionId),
|
|
1390
1939
|
)
|
|
1940
|
+
}
|
|
1391
1941
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1942
|
+
private async loadCollectionMetadataUnscheduled(
|
|
1943
|
+
collectionId: string,
|
|
1944
|
+
): Promise<Array<{ key: string; value: unknown }>> {
|
|
1945
|
+
await this.ensureCollectionReady(collectionId)
|
|
1946
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1947
|
+
await this.assertCurrentSchemaVersion(
|
|
1948
|
+
collectionId,
|
|
1949
|
+
transactionDriver,
|
|
1950
|
+
`load collection metadata`,
|
|
1951
|
+
)
|
|
1952
|
+
const rows = await transactionDriver.query<{
|
|
1953
|
+
key: string
|
|
1954
|
+
value: string
|
|
1955
|
+
}>(
|
|
1956
|
+
`SELECT key, value
|
|
1957
|
+
FROM collection_metadata
|
|
1958
|
+
WHERE collection_id = ?`,
|
|
1959
|
+
[collectionId],
|
|
1960
|
+
)
|
|
1961
|
+
|
|
1962
|
+
return rows.map((row) => ({
|
|
1963
|
+
key: row.key,
|
|
1964
|
+
value: deserializePersistedRowValue(row.value),
|
|
1965
|
+
}))
|
|
1966
|
+
})
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
scanRows(
|
|
1970
|
+
collectionId: string,
|
|
1971
|
+
options?: PersistedRowScanOptions,
|
|
1972
|
+
): Promise<Array<PersistedScannedRow>> {
|
|
1973
|
+
return this.runRegular(() =>
|
|
1974
|
+
this.scanRowsUnscheduled(collectionId, options),
|
|
1975
|
+
)
|
|
1396
1976
|
}
|
|
1397
1977
|
|
|
1398
|
-
async
|
|
1978
|
+
private async scanRowsUnscheduled(
|
|
1399
1979
|
collectionId: string,
|
|
1400
1980
|
options?: PersistedRowScanOptions,
|
|
1401
1981
|
): Promise<Array<PersistedScannedRow>> {
|
|
1402
1982
|
const tableMapping = await this.ensureCollectionReady(collectionId)
|
|
1403
1983
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
1984
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1985
|
+
await this.assertCurrentSchemaVersion(
|
|
1986
|
+
collectionId,
|
|
1987
|
+
transactionDriver,
|
|
1988
|
+
`scan persisted rows`,
|
|
1989
|
+
)
|
|
1990
|
+
const storedRows = await transactionDriver.query<StoredSqliteRow>(
|
|
1991
|
+
options?.metadataOnly
|
|
1992
|
+
? `SELECT key, value, metadata, row_version
|
|
1993
|
+
FROM ${collectionTableSql}
|
|
1994
|
+
WHERE metadata IS NOT NULL`
|
|
1995
|
+
: `SELECT key, value, metadata, row_version
|
|
1996
|
+
FROM ${collectionTableSql}`,
|
|
1997
|
+
)
|
|
1404
1998
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
)
|
|
1999
|
+
return decodeStoredSqliteRows(storedRows).map((row) => ({
|
|
2000
|
+
key: row.key,
|
|
2001
|
+
value: row.value,
|
|
2002
|
+
metadata: row.metadata,
|
|
2003
|
+
}))
|
|
2004
|
+
})
|
|
2005
|
+
}
|
|
1413
2006
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
2007
|
+
ensureIndex(
|
|
2008
|
+
collectionId: string,
|
|
2009
|
+
signature: string,
|
|
2010
|
+
spec: PersistedIndexSpec,
|
|
2011
|
+
): Promise<void> {
|
|
2012
|
+
return this.runRegular(() =>
|
|
2013
|
+
this.ensureIndexUnscheduled(collectionId, signature, spec),
|
|
2014
|
+
)
|
|
1419
2015
|
}
|
|
1420
2016
|
|
|
1421
|
-
async
|
|
2017
|
+
private async ensureIndexUnscheduled(
|
|
1422
2018
|
collectionId: string,
|
|
1423
2019
|
signature: string,
|
|
1424
2020
|
spec: PersistedIndexSpec,
|
|
@@ -1431,11 +2027,44 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1431
2027
|
normalizeIndexSqlFragment(fragment),
|
|
1432
2028
|
)
|
|
1433
2029
|
const expressionSql = normalizedExpressionSql.join(`, `)
|
|
2030
|
+
const persistedExpressionSql = JSON.stringify(normalizedExpressionSql)
|
|
1434
2031
|
const whereSql = spec.whereSql
|
|
1435
2032
|
? normalizeIndexSqlFragment(spec.whereSql)
|
|
1436
2033
|
: undefined
|
|
2034
|
+
const persistedWhereSql = whereSql ?? null
|
|
1437
2035
|
|
|
1438
2036
|
await this.runInTransaction(async (transactionDriver) => {
|
|
2037
|
+
await this.assertCurrentSchemaVersion(
|
|
2038
|
+
collectionId,
|
|
2039
|
+
transactionDriver,
|
|
2040
|
+
`create a persisted index`,
|
|
2041
|
+
)
|
|
2042
|
+
|
|
2043
|
+
const existingRows = await transactionDriver.query<{
|
|
2044
|
+
index_name: string
|
|
2045
|
+
expression_sql: string
|
|
2046
|
+
where_sql: string | null
|
|
2047
|
+
}>(
|
|
2048
|
+
`SELECT index_name, expression_sql, where_sql
|
|
2049
|
+
FROM persisted_index_registry
|
|
2050
|
+
WHERE collection_id = ? AND signature = ?
|
|
2051
|
+
LIMIT 1`,
|
|
2052
|
+
[collectionId, signature],
|
|
2053
|
+
)
|
|
2054
|
+
const existing = existingRows[0]
|
|
2055
|
+
if (
|
|
2056
|
+
existing &&
|
|
2057
|
+
(existing.index_name !== indexName ||
|
|
2058
|
+
existing.expression_sql !== persistedExpressionSql ||
|
|
2059
|
+
existing.where_sql !== persistedWhereSql)
|
|
2060
|
+
) {
|
|
2061
|
+
// A compiler upgrade can change normalized SQL without changing the
|
|
2062
|
+
// logical index signature. Rebuild only that stale physical index so
|
|
2063
|
+
// the registry and SQLite planner describe the same expression.
|
|
2064
|
+
await transactionDriver.exec(
|
|
2065
|
+
`DROP INDEX IF EXISTS ${quoteIdentifier(existing.index_name)}`,
|
|
2066
|
+
)
|
|
2067
|
+
}
|
|
1439
2068
|
await transactionDriver.run(
|
|
1440
2069
|
`INSERT INTO persisted_index_registry (
|
|
1441
2070
|
collection_id,
|
|
@@ -1463,8 +2092,8 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1463
2092
|
collectionId,
|
|
1464
2093
|
signature,
|
|
1465
2094
|
indexName,
|
|
1466
|
-
|
|
1467
|
-
|
|
2095
|
+
persistedExpressionSql,
|
|
2096
|
+
persistedWhereSql,
|
|
1468
2097
|
],
|
|
1469
2098
|
)
|
|
1470
2099
|
|
|
@@ -1478,59 +2107,110 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1478
2107
|
})
|
|
1479
2108
|
}
|
|
1480
2109
|
|
|
1481
|
-
|
|
2110
|
+
markIndexRemoved(collectionId: string, signature: string): Promise<void> {
|
|
2111
|
+
return this.runRegular(() =>
|
|
2112
|
+
this.markIndexRemovedUnscheduled(collectionId, signature),
|
|
2113
|
+
)
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
private async markIndexRemovedUnscheduled(
|
|
1482
2117
|
collectionId: string,
|
|
1483
2118
|
signature: string,
|
|
1484
2119
|
): Promise<void> {
|
|
1485
2120
|
await this.ensureCollectionReady(collectionId)
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
WHERE collection_id = ? AND signature = ?`,
|
|
1501
|
-
[collectionId, signature],
|
|
1502
|
-
)
|
|
2121
|
+
await this.runInTransaction(async (transactionDriver) => {
|
|
2122
|
+
await this.assertCurrentSchemaVersion(
|
|
2123
|
+
collectionId,
|
|
2124
|
+
transactionDriver,
|
|
2125
|
+
`remove a persisted index`,
|
|
2126
|
+
)
|
|
2127
|
+
const rows = await transactionDriver.query<{ index_name: string }>(
|
|
2128
|
+
`SELECT index_name
|
|
2129
|
+
FROM persisted_index_registry
|
|
2130
|
+
WHERE collection_id = ? AND signature = ?
|
|
2131
|
+
LIMIT 1`,
|
|
2132
|
+
[collectionId, signature],
|
|
2133
|
+
)
|
|
2134
|
+
const indexName = rows[0]?.index_name
|
|
1503
2135
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
2136
|
+
await transactionDriver.run(
|
|
2137
|
+
`UPDATE persisted_index_registry
|
|
2138
|
+
SET removed = 1,
|
|
2139
|
+
updated_at = CAST(strftime('%s', 'now') AS INTEGER),
|
|
2140
|
+
last_used_at = CAST(strftime('%s', 'now') AS INTEGER)
|
|
2141
|
+
WHERE collection_id = ? AND signature = ?`,
|
|
2142
|
+
[collectionId, signature],
|
|
1507
2143
|
)
|
|
1508
|
-
|
|
2144
|
+
|
|
2145
|
+
if (indexName) {
|
|
2146
|
+
await transactionDriver.exec(
|
|
2147
|
+
`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`,
|
|
2148
|
+
)
|
|
2149
|
+
}
|
|
2150
|
+
})
|
|
1509
2151
|
}
|
|
1510
2152
|
|
|
1511
|
-
|
|
2153
|
+
getStreamPosition(collectionId: string): Promise<{
|
|
2154
|
+
latestTerm: number
|
|
2155
|
+
latestSeq: number
|
|
2156
|
+
latestRowVersion: number
|
|
2157
|
+
}> {
|
|
2158
|
+
// Election must not queue behind a hydrate awaiting its first writer route.
|
|
2159
|
+
// The stream-position snapshot still uses the driver's transaction admission.
|
|
2160
|
+
return this.getStreamPositionUnscheduled(collectionId)
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
private async getStreamPositionUnscheduled(collectionId: string): Promise<{
|
|
1512
2164
|
latestTerm: number
|
|
1513
2165
|
latestSeq: number
|
|
1514
2166
|
latestRowVersion: number
|
|
1515
2167
|
}> {
|
|
1516
2168
|
await this.ensureCollectionReady(collectionId)
|
|
2169
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
2170
|
+
await this.assertCurrentSchemaVersion(
|
|
2171
|
+
collectionId,
|
|
2172
|
+
transactionDriver,
|
|
2173
|
+
`read the stream position`,
|
|
2174
|
+
)
|
|
2175
|
+
const [position, latestRowVersion] = await Promise.all([
|
|
2176
|
+
this.readStreamPosition(collectionId, transactionDriver),
|
|
2177
|
+
this.readLatestRowVersion(collectionId, transactionDriver),
|
|
2178
|
+
])
|
|
1517
2179
|
|
|
1518
|
-
|
|
1519
|
-
|
|
2180
|
+
return {
|
|
2181
|
+
...position,
|
|
2182
|
+
latestRowVersion,
|
|
2183
|
+
}
|
|
2184
|
+
})
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
private async readLatestRowVersion(
|
|
2188
|
+
collectionId: string,
|
|
2189
|
+
driver: SQLiteDriver,
|
|
2190
|
+
): Promise<number> {
|
|
2191
|
+
const versionRows = await driver.query<{ latest_row_version: number }>(
|
|
2192
|
+
`SELECT latest_row_version
|
|
2193
|
+
FROM collection_version
|
|
2194
|
+
WHERE collection_id = ?
|
|
2195
|
+
LIMIT 1`,
|
|
2196
|
+
[collectionId],
|
|
2197
|
+
)
|
|
2198
|
+
return versionRows[0]?.latest_row_version ?? 0
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
private async readStreamPosition(
|
|
2202
|
+
collectionId: string,
|
|
2203
|
+
driver: SQLiteDriver,
|
|
2204
|
+
): Promise<{ latestTerm: number; latestSeq: number }> {
|
|
2205
|
+
const [termRows, seqRows] = await Promise.all([
|
|
2206
|
+
driver.query<{ latest_term: number }>(
|
|
1520
2207
|
`SELECT latest_term
|
|
1521
2208
|
FROM leader_term
|
|
1522
2209
|
WHERE collection_id = ?
|
|
1523
2210
|
LIMIT 1`,
|
|
1524
2211
|
[collectionId],
|
|
1525
2212
|
),
|
|
1526
|
-
|
|
1527
|
-
`SELECT latest_row_version
|
|
1528
|
-
FROM collection_version
|
|
1529
|
-
WHERE collection_id = ?
|
|
1530
|
-
LIMIT 1`,
|
|
1531
|
-
[collectionId],
|
|
1532
|
-
),
|
|
1533
|
-
this.driver.query<{ max_seq: number }>(
|
|
2213
|
+
driver.query<{ max_seq: number }>(
|
|
1534
2214
|
`SELECT MAX(seq) AS max_seq
|
|
1535
2215
|
FROM applied_tx
|
|
1536
2216
|
WHERE collection_id = ? AND term = (
|
|
@@ -1543,11 +2223,55 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1543
2223
|
return {
|
|
1544
2224
|
latestTerm: termRows[0]?.latest_term ?? 0,
|
|
1545
2225
|
latestSeq: seqRows[0]?.max_seq ?? 0,
|
|
1546
|
-
latestRowVersion: versionRows[0]?.latest_row_version ?? 0,
|
|
1547
2226
|
}
|
|
1548
2227
|
}
|
|
1549
2228
|
|
|
1550
|
-
async
|
|
2229
|
+
private async readKeySetEvidence(
|
|
2230
|
+
collectionId: string,
|
|
2231
|
+
driver: SQLiteDriver,
|
|
2232
|
+
): Promise<{
|
|
2233
|
+
latestRowVersion: number
|
|
2234
|
+
keySet: PersistedKeySetEvidence
|
|
2235
|
+
}> {
|
|
2236
|
+
const versionRows = await driver.query<{
|
|
2237
|
+
latest_row_version: number
|
|
2238
|
+
key_set_evidence_available: number
|
|
2239
|
+
key_set_incompatible: number
|
|
2240
|
+
}>(
|
|
2241
|
+
`SELECT
|
|
2242
|
+
latest_row_version,
|
|
2243
|
+
key_set_evidence_available,
|
|
2244
|
+
key_set_evidence_incompatible AS key_set_incompatible
|
|
2245
|
+
FROM collection_version
|
|
2246
|
+
WHERE collection_id = ?
|
|
2247
|
+
LIMIT 1`,
|
|
2248
|
+
[collectionId],
|
|
2249
|
+
)
|
|
2250
|
+
const version = versionRows[0]
|
|
2251
|
+
|
|
2252
|
+
return {
|
|
2253
|
+
latestRowVersion: version?.latest_row_version ?? 0,
|
|
2254
|
+
keySet: {
|
|
2255
|
+
status:
|
|
2256
|
+
version?.key_set_evidence_available !== 1
|
|
2257
|
+
? `unknown`
|
|
2258
|
+
: version.key_set_incompatible === 1
|
|
2259
|
+
? `incompatible`
|
|
2260
|
+
: `consistent`,
|
|
2261
|
+
},
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
pullSince(
|
|
2266
|
+
collectionId: string,
|
|
2267
|
+
fromRowVersion: number,
|
|
2268
|
+
): Promise<SQLitePullSinceResult<string | number>> {
|
|
2269
|
+
return this.runRegular(() =>
|
|
2270
|
+
this.pullSinceUnscheduled(collectionId, fromRowVersion),
|
|
2271
|
+
)
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
private async pullSinceUnscheduled(
|
|
1551
2275
|
collectionId: string,
|
|
1552
2276
|
fromRowVersion: number,
|
|
1553
2277
|
): Promise<SQLitePullSinceResult<string | number>> {
|
|
@@ -1555,136 +2279,144 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1555
2279
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
1556
2280
|
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName)
|
|
1557
2281
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
2282
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
2283
|
+
await this.assertCurrentSchemaVersion(
|
|
2284
|
+
collectionId,
|
|
2285
|
+
transactionDriver,
|
|
2286
|
+
`read persisted transaction deltas`,
|
|
2287
|
+
)
|
|
2288
|
+
const [
|
|
2289
|
+
changedRows,
|
|
2290
|
+
deletedRows,
|
|
2291
|
+
latestVersionRows,
|
|
2292
|
+
replayRows,
|
|
2293
|
+
replayAvailabilityRows,
|
|
2294
|
+
] = await Promise.all([
|
|
2295
|
+
transactionDriver.query<{ key: string }>(
|
|
2296
|
+
`SELECT key
|
|
1567
2297
|
FROM ${collectionTableSql}
|
|
1568
2298
|
WHERE row_version > ?`,
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
2299
|
+
[fromRowVersion],
|
|
2300
|
+
),
|
|
2301
|
+
transactionDriver.query<{ key: string }>(
|
|
2302
|
+
`SELECT key
|
|
1573
2303
|
FROM ${tombstoneTableSql}
|
|
1574
2304
|
WHERE row_version > ?`,
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
2305
|
+
[fromRowVersion],
|
|
2306
|
+
),
|
|
2307
|
+
transactionDriver.query<{ latest_row_version: number }>(
|
|
2308
|
+
`SELECT latest_row_version
|
|
1579
2309
|
FROM collection_version
|
|
1580
2310
|
WHERE collection_id = ?
|
|
1581
2311
|
LIMIT 1`,
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2312
|
+
[collectionId],
|
|
2313
|
+
),
|
|
2314
|
+
transactionDriver.query<{
|
|
2315
|
+
tx_id: string
|
|
2316
|
+
row_version: number
|
|
2317
|
+
replay_json: string | null
|
|
2318
|
+
replay_requires_full_reload: number
|
|
2319
|
+
}>(
|
|
2320
|
+
`SELECT tx_id, row_version, replay_json, replay_requires_full_reload
|
|
1591
2321
|
FROM applied_tx
|
|
1592
2322
|
WHERE collection_id = ? AND row_version > ?
|
|
1593
2323
|
ORDER BY term ASC, seq ASC`,
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
2324
|
+
[collectionId, fromRowVersion],
|
|
2325
|
+
),
|
|
2326
|
+
transactionDriver.query<{ min_row_version: number | null }>(
|
|
2327
|
+
`SELECT MIN(row_version) AS min_row_version
|
|
1598
2328
|
FROM applied_tx
|
|
1599
2329
|
WHERE collection_id = ?`,
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
2330
|
+
[collectionId],
|
|
2331
|
+
),
|
|
2332
|
+
])
|
|
1603
2333
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
2334
|
+
const latestRowVersion = latestVersionRows[0]?.latest_row_version ?? 0
|
|
2335
|
+
const replayFloor = replayAvailabilityRows[0]?.min_row_version
|
|
2336
|
+
if (
|
|
2337
|
+
latestRowVersion > fromRowVersion &&
|
|
2338
|
+
(replayFloor == null || replayFloor > fromRowVersion + 1)
|
|
2339
|
+
) {
|
|
2340
|
+
return {
|
|
2341
|
+
latestRowVersion,
|
|
2342
|
+
requiresFullReload: true,
|
|
2343
|
+
}
|
|
1613
2344
|
}
|
|
1614
|
-
}
|
|
1615
2345
|
|
|
1616
|
-
|
|
2346
|
+
const changedKeyCount = changedRows.length + deletedRows.length
|
|
1617
2347
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
2348
|
+
if (changedKeyCount > this.pullSinceReloadThreshold) {
|
|
2349
|
+
return {
|
|
2350
|
+
latestRowVersion,
|
|
2351
|
+
requiresFullReload: true,
|
|
2352
|
+
}
|
|
1622
2353
|
}
|
|
1623
|
-
}
|
|
1624
2354
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
2355
|
+
if (
|
|
2356
|
+
replayRows.some(
|
|
2357
|
+
(row) =>
|
|
2358
|
+
row.replay_requires_full_reload !== 0 || row.replay_json == null,
|
|
2359
|
+
)
|
|
2360
|
+
) {
|
|
2361
|
+
return {
|
|
2362
|
+
latestRowVersion,
|
|
2363
|
+
requiresFullReload: true,
|
|
2364
|
+
}
|
|
1634
2365
|
}
|
|
1635
|
-
}
|
|
1636
2366
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
2367
|
+
const decodeKey = (encodedKey: string): string | number => {
|
|
2368
|
+
try {
|
|
2369
|
+
return decodePersistedStorageKey(encodedKey)
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
throw new InvalidPersistedStorageKeyEncodingError(
|
|
2372
|
+
`${encodedKey}: ${(error as Error).message}`,
|
|
2373
|
+
)
|
|
2374
|
+
}
|
|
1644
2375
|
}
|
|
1645
|
-
}
|
|
1646
2376
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
)
|
|
1651
|
-
if (!parsed) {
|
|
1652
|
-
throw new InvalidPersistedCollectionConfigError(
|
|
1653
|
-
`missing replay payload for applied_tx row`,
|
|
2377
|
+
const deltas = replayRows.map((row) => {
|
|
2378
|
+
const parsed = deserializePersistedRowValue<ReplayableTxDelta | null>(
|
|
2379
|
+
row.replay_json ?? `null`,
|
|
1654
2380
|
)
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2381
|
+
if (!parsed) {
|
|
2382
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
2383
|
+
`missing replay payload for applied_tx row`,
|
|
2384
|
+
)
|
|
2385
|
+
}
|
|
2386
|
+
return parsed
|
|
2387
|
+
})
|
|
2388
|
+
|
|
2389
|
+
const replayChangeCount = deltas.reduce(
|
|
2390
|
+
(count, delta) =>
|
|
2391
|
+
count +
|
|
2392
|
+
delta.changedRows.length +
|
|
2393
|
+
delta.deletedKeys.length +
|
|
2394
|
+
delta.rowMetadataMutations.length +
|
|
2395
|
+
delta.collectionMetadataMutations.length,
|
|
2396
|
+
0,
|
|
2397
|
+
)
|
|
1658
2398
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
delta.collectionMetadataMutations.length,
|
|
1666
|
-
0,
|
|
1667
|
-
)
|
|
2399
|
+
if (replayChangeCount > this.pullSinceReloadThreshold) {
|
|
2400
|
+
return {
|
|
2401
|
+
latestRowVersion,
|
|
2402
|
+
requiresFullReload: true,
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
1668
2405
|
|
|
1669
|
-
if (replayChangeCount > this.pullSinceReloadThreshold) {
|
|
1670
2406
|
return {
|
|
1671
2407
|
latestRowVersion,
|
|
1672
|
-
requiresFullReload:
|
|
2408
|
+
requiresFullReload: false,
|
|
2409
|
+
changedKeys: changedRows.map((row) => decodeKey(row.key)),
|
|
2410
|
+
deletedKeys: deletedRows.map((row) => decodeKey(row.key)),
|
|
2411
|
+
deltas,
|
|
1673
2412
|
}
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
return {
|
|
1677
|
-
latestRowVersion,
|
|
1678
|
-
requiresFullReload: false,
|
|
1679
|
-
changedKeys: changedRows.map((row) => decodeKey(row.key)),
|
|
1680
|
-
deletedKeys: deletedRows.map((row) => decodeKey(row.key)),
|
|
1681
|
-
deltas,
|
|
1682
|
-
}
|
|
2413
|
+
})
|
|
1683
2414
|
}
|
|
1684
2415
|
|
|
1685
2416
|
private async loadSubsetInternal(
|
|
1686
2417
|
tableMapping: CollectionTableMapping,
|
|
1687
2418
|
options: LoadSubsetOptions,
|
|
2419
|
+
driver: SQLiteDriver = this.driver,
|
|
1688
2420
|
): Promise<Array<InMemoryRow<string | number, Record<string, unknown>>>> {
|
|
1689
2421
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
1690
2422
|
const whereCompiled = options.where
|
|
@@ -1705,10 +2437,7 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1705
2437
|
queryParams.push(...orderByCompiled.params)
|
|
1706
2438
|
}
|
|
1707
2439
|
|
|
1708
|
-
const storedRows = await
|
|
1709
|
-
sql,
|
|
1710
|
-
queryParams,
|
|
1711
|
-
)
|
|
2440
|
+
const storedRows = await driver.query<StoredSqliteRow>(sql, queryParams)
|
|
1712
2441
|
const parsedRows = decodeStoredSqliteRows(storedRows)
|
|
1713
2442
|
|
|
1714
2443
|
const filteredRows = this.applyInMemoryWhere(parsedRows, options.where)
|
|
@@ -1795,13 +2524,14 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1795
2524
|
private async touchRequiredIndexes(
|
|
1796
2525
|
collectionId: string,
|
|
1797
2526
|
requiredIndexSignatures: ReadonlyArray<string> | undefined,
|
|
2527
|
+
driver: SQLiteDriver = this.driver,
|
|
1798
2528
|
): Promise<void> {
|
|
1799
2529
|
if (!requiredIndexSignatures || requiredIndexSignatures.length === 0) {
|
|
1800
2530
|
return
|
|
1801
2531
|
}
|
|
1802
2532
|
|
|
1803
2533
|
for (const signature of requiredIndexSignatures) {
|
|
1804
|
-
await
|
|
2534
|
+
await driver.run(
|
|
1805
2535
|
`UPDATE persisted_index_registry
|
|
1806
2536
|
SET last_used_at = CAST(strftime('%s', 'now') AS INTEGER),
|
|
1807
2537
|
updated_at = CAST(strftime('%s', 'now') AS INTEGER)
|
|
@@ -1884,10 +2614,15 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1884
2614
|
}
|
|
1885
2615
|
}
|
|
1886
2616
|
|
|
1887
|
-
private async
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2617
|
+
private async loadCollectionRegistration(collectionId: string): Promise<
|
|
2618
|
+
| {
|
|
2619
|
+
table_name: string
|
|
2620
|
+
tombstone_table_name: string
|
|
2621
|
+
schema_version: number
|
|
2622
|
+
}
|
|
2623
|
+
| undefined
|
|
2624
|
+
> {
|
|
2625
|
+
const rows = await this.driver.query<{
|
|
1891
2626
|
table_name: string
|
|
1892
2627
|
tombstone_table_name: string
|
|
1893
2628
|
schema_version: number
|
|
@@ -1899,25 +2634,17 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1899
2634
|
[collectionId],
|
|
1900
2635
|
)
|
|
1901
2636
|
|
|
1902
|
-
|
|
1903
|
-
|
|
2637
|
+
return rows[0]
|
|
2638
|
+
}
|
|
1904
2639
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2640
|
+
private async ensureCollectionReadyInternal(
|
|
2641
|
+
collectionId: string,
|
|
2642
|
+
): Promise<CollectionTableMapping> {
|
|
2643
|
+
let registration = await this.loadCollectionRegistration(collectionId)
|
|
1908
2644
|
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
existingRows[0]!.schema_version,
|
|
1913
|
-
this.schemaVersion,
|
|
1914
|
-
tableName,
|
|
1915
|
-
tombstoneTableName,
|
|
1916
|
-
)
|
|
1917
|
-
}
|
|
1918
|
-
} else {
|
|
1919
|
-
tableName = createPersistedTableName(collectionId, `c`)
|
|
1920
|
-
tombstoneTableName = createPersistedTableName(collectionId, `t`)
|
|
2645
|
+
if (!registration) {
|
|
2646
|
+
const tableName = createPersistedTableName(collectionId, `c`)
|
|
2647
|
+
const tombstoneTableName = createPersistedTableName(collectionId, `t`)
|
|
1921
2648
|
await this.driver.run(
|
|
1922
2649
|
`INSERT INTO collection_registry (
|
|
1923
2650
|
collection_id,
|
|
@@ -1926,9 +2653,29 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1926
2653
|
schema_version,
|
|
1927
2654
|
updated_at
|
|
1928
2655
|
)
|
|
1929
|
-
VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
|
|
2656
|
+
VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
|
|
2657
|
+
ON CONFLICT DO NOTHING`,
|
|
1930
2658
|
[collectionId, tableName, tombstoneTableName, this.schemaVersion],
|
|
1931
2659
|
)
|
|
2660
|
+
|
|
2661
|
+
registration = await this.loadCollectionRegistration(collectionId)
|
|
2662
|
+
if (!registration) {
|
|
2663
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
2664
|
+
`Unable to register persistence tables for collection "${collectionId}"`,
|
|
2665
|
+
)
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
const tableName = registration.table_name
|
|
2670
|
+
const tombstoneTableName = registration.tombstone_table_name
|
|
2671
|
+
if (registration.schema_version !== this.schemaVersion) {
|
|
2672
|
+
await this.handleSchemaMismatch(
|
|
2673
|
+
collectionId,
|
|
2674
|
+
registration.schema_version,
|
|
2675
|
+
this.schemaVersion,
|
|
2676
|
+
tableName,
|
|
2677
|
+
tombstoneTableName,
|
|
2678
|
+
)
|
|
1932
2679
|
}
|
|
1933
2680
|
|
|
1934
2681
|
const collectionTableSql = quoteIdentifier(tableName)
|
|
@@ -1959,8 +2706,13 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1959
2706
|
ON ${tombstoneTableSql} (row_version)`,
|
|
1960
2707
|
)
|
|
1961
2708
|
await this.driver.run(
|
|
1962
|
-
`INSERT INTO collection_version (
|
|
1963
|
-
|
|
2709
|
+
`INSERT INTO collection_version (
|
|
2710
|
+
collection_id,
|
|
2711
|
+
latest_row_version,
|
|
2712
|
+
key_set_evidence_available,
|
|
2713
|
+
key_set_evidence_incompatible
|
|
2714
|
+
)
|
|
2715
|
+
VALUES (?, 0, 1, 0)
|
|
1964
2716
|
ON CONFLICT(collection_id) DO NOTHING`,
|
|
1965
2717
|
[collectionId],
|
|
1966
2718
|
)
|
|
@@ -1970,7 +2722,7 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1970
2722
|
ON CONFLICT(collection_id) DO NOTHING`,
|
|
1971
2723
|
[collectionId],
|
|
1972
2724
|
)
|
|
1973
|
-
|
|
2725
|
+
await this.ensureCollectionKeyEvidenceTriggers(collectionId, tableName)
|
|
1974
2726
|
const mapping = {
|
|
1975
2727
|
tableName,
|
|
1976
2728
|
tombstoneTableName,
|
|
@@ -1979,6 +2731,73 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1979
2731
|
return mapping
|
|
1980
2732
|
}
|
|
1981
2733
|
|
|
2734
|
+
private async ensureCollectionKeyEvidenceTriggers(
|
|
2735
|
+
collectionId: string,
|
|
2736
|
+
tableName: string,
|
|
2737
|
+
): Promise<void> {
|
|
2738
|
+
const collectionTableSql = quoteIdentifier(tableName)
|
|
2739
|
+
const collectionIdLiteral = toSqliteLiteral(collectionId)
|
|
2740
|
+
const insertTriggerSql = quoteIdentifier(`${tableName}_key_evidence_insert`)
|
|
2741
|
+
const deleteTriggerSql = quoteIdentifier(`${tableName}_key_evidence_delete`)
|
|
2742
|
+
const updateTriggerSql = quoteIdentifier(`${tableName}_key_evidence_update`)
|
|
2743
|
+
|
|
2744
|
+
await this.driver.exec(
|
|
2745
|
+
`CREATE TRIGGER IF NOT EXISTS ${insertTriggerSql}
|
|
2746
|
+
AFTER INSERT ON ${collectionTableSql}
|
|
2747
|
+
WHEN EXISTS (
|
|
2748
|
+
SELECT 1
|
|
2749
|
+
FROM collection_version
|
|
2750
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
2751
|
+
AND key_set_evidence_available = 1
|
|
2752
|
+
) AND NOT EXISTS (
|
|
2753
|
+
SELECT 1
|
|
2754
|
+
FROM collection_expected_keys
|
|
2755
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
2756
|
+
AND key = NEW.key
|
|
2757
|
+
)
|
|
2758
|
+
BEGIN
|
|
2759
|
+
UPDATE collection_version
|
|
2760
|
+
SET key_set_evidence_incompatible = 1
|
|
2761
|
+
WHERE collection_id = ${collectionIdLiteral};
|
|
2762
|
+
END`,
|
|
2763
|
+
)
|
|
2764
|
+
await this.driver.exec(
|
|
2765
|
+
`CREATE TRIGGER IF NOT EXISTS ${deleteTriggerSql}
|
|
2766
|
+
AFTER DELETE ON ${collectionTableSql}
|
|
2767
|
+
WHEN EXISTS (
|
|
2768
|
+
SELECT 1
|
|
2769
|
+
FROM collection_version
|
|
2770
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
2771
|
+
AND key_set_evidence_available = 1
|
|
2772
|
+
) AND EXISTS (
|
|
2773
|
+
SELECT 1
|
|
2774
|
+
FROM collection_expected_keys
|
|
2775
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
2776
|
+
AND key = OLD.key
|
|
2777
|
+
)
|
|
2778
|
+
BEGIN
|
|
2779
|
+
UPDATE collection_version
|
|
2780
|
+
SET key_set_evidence_incompatible = 1
|
|
2781
|
+
WHERE collection_id = ${collectionIdLiteral};
|
|
2782
|
+
END`,
|
|
2783
|
+
)
|
|
2784
|
+
await this.driver.exec(
|
|
2785
|
+
`CREATE TRIGGER IF NOT EXISTS ${updateTriggerSql}
|
|
2786
|
+
AFTER UPDATE OF key ON ${collectionTableSql}
|
|
2787
|
+
WHEN OLD.key <> NEW.key AND EXISTS (
|
|
2788
|
+
SELECT 1
|
|
2789
|
+
FROM collection_version
|
|
2790
|
+
WHERE collection_id = ${collectionIdLiteral}
|
|
2791
|
+
AND key_set_evidence_available = 1
|
|
2792
|
+
)
|
|
2793
|
+
BEGIN
|
|
2794
|
+
UPDATE collection_version
|
|
2795
|
+
SET key_set_evidence_incompatible = 1
|
|
2796
|
+
WHERE collection_id = ${collectionIdLiteral};
|
|
2797
|
+
END`,
|
|
2798
|
+
)
|
|
2799
|
+
}
|
|
2800
|
+
|
|
1982
2801
|
private async handleSchemaMismatch(
|
|
1983
2802
|
collectionId: string,
|
|
1984
2803
|
previousSchemaVersion: number,
|
|
@@ -1997,6 +2816,27 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1997
2816
|
const tombstoneTableSql = quoteIdentifier(tombstoneTableName)
|
|
1998
2817
|
|
|
1999
2818
|
await this.runInTransaction(async (transactionDriver) => {
|
|
2819
|
+
const currentSchemaRows = await transactionDriver.query<{
|
|
2820
|
+
schema_version: number
|
|
2821
|
+
}>(
|
|
2822
|
+
`SELECT schema_version
|
|
2823
|
+
FROM collection_registry
|
|
2824
|
+
WHERE collection_id = ?
|
|
2825
|
+
LIMIT 1`,
|
|
2826
|
+
[collectionId],
|
|
2827
|
+
)
|
|
2828
|
+
const currentSchemaVersion = currentSchemaRows[0]?.schema_version
|
|
2829
|
+
if (currentSchemaVersion === nextSchemaVersion) {
|
|
2830
|
+
return
|
|
2831
|
+
}
|
|
2832
|
+
if (currentSchemaVersion !== previousSchemaVersion) {
|
|
2833
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
2834
|
+
`Schema version changed concurrently for collection "${collectionId}": ` +
|
|
2835
|
+
`found ${currentSchemaVersion ?? `no registry entry`} after observing ${previousSchemaVersion}; ` +
|
|
2836
|
+
`refusing to reset it to ${nextSchemaVersion}.`,
|
|
2837
|
+
)
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2000
2840
|
const persistedIndexes = await transactionDriver.query<{
|
|
2001
2841
|
index_name: string
|
|
2002
2842
|
}>(
|
|
@@ -2011,6 +2851,11 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2011
2851
|
)
|
|
2012
2852
|
}
|
|
2013
2853
|
|
|
2854
|
+
await transactionDriver.run(
|
|
2855
|
+
`DELETE FROM collection_expected_keys
|
|
2856
|
+
WHERE collection_id = ?`,
|
|
2857
|
+
[collectionId],
|
|
2858
|
+
)
|
|
2014
2859
|
await transactionDriver.run(`DELETE FROM ${collectionTableSql}`)
|
|
2015
2860
|
await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`)
|
|
2016
2861
|
await transactionDriver.run(
|
|
@@ -2023,6 +2868,11 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2023
2868
|
WHERE collection_id = ?`,
|
|
2024
2869
|
[collectionId],
|
|
2025
2870
|
)
|
|
2871
|
+
await transactionDriver.run(
|
|
2872
|
+
`DELETE FROM collection_metadata
|
|
2873
|
+
WHERE collection_id = ?`,
|
|
2874
|
+
[collectionId],
|
|
2875
|
+
)
|
|
2026
2876
|
await transactionDriver.run(
|
|
2027
2877
|
`UPDATE collection_registry
|
|
2028
2878
|
SET schema_version = ?,
|
|
@@ -2031,10 +2881,17 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2031
2881
|
[nextSchemaVersion, collectionId],
|
|
2032
2882
|
)
|
|
2033
2883
|
await transactionDriver.run(
|
|
2034
|
-
`INSERT INTO collection_version (
|
|
2035
|
-
|
|
2884
|
+
`INSERT INTO collection_version (
|
|
2885
|
+
collection_id,
|
|
2886
|
+
latest_row_version,
|
|
2887
|
+
key_set_evidence_available,
|
|
2888
|
+
key_set_evidence_incompatible
|
|
2889
|
+
)
|
|
2890
|
+
VALUES (?, 0, 1, 0)
|
|
2036
2891
|
ON CONFLICT(collection_id) DO UPDATE SET
|
|
2037
|
-
latest_row_version = 0
|
|
2892
|
+
latest_row_version = 0,
|
|
2893
|
+
key_set_evidence_available = 1,
|
|
2894
|
+
key_set_evidence_incompatible = 0`,
|
|
2038
2895
|
[collectionId],
|
|
2039
2896
|
)
|
|
2040
2897
|
await transactionDriver.run(
|
|
@@ -2110,7 +2967,37 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2110
2967
|
await this.driver.exec(
|
|
2111
2968
|
`CREATE TABLE IF NOT EXISTS collection_version (
|
|
2112
2969
|
collection_id TEXT PRIMARY KEY,
|
|
2113
|
-
latest_row_version INTEGER NOT NULL
|
|
2970
|
+
latest_row_version INTEGER NOT NULL,
|
|
2971
|
+
key_set_evidence_available INTEGER NOT NULL DEFAULT 0,
|
|
2972
|
+
key_set_evidence_incompatible INTEGER NOT NULL DEFAULT 0
|
|
2973
|
+
)`,
|
|
2974
|
+
)
|
|
2975
|
+
const collectionVersionColumns = await this.driver.query<{ name: string }>(
|
|
2976
|
+
`PRAGMA table_info(collection_version)`,
|
|
2977
|
+
)
|
|
2978
|
+
const keyEvidenceColumns = [
|
|
2979
|
+
`key_set_evidence_available`,
|
|
2980
|
+
`key_set_evidence_incompatible`,
|
|
2981
|
+
] as const
|
|
2982
|
+
for (const columnName of keyEvidenceColumns) {
|
|
2983
|
+
if (collectionVersionColumns.some(({ name }) => name === columnName)) {
|
|
2984
|
+
continue
|
|
2985
|
+
}
|
|
2986
|
+
try {
|
|
2987
|
+
await this.driver.exec(
|
|
2988
|
+
`ALTER TABLE collection_version ADD COLUMN ${columnName} INTEGER NOT NULL DEFAULT 0`,
|
|
2989
|
+
)
|
|
2990
|
+
} catch (error) {
|
|
2991
|
+
if (!isDuplicateColumnAddError(error, columnName)) {
|
|
2992
|
+
throw error
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
await this.driver.exec(
|
|
2997
|
+
`CREATE TABLE IF NOT EXISTS collection_expected_keys (
|
|
2998
|
+
collection_id TEXT NOT NULL,
|
|
2999
|
+
key TEXT NOT NULL,
|
|
3000
|
+
PRIMARY KEY (collection_id, key)
|
|
2114
3001
|
)`,
|
|
2115
3002
|
)
|
|
2116
3003
|
await this.driver.exec(
|