@tanstack/db-sqlite-persistence-core 0.4.3 → 0.4.5
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 +14 -6
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/index.cjs +5 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/persisted.cjs +59 -62
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +7 -1
- package/dist/cjs/remote-subset-wire.cjs +100 -73
- package/dist/cjs/remote-subset-wire.cjs.map +1 -1
- package/dist/cjs/remote-subset-wire.d.cts +2 -0
- package/dist/cjs/sqlite-core-adapter.cjs +433 -184
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-value.cjs +51 -0
- package/dist/cjs/sqlite-value.cjs.map +1 -1
- package/dist/cjs/sqlite-value.d.cts +12 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +8 -3
- package/dist/esm/persisted.d.ts +7 -1
- package/dist/esm/persisted.js +63 -66
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-wire.d.ts +2 -0
- package/dist/esm/remote-subset-wire.js +101 -74
- package/dist/esm/remote-subset-wire.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.js +435 -186
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/dist/esm/sqlite-value.d.ts +12 -0
- package/dist/esm/sqlite-value.js +53 -2
- package/dist/esm/sqlite-value.js.map +1 -1
- package/package.json +4 -3
- package/src/errors.ts +1 -2
- package/src/persisted.ts +111 -91
- package/src/remote-subset-wire.ts +150 -85
- package/src/sqlite-core-adapter.ts +686 -235
- package/src/sqlite-value.ts +98 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IR,
|
|
3
|
+
compareTemporalValues,
|
|
3
4
|
compileSingleRowExpression,
|
|
4
5
|
toBooleanPredicate,
|
|
5
6
|
} from '@tanstack/db'
|
|
@@ -17,7 +18,11 @@ import {
|
|
|
17
18
|
PERSISTED_TYPE_TAG,
|
|
18
19
|
PERSISTED_VALUE_TAG,
|
|
19
20
|
assertSQLiteBigIntInRange,
|
|
21
|
+
reviveSQLiteTemporal,
|
|
20
22
|
serializeSQLiteBigInt,
|
|
23
|
+
serializeSQLiteTemporal,
|
|
24
|
+
sqliteTemporalIdentity,
|
|
25
|
+
sqliteTemporalKind,
|
|
21
26
|
} from './sqlite-value'
|
|
22
27
|
import type { LoadSubsetOptions } from '@tanstack/db'
|
|
23
28
|
import type {
|
|
@@ -36,7 +41,7 @@ type SqliteSupportedValue = null | number | string
|
|
|
36
41
|
|
|
37
42
|
// The default stays below SQLite's older 999-variable limit. Drivers with a
|
|
38
43
|
// lower binding cap use smaller chunks; each replacement row binds four values.
|
|
39
|
-
const REPLACEMENT_BATCH_SIZE =
|
|
44
|
+
const REPLACEMENT_BATCH_SIZE = 125
|
|
40
45
|
|
|
41
46
|
type CollectionTableMapping = {
|
|
42
47
|
tableName: string
|
|
@@ -47,6 +52,7 @@ type CompiledSqlFragment = {
|
|
|
47
52
|
supported: boolean
|
|
48
53
|
sql: string
|
|
49
54
|
params: Array<SqliteSupportedValue>
|
|
55
|
+
identitySql?: string
|
|
50
56
|
valueKind?: CompiledValueKind
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -60,9 +66,7 @@ type StoredSqliteRow = {
|
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
type SQLiteCoreAdapterSchemaMismatchPolicy =
|
|
63
|
-
| `sync-
|
|
64
|
-
| `sync-absent-error`
|
|
65
|
-
| `reset`
|
|
69
|
+
`sync-present-reset` | `sync-absent-error` | `reset`
|
|
66
70
|
|
|
67
71
|
export type SQLiteCoreAdapterOptions = {
|
|
68
72
|
driver: SQLiteDriver
|
|
@@ -222,6 +226,7 @@ function observeSharedLogicalSchedulingSupport(
|
|
|
222
226
|
}
|
|
223
227
|
|
|
224
228
|
return {
|
|
229
|
+
maxBoundParameters: driver.maxBoundParameters,
|
|
225
230
|
exec: (sql) => observe(driver.exec(sql)),
|
|
226
231
|
query: <T>(sql: string, params: ReadonlyArray<unknown> = []) =>
|
|
227
232
|
observe(driver.query<T>(sql, params)),
|
|
@@ -256,7 +261,6 @@ export const DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS = 1_000
|
|
|
256
261
|
*/
|
|
257
262
|
export const DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = 24 * 60 * 60
|
|
258
263
|
|
|
259
|
-
const SQLITE_MAX_IN_BATCH_SIZE = 900
|
|
260
264
|
const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
|
261
265
|
const FORBIDDEN_SQL_FRAGMENT_PATTERN = /(;|--|\/\*)/
|
|
262
266
|
type CompiledValueKind = `unknown` | `bigint` | `date` | `datetime`
|
|
@@ -266,6 +270,9 @@ type PersistedTaggedValueType =
|
|
|
266
270
|
| `nan`
|
|
267
271
|
| `infinity`
|
|
268
272
|
| `-infinity`
|
|
273
|
+
| `string`
|
|
274
|
+
| `Temporal.Instant`
|
|
275
|
+
| `Temporal.PlainDate`
|
|
269
276
|
type PersistedTaggedValue = {
|
|
270
277
|
[PERSISTED_TYPE_TAG]: PersistedTaggedValueType
|
|
271
278
|
[PERSISTED_VALUE_TAG]: string
|
|
@@ -277,6 +284,9 @@ const persistedTaggedValueTypes = new Set<PersistedTaggedValueType>([
|
|
|
277
284
|
`nan`,
|
|
278
285
|
`infinity`,
|
|
279
286
|
`-infinity`,
|
|
287
|
+
`string`,
|
|
288
|
+
`Temporal.Instant`,
|
|
289
|
+
`Temporal.PlainDate`,
|
|
280
290
|
])
|
|
281
291
|
|
|
282
292
|
const orderByObjectIds = new WeakMap<object, number>()
|
|
@@ -339,6 +349,9 @@ function encodePersistedJsonValue(value: unknown): unknown {
|
|
|
339
349
|
return serializeSQLiteBigInt(value) satisfies PersistedTaggedValue
|
|
340
350
|
}
|
|
341
351
|
|
|
352
|
+
const temporal = serializeSQLiteTemporal(value)
|
|
353
|
+
if (temporal) return temporal
|
|
354
|
+
|
|
342
355
|
if (value instanceof Date) {
|
|
343
356
|
return {
|
|
344
357
|
[PERSISTED_TYPE_TAG]: `date`,
|
|
@@ -381,6 +394,13 @@ function encodePersistedJsonValue(value: unknown): unknown {
|
|
|
381
394
|
encodedRecord[key] = encodedValue
|
|
382
395
|
}
|
|
383
396
|
}
|
|
397
|
+
// Escape the marker field itself, leaving ordinary nested JSON paths intact.
|
|
398
|
+
if (typeof recordValue[PERSISTED_TYPE_TAG] === `string`) {
|
|
399
|
+
encodedRecord[PERSISTED_TYPE_TAG] = {
|
|
400
|
+
[PERSISTED_TYPE_TAG]: `string`,
|
|
401
|
+
[PERSISTED_VALUE_TAG]: recordValue[PERSISTED_TYPE_TAG],
|
|
402
|
+
}
|
|
403
|
+
}
|
|
384
404
|
return encodedRecord
|
|
385
405
|
}
|
|
386
406
|
|
|
@@ -394,6 +414,14 @@ function decodePersistedJsonValue(value: unknown): unknown {
|
|
|
394
414
|
|
|
395
415
|
if (isPersistedTaggedValue(value)) {
|
|
396
416
|
switch (value[PERSISTED_TYPE_TAG]) {
|
|
417
|
+
case `string`:
|
|
418
|
+
return value[PERSISTED_VALUE_TAG]
|
|
419
|
+
case `Temporal.Instant`:
|
|
420
|
+
case `Temporal.PlainDate`:
|
|
421
|
+
return reviveSQLiteTemporal(
|
|
422
|
+
value[PERSISTED_TYPE_TAG],
|
|
423
|
+
value[PERSISTED_VALUE_TAG],
|
|
424
|
+
)
|
|
397
425
|
case `bigint`:
|
|
398
426
|
return BigInt(value[PERSISTED_VALUE_TAG])
|
|
399
427
|
case `date`: {
|
|
@@ -465,7 +493,8 @@ function toSqliteParameterValue(value: unknown): SqliteSupportedValue {
|
|
|
465
493
|
return value.toISOString()
|
|
466
494
|
}
|
|
467
495
|
|
|
468
|
-
|
|
496
|
+
const temporal = serializeSQLiteTemporal(value)
|
|
497
|
+
return temporal?.order ?? serializePersistedRowValue(value)
|
|
469
498
|
}
|
|
470
499
|
|
|
471
500
|
function toSqliteLiteral(value: SqliteSupportedValue): string {
|
|
@@ -477,6 +506,10 @@ function toSqliteLiteral(value: SqliteSupportedValue): string {
|
|
|
477
506
|
return Number.isFinite(value) ? String(value) : `NULL`
|
|
478
507
|
}
|
|
479
508
|
|
|
509
|
+
if (value.includes(`\u0000`)) {
|
|
510
|
+
// JSON escapes NUL before SQL parsing and matches stored-string extraction.
|
|
511
|
+
return `json_extract(${toSqliteLiteral(JSON.stringify(value))}, '$')`
|
|
512
|
+
}
|
|
480
513
|
return `'${value.replace(/'/g, `''`)}'`
|
|
481
514
|
}
|
|
482
515
|
|
|
@@ -569,6 +602,10 @@ function compareOrderByValues(
|
|
|
569
602
|
return left.getTime() - right.getTime()
|
|
570
603
|
}
|
|
571
604
|
|
|
605
|
+
if (sqliteTemporalKind(left) && sqliteTemporalKind(right)) {
|
|
606
|
+
return compareTemporalValues(left, right)
|
|
607
|
+
}
|
|
608
|
+
|
|
572
609
|
const leftIsObject = typeof left === `object`
|
|
573
610
|
const rightIsObject = typeof right === `object`
|
|
574
611
|
if (leftIsObject || rightIsObject) {
|
|
@@ -610,6 +647,39 @@ function createJsonPath(path: Array<string>): string | null {
|
|
|
610
647
|
return jsonPath
|
|
611
648
|
}
|
|
612
649
|
|
|
650
|
+
const MAX_INDEXED_NUMERIC_PATH_SEGMENTS = 3
|
|
651
|
+
|
|
652
|
+
function hasDeepNumericPath(expression: IR.BasicExpression): boolean {
|
|
653
|
+
if (expression.type === `ref`) {
|
|
654
|
+
return (
|
|
655
|
+
IR.getPropRefPropertyPath(expression).filter((segment) =>
|
|
656
|
+
/^[0-9]+$/.test(String(segment)),
|
|
657
|
+
).length > MAX_INDEXED_NUMERIC_PATH_SEGMENTS
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
return expression.type === `func` && expression.args.some(hasDeepNumericPath)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function createJsonPathVariants(path: Array<string>): Array<string> | null {
|
|
664
|
+
const canonical = createJsonPath(path)
|
|
665
|
+
if (!canonical) return null
|
|
666
|
+
|
|
667
|
+
let paths = [`$`]
|
|
668
|
+
for (const segment of path) {
|
|
669
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)) {
|
|
670
|
+
paths = paths.map((prefix) => `${prefix}.${segment}`)
|
|
671
|
+
} else if (/^[0-9]+$/.test(segment)) {
|
|
672
|
+
if (paths.length >= 2 ** MAX_INDEXED_NUMERIC_PATH_SEGMENTS)
|
|
673
|
+
return [canonical]
|
|
674
|
+
paths = paths.flatMap((prefix) => [
|
|
675
|
+
`${prefix}[${segment}]`,
|
|
676
|
+
`${prefix}."${segment}"`,
|
|
677
|
+
])
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return paths
|
|
681
|
+
}
|
|
682
|
+
|
|
613
683
|
function getLiteralValueKind(value: unknown): CompiledValueKind {
|
|
614
684
|
if (typeof value === `bigint`) {
|
|
615
685
|
return `bigint`
|
|
@@ -693,16 +763,23 @@ function compileRefExpressionSql(jsonPath: string): CompiledSqlFragment {
|
|
|
693
763
|
const taggedValuePathSql = toSqliteLiteral(taggedValuePath)
|
|
694
764
|
const jsonPathSql = toSqliteLiteral(jsonPath)
|
|
695
765
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
766
|
+
const typeSql = `json_extract(value, ${typePathSql})`
|
|
767
|
+
const orderPathSql = toSqliteLiteral(`${jsonPath}.order`)
|
|
768
|
+
const sql = `(CASE ${typeSql}
|
|
699
769
|
WHEN 'bigint' THEN CAST(json_extract(value, ${taggedValuePathSql}) AS NUMERIC)
|
|
700
770
|
WHEN 'date' THEN json_extract(value, ${taggedValuePathSql})
|
|
771
|
+
WHEN 'string' THEN json_extract(value, ${taggedValuePathSql})
|
|
772
|
+
WHEN 'Temporal.Instant' THEN json_extract(value, ${orderPathSql})
|
|
773
|
+
WHEN 'Temporal.PlainDate' THEN json_extract(value, ${orderPathSql})
|
|
701
774
|
WHEN 'nan' THEN NULL
|
|
702
775
|
WHEN 'infinity' THEN NULL
|
|
703
776
|
WHEN '-infinity' THEN NULL
|
|
704
777
|
ELSE json_extract(value, ${jsonPathSql})
|
|
705
|
-
END)
|
|
778
|
+
END)`
|
|
779
|
+
return {
|
|
780
|
+
supported: true,
|
|
781
|
+
sql,
|
|
782
|
+
identitySql: `(CASE WHEN ${typeSql} IN ('Temporal.Instant', 'Temporal.PlainDate') THEN ${typeSql} || ':' || json_extract(value, ${taggedValuePathSql}) ELSE ${sql} END)`,
|
|
706
783
|
params: [],
|
|
707
784
|
valueKind: `unknown`,
|
|
708
785
|
}
|
|
@@ -754,6 +831,13 @@ function stableStringify(value: unknown): string {
|
|
|
754
831
|
return serializePersistedRowValue(value)
|
|
755
832
|
}
|
|
756
833
|
|
|
834
|
+
/** Balance disjunctions so a large membership index stays below SQL depth limits. */
|
|
835
|
+
function joinSqlDisjunction(parts: ReadonlyArray<string>): string {
|
|
836
|
+
if (parts.length === 1) return parts[0]!
|
|
837
|
+
const middle = Math.floor(parts.length / 2)
|
|
838
|
+
return `(${joinSqlDisjunction(parts.slice(0, middle))} OR ${joinSqlDisjunction(parts.slice(middle))})`
|
|
839
|
+
}
|
|
840
|
+
|
|
757
841
|
function argumentCompilationContext(
|
|
758
842
|
parentName: string,
|
|
759
843
|
argumentIndex: number,
|
|
@@ -788,31 +872,78 @@ function argumentCompilationContext(
|
|
|
788
872
|
}
|
|
789
873
|
}
|
|
790
874
|
|
|
875
|
+
function hasUnpairedSurrogate(value: string): boolean {
|
|
876
|
+
// With /u, paired surrogates form one code point; lone surrogates still match.
|
|
877
|
+
return /[\uD800-\uDFFF]/u.test(value)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function isSafeEqualityLiteral(value: unknown): boolean {
|
|
881
|
+
return (
|
|
882
|
+
typeof value === `string` ||
|
|
883
|
+
typeof value === `boolean` ||
|
|
884
|
+
typeof value === `bigint` ||
|
|
885
|
+
sqliteTemporalKind(value) !== undefined
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function isSafeCoalesceOperand(
|
|
890
|
+
expression: IR.BasicExpression | undefined,
|
|
891
|
+
): boolean {
|
|
892
|
+
return (
|
|
893
|
+
expression?.type === `func` &&
|
|
894
|
+
expression.name === `coalesce` &&
|
|
895
|
+
expression.args.length === 2 &&
|
|
896
|
+
expression.args[0]?.type === `ref` &&
|
|
897
|
+
expression.args[1]?.type === `val` &&
|
|
898
|
+
isSafeEqualityLiteral(expression.args[1].value) &&
|
|
899
|
+
!(
|
|
900
|
+
typeof expression.args[1].value === `string` &&
|
|
901
|
+
hasUnpairedSurrogate(expression.args[1].value)
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
}
|
|
905
|
+
|
|
791
906
|
function compileSqlExpression(
|
|
792
907
|
expression: IR.BasicExpression,
|
|
793
908
|
context: SqlExpressionCompilationContext = `predicate`,
|
|
794
909
|
): CompiledSqlFragment {
|
|
795
910
|
if (expression.type === `val`) {
|
|
796
|
-
|
|
911
|
+
if (
|
|
912
|
+
context === `predicate` &&
|
|
913
|
+
typeof expression.value === `string` &&
|
|
914
|
+
hasUnpairedSurrogate(expression.value)
|
|
915
|
+
) {
|
|
916
|
+
// JSON extraction changes lone surrogates in stored strings. Apply the
|
|
917
|
+
// same extraction to the binding so an indexed equality keeps its row.
|
|
918
|
+
return {
|
|
919
|
+
supported: true,
|
|
920
|
+
sql: `json_extract(?, '$')`,
|
|
921
|
+
params: [JSON.stringify(expression.value)],
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const temporal = serializeSQLiteTemporal(expression.value)
|
|
925
|
+
const value = temporal?.order ?? toSqliteParameterValue(expression.value)
|
|
797
926
|
return {
|
|
927
|
+
identitySql: temporal
|
|
928
|
+
? toSqliteLiteral(sqliteTemporalIdentity(temporal))
|
|
929
|
+
: undefined,
|
|
798
930
|
supported: true,
|
|
799
931
|
sql:
|
|
800
932
|
context === `index-expression`
|
|
801
|
-
? toSqliteExpressionLiteral(
|
|
933
|
+
? toSqliteExpressionLiteral(
|
|
934
|
+
typeof expression.value === `bigint` ? expression.value : value,
|
|
935
|
+
)
|
|
802
936
|
: `?`,
|
|
803
|
-
params:
|
|
804
|
-
|
|
805
|
-
? [toSqliteParameterValue(expression.value)]
|
|
806
|
-
: [],
|
|
807
|
-
valueKind,
|
|
937
|
+
params: context === `predicate` ? [value] : [],
|
|
938
|
+
valueKind: getLiteralValueKind(expression.value),
|
|
808
939
|
}
|
|
809
940
|
}
|
|
810
941
|
|
|
811
942
|
if (expression.type === `ref`) {
|
|
812
|
-
const
|
|
943
|
+
const jsonPaths = createJsonPathVariants(
|
|
813
944
|
IR.getPropRefPropertyPath(expression).map(String),
|
|
814
945
|
)
|
|
815
|
-
if (!
|
|
946
|
+
if (!jsonPaths) {
|
|
816
947
|
return {
|
|
817
948
|
supported: false,
|
|
818
949
|
sql: ``,
|
|
@@ -820,10 +951,21 @@ function compileSqlExpression(
|
|
|
820
951
|
}
|
|
821
952
|
}
|
|
822
953
|
|
|
823
|
-
return compileRefExpressionSql(
|
|
954
|
+
if (jsonPaths.length === 1) return compileRefExpressionSql(jsonPaths[0]!)
|
|
955
|
+
const variants = jsonPaths.map(compileRefExpressionSql)
|
|
956
|
+
return {
|
|
957
|
+
supported: true,
|
|
958
|
+
identitySql: `COALESCE(${variants.map((variant) => variant.identitySql).join(`, `)})`,
|
|
959
|
+
sql: `COALESCE(${variants.map((variant) => variant.sql).join(`, `)})`,
|
|
960
|
+
params: [],
|
|
961
|
+
valueKind: `unknown`,
|
|
962
|
+
}
|
|
824
963
|
}
|
|
825
964
|
|
|
826
|
-
|
|
965
|
+
// IN owns its list encoding; compiling that list as a scalar discards work.
|
|
966
|
+
const args =
|
|
967
|
+
expression.name === `in` ? expression.args.slice(0, 1) : expression.args
|
|
968
|
+
const compiledArgs = args.map((arg, index) =>
|
|
827
969
|
compileSqlExpression(
|
|
828
970
|
arg,
|
|
829
971
|
argumentCompilationContext(expression.name, index, arg, context),
|
|
@@ -873,38 +1015,49 @@ function compileSqlExpression(
|
|
|
873
1015
|
lte: `<=`,
|
|
874
1016
|
}
|
|
875
1017
|
|
|
1018
|
+
const comparison = compileComparisonSql(
|
|
1019
|
+
operatorByName[expression.name],
|
|
1020
|
+
expression.args[0]!,
|
|
1021
|
+
expression.args[1]!,
|
|
1022
|
+
argSql[0],
|
|
1023
|
+
argSql[1],
|
|
1024
|
+
valueKind,
|
|
1025
|
+
getCompiledValueKind(compiledArgs[0]),
|
|
1026
|
+
getCompiledValueKind(compiledArgs[1]),
|
|
1027
|
+
)
|
|
1028
|
+
// Native order ties need canonical identity, including in persisted
|
|
1029
|
+
// expressions. Ordinary string predicates keep one binding; residual
|
|
1030
|
+
// filtering removes native order-key collisions from their candidate set.
|
|
1031
|
+
const needsIdentity =
|
|
1032
|
+
expression.name === `eq` &&
|
|
1033
|
+
expression.args.every(
|
|
1034
|
+
(arg) =>
|
|
1035
|
+
arg.type !== `val` ||
|
|
1036
|
+
sqliteTemporalKind(arg.value) ||
|
|
1037
|
+
(context === `index-expression` && typeof arg.value === `string`),
|
|
1038
|
+
)
|
|
876
1039
|
return {
|
|
877
1040
|
supported: true,
|
|
878
|
-
sql:
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
expression.args[1]!,
|
|
882
|
-
argSql[0],
|
|
883
|
-
argSql[1],
|
|
884
|
-
valueKind,
|
|
885
|
-
getCompiledValueKind(compiledArgs[0]),
|
|
886
|
-
getCompiledValueKind(compiledArgs[1]),
|
|
887
|
-
),
|
|
888
|
-
params,
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
case `and`: {
|
|
892
|
-
if (argSql.length < 2) {
|
|
893
|
-
return { supported: false, sql: ``, params: [] }
|
|
894
|
-
}
|
|
895
|
-
return {
|
|
896
|
-
supported: true,
|
|
897
|
-
sql: argSql.map((sql) => `(${sql})`).join(` AND `),
|
|
1041
|
+
sql: needsIdentity
|
|
1042
|
+
? `(${comparison} AND (${compiledArgs[0].identitySql ?? argSql[0]} = ${compiledArgs[1].identitySql ?? argSql[1]}))`
|
|
1043
|
+
: comparison,
|
|
898
1044
|
params,
|
|
899
1045
|
}
|
|
900
1046
|
}
|
|
1047
|
+
case `and`:
|
|
901
1048
|
case `or`: {
|
|
902
|
-
if (argSql.length
|
|
903
|
-
return {
|
|
1049
|
+
if (argSql.length === 0) {
|
|
1050
|
+
return {
|
|
1051
|
+
supported: true,
|
|
1052
|
+
sql: expression.name === `and` ? `(1 = 1)` : `(0 = 1)`,
|
|
1053
|
+
params: [],
|
|
1054
|
+
}
|
|
904
1055
|
}
|
|
905
1056
|
return {
|
|
906
1057
|
supported: true,
|
|
907
|
-
sql: argSql
|
|
1058
|
+
sql: argSql
|
|
1059
|
+
.map((sql) => `(${sql})`)
|
|
1060
|
+
.join(expression.name === `and` ? ` AND ` : ` OR `),
|
|
908
1061
|
params,
|
|
909
1062
|
}
|
|
910
1063
|
}
|
|
@@ -938,54 +1091,54 @@ function compileSqlExpression(
|
|
|
938
1091
|
return { supported: false, sql: ``, params: [] }
|
|
939
1092
|
}
|
|
940
1093
|
|
|
941
|
-
|
|
1094
|
+
// Order and identity are two projections of one validated native encoding.
|
|
1095
|
+
const values = listValue.map((value) => {
|
|
1096
|
+
const temporal = serializeSQLiteTemporal(value)
|
|
942
1097
|
return {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1098
|
+
value:
|
|
1099
|
+
typeof value === `bigint`
|
|
1100
|
+
? assertSQLiteBigIntInRange(value)
|
|
1101
|
+
: (temporal?.order ?? toSqliteParameterValue(value)),
|
|
1102
|
+
identity: temporal ? sqliteTemporalIdentity(temporal) : undefined,
|
|
948
1103
|
}
|
|
1104
|
+
})
|
|
1105
|
+
let identity = ``
|
|
1106
|
+
if (
|
|
1107
|
+
listValue.some(
|
|
1108
|
+
(value) =>
|
|
1109
|
+
sqliteTemporalKind(value) ||
|
|
1110
|
+
(context === `index-expression` && typeof value === `string`),
|
|
1111
|
+
)
|
|
1112
|
+
) {
|
|
1113
|
+
const leftIdentity = compiledArgs[0]?.identitySql ?? leftSql
|
|
1114
|
+
// Index expressions cannot contain subqueries. Both forms preserve
|
|
1115
|
+
// correlated membership and SQL's three-valued Boolean semantics.
|
|
1116
|
+
identity =
|
|
1117
|
+
context === `index-expression`
|
|
1118
|
+
? ` AND (${joinSqlDisjunction(values.map(({ value, identity: nativeIdentity }) => `(${leftSql} = ${toSqliteExpressionLiteral(value)} AND ${leftIdentity} = ${toSqliteExpressionLiteral(nativeIdentity ?? value)})`))})`
|
|
1119
|
+
: ` AND ((${leftSql}, ${leftIdentity}) IN (VALUES ${[...new Set(values.map(({ value, identity: nativeIdentity }) => `(${toSqliteExpressionLiteral(value)}, ${toSqliteExpressionLiteral(nativeIdentity ?? value)})`))].join(`, `)}))`
|
|
949
1120
|
}
|
|
950
|
-
|
|
951
|
-
if (listValue.length > SQLITE_MAX_IN_BATCH_SIZE) {
|
|
952
|
-
const chunkClauses: Array<string> = []
|
|
953
|
-
const batchedParams: Array<SqliteSupportedValue> = []
|
|
954
|
-
|
|
955
|
-
for (
|
|
956
|
-
let startIndex = 0;
|
|
957
|
-
startIndex < listValue.length;
|
|
958
|
-
startIndex += SQLITE_MAX_IN_BATCH_SIZE
|
|
959
|
-
) {
|
|
960
|
-
const chunkValues = listValue.slice(
|
|
961
|
-
startIndex,
|
|
962
|
-
startIndex + SQLITE_MAX_IN_BATCH_SIZE,
|
|
963
|
-
)
|
|
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)
|
|
971
|
-
}
|
|
972
|
-
|
|
1121
|
+
if (context === `index-expression`) {
|
|
973
1122
|
return {
|
|
974
1123
|
supported: true,
|
|
975
|
-
sql: `(${
|
|
976
|
-
|
|
1124
|
+
sql: `(${leftSql} IN (${values
|
|
1125
|
+
.map(({ value }) => toSqliteExpressionLiteral(value))
|
|
1126
|
+
.join(`, `)})${identity})`,
|
|
1127
|
+
params: leftParams,
|
|
977
1128
|
}
|
|
978
1129
|
}
|
|
979
1130
|
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1131
|
+
const jsonList = `[${values
|
|
1132
|
+
.map(({ value }) =>
|
|
1133
|
+
typeof value === `bigint`
|
|
1134
|
+
? assertSQLiteBigIntInRange(value).toString()
|
|
1135
|
+
: JSON.stringify(value),
|
|
1136
|
+
)
|
|
1137
|
+
.join(`,`)}]`
|
|
985
1138
|
return {
|
|
986
1139
|
supported: true,
|
|
987
|
-
sql: `(${leftSql} IN (${
|
|
988
|
-
params: [...leftParams,
|
|
1140
|
+
sql: `(${leftSql} IN (SELECT value FROM json_each(?))${identity})`,
|
|
1141
|
+
params: [...leftParams, jsonList],
|
|
989
1142
|
}
|
|
990
1143
|
}
|
|
991
1144
|
case `like`:
|
|
@@ -1012,7 +1165,12 @@ function compileSqlExpression(
|
|
|
1012
1165
|
case `concat`:
|
|
1013
1166
|
return { supported: true, sql: `(${argSql.join(` || `)})`, params }
|
|
1014
1167
|
case `coalesce`:
|
|
1015
|
-
return {
|
|
1168
|
+
return {
|
|
1169
|
+
supported: true,
|
|
1170
|
+
sql: `COALESCE(${argSql.join(`, `)})`,
|
|
1171
|
+
identitySql: `COALESCE(${compiledArgs.map((arg) => arg.identitySql ?? arg.sql).join(`, `)})`,
|
|
1172
|
+
params,
|
|
1173
|
+
}
|
|
1016
1174
|
case `add`:
|
|
1017
1175
|
return { supported: true, sql: `(${argSql[0]} + ${argSql[1]})`, params }
|
|
1018
1176
|
case `subtract`:
|
|
@@ -1050,6 +1208,235 @@ function compileSqlExpression(
|
|
|
1050
1208
|
}
|
|
1051
1209
|
}
|
|
1052
1210
|
|
|
1211
|
+
/** A raw ref/native-literal leaf has exact native truth before prefilter broadening. */
|
|
1212
|
+
function nativeComparisonRef(
|
|
1213
|
+
expression: IR.BasicExpression,
|
|
1214
|
+
): IR.PropRef | undefined {
|
|
1215
|
+
if (expression.type !== `func` || expression.args.length !== 2)
|
|
1216
|
+
return undefined
|
|
1217
|
+
const [left, right] = expression.args
|
|
1218
|
+
if (expression.name === `in`) {
|
|
1219
|
+
return left?.type === `ref` &&
|
|
1220
|
+
right?.type === `val` &&
|
|
1221
|
+
Array.isArray(right.value) &&
|
|
1222
|
+
right.value.length > 0 &&
|
|
1223
|
+
right.value.every((value) => sqliteTemporalKind(value))
|
|
1224
|
+
? left
|
|
1225
|
+
: undefined
|
|
1226
|
+
}
|
|
1227
|
+
if (![`eq`, `gt`, `gte`, `lt`, `lte`].includes(expression.name))
|
|
1228
|
+
return undefined
|
|
1229
|
+
if (
|
|
1230
|
+
left?.type === `ref` &&
|
|
1231
|
+
right?.type === `val` &&
|
|
1232
|
+
sqliteTemporalKind(right.value)
|
|
1233
|
+
)
|
|
1234
|
+
return left
|
|
1235
|
+
if (
|
|
1236
|
+
right?.type === `ref` &&
|
|
1237
|
+
left?.type === `val` &&
|
|
1238
|
+
sqliteTemporalKind(left.value)
|
|
1239
|
+
)
|
|
1240
|
+
return right
|
|
1241
|
+
return undefined
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function compileNativeRefGuard(ref: IR.PropRef): string {
|
|
1245
|
+
const paths = createJsonPathVariants(
|
|
1246
|
+
IR.getPropRefPropertyPath(ref).map(String),
|
|
1247
|
+
)!
|
|
1248
|
+
const kinds = paths.map(
|
|
1249
|
+
(path) =>
|
|
1250
|
+
`json_extract(value, ${toSqliteLiteral(`${path}.${PERSISTED_TYPE_TAG}`)})`,
|
|
1251
|
+
)
|
|
1252
|
+
const kind = kinds.length === 1 ? kinds[0] : `COALESCE(${kinds.join(`, `)})`
|
|
1253
|
+
return `${kind} IN ('Temporal.Instant', 'Temporal.PlainDate')`
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function compileSafeSqlPrefilter(
|
|
1257
|
+
expression: IR.BasicExpression,
|
|
1258
|
+
compiled?: CompiledSqlFragment,
|
|
1259
|
+
): CompiledSqlFragment | undefined {
|
|
1260
|
+
// Every public match must pass this SQL candidate before JavaScript applies
|
|
1261
|
+
// the authoritative row predicate. Extra candidates are allowed.
|
|
1262
|
+
const direct = () => {
|
|
1263
|
+
const candidate = compiled ?? compileSqlExpression(expression)
|
|
1264
|
+
return candidate.supported ? candidate : undefined
|
|
1265
|
+
}
|
|
1266
|
+
if (expression.type === `val`) {
|
|
1267
|
+
return typeof expression.value === `boolean` || expression.value == null
|
|
1268
|
+
? direct()
|
|
1269
|
+
: undefined
|
|
1270
|
+
}
|
|
1271
|
+
if (expression.type === `ref`) return undefined
|
|
1272
|
+
|
|
1273
|
+
if (expression.name === `and` || expression.name === `or`) {
|
|
1274
|
+
if (expression.args.length === 0) return direct()
|
|
1275
|
+
const candidates = expression.args.map((argument) =>
|
|
1276
|
+
compileSafeSqlPrefilter(argument),
|
|
1277
|
+
)
|
|
1278
|
+
if (expression.name === `or` && candidates.some((candidate) => !candidate))
|
|
1279
|
+
return undefined
|
|
1280
|
+
const selected = candidates.filter(
|
|
1281
|
+
(candidate): candidate is CompiledSqlFragment => candidate !== undefined,
|
|
1282
|
+
)
|
|
1283
|
+
if (selected.length === 0) return undefined
|
|
1284
|
+
return {
|
|
1285
|
+
supported: true,
|
|
1286
|
+
sql: selected
|
|
1287
|
+
.map(({ sql }) => `(${sql})`)
|
|
1288
|
+
.join(expression.name === `and` ? ` AND ` : ` OR `),
|
|
1289
|
+
params: selected.flatMap(({ params }) => params),
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// More than three digit segments need too many alternative JSON paths.
|
|
1294
|
+
// Keep their prefilter unbounded rather than exclude an object-key match.
|
|
1295
|
+
if (hasDeepNumericPath(expression)) return undefined
|
|
1296
|
+
compiled ??= compileSqlExpression(expression)
|
|
1297
|
+
if (!compiled.supported) return undefined
|
|
1298
|
+
const [left, right] = expression.args
|
|
1299
|
+
if (
|
|
1300
|
+
expression.name === `not` &&
|
|
1301
|
+
expression.args.length === 1 &&
|
|
1302
|
+
left &&
|
|
1303
|
+
nativeComparisonRef(left)
|
|
1304
|
+
) {
|
|
1305
|
+
// Negate the original leaf, never a broadened prefilter. Nonfinite stored
|
|
1306
|
+
// scalars project to NULL; retaining them also retains valid NOT matches.
|
|
1307
|
+
return { ...compiled, sql: `COALESCE(${compiled.sql}, 1)` }
|
|
1308
|
+
}
|
|
1309
|
+
if (expression.args.length === 2) {
|
|
1310
|
+
const nativeRef = nativeComparisonRef(expression)
|
|
1311
|
+
if (nativeRef) {
|
|
1312
|
+
if (expression.name === `eq` || expression.name === `in`) return compiled
|
|
1313
|
+
// NaN orders above native values in the core evaluator. NULL candidates
|
|
1314
|
+
// also preserve that law with either operand direction.
|
|
1315
|
+
const fieldSql = compileSqlExpression(nativeRef, `index-expression`).sql
|
|
1316
|
+
return { ...compiled, sql: `(${compiled.sql} OR ${fieldSql} IS NULL)` }
|
|
1317
|
+
}
|
|
1318
|
+
if (
|
|
1319
|
+
expression.name === `eq` &&
|
|
1320
|
+
left?.type === `ref` &&
|
|
1321
|
+
right?.type === `ref`
|
|
1322
|
+
) {
|
|
1323
|
+
// Ref/ref scalar coercions are not generally SQL-exact. Restrict only
|
|
1324
|
+
// native pairs; leave every other pair to the authoritative evaluator.
|
|
1325
|
+
return {
|
|
1326
|
+
...compiled,
|
|
1327
|
+
sql: `(CASE WHEN ${compileNativeRefGuard(left)} AND ${compileNativeRefGuard(right)} THEN ${compiled.sql} ELSE 1 END)`,
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (
|
|
1331
|
+
[`gt`, `gte`, `lt`, `lte`].includes(expression.name) &&
|
|
1332
|
+
(left?.type === `ref` || right?.type === `ref`)
|
|
1333
|
+
) {
|
|
1334
|
+
const field =
|
|
1335
|
+
left?.type === `ref` ? left : right?.type === `ref` ? right : undefined
|
|
1336
|
+
const literal = field === left ? right : left
|
|
1337
|
+
if (
|
|
1338
|
+
field &&
|
|
1339
|
+
literal?.type === `val` &&
|
|
1340
|
+
((typeof literal.value === `number` &&
|
|
1341
|
+
Number.isSafeInteger(literal.value)) ||
|
|
1342
|
+
typeof literal.value === `bigint`)
|
|
1343
|
+
) {
|
|
1344
|
+
const fieldSql = compileSqlExpression(field, `index-expression`).sql
|
|
1345
|
+
const literalSql = compileSqlExpression(
|
|
1346
|
+
literal,
|
|
1347
|
+
typeof literal.value === `bigint` ? `index-expression` : `predicate`,
|
|
1348
|
+
)
|
|
1349
|
+
const greaterSide =
|
|
1350
|
+
(field === left && [`gt`, `gte`].includes(expression.name)) ||
|
|
1351
|
+
(field === right && [`lt`, `lte`].includes(expression.name))
|
|
1352
|
+
const unsafeBigInt =
|
|
1353
|
+
typeof literal.value === `bigint` &&
|
|
1354
|
+
!Number.isSafeInteger(Number(literal.value))
|
|
1355
|
+
const roundedNumberCandidates = unsafeBigInt
|
|
1356
|
+
? literal.value > 0n
|
|
1357
|
+
? ` OR ${fieldSql} >= ${Number.MAX_SAFE_INTEGER}`
|
|
1358
|
+
: ` OR ${fieldSql} <= -${Number.MAX_SAFE_INTEGER}`
|
|
1359
|
+
: ``
|
|
1360
|
+
return {
|
|
1361
|
+
supported: true,
|
|
1362
|
+
sql: greaterSide
|
|
1363
|
+
? `(${fieldSql} >= ${literalSql.sql} OR ${fieldSql} IS NULL${roundedNumberCandidates})`
|
|
1364
|
+
: `(${fieldSql} <= ${literalSql.sql} OR ${fieldSql} IS NULL OR ${fieldSql} >= ''${roundedNumberCandidates})`,
|
|
1365
|
+
params: literalSql.params,
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (expression.name === `eq`) {
|
|
1370
|
+
if (
|
|
1371
|
+
((left?.type === `ref` || isSafeCoalesceOperand(left)) &&
|
|
1372
|
+
right?.type === `val` &&
|
|
1373
|
+
isSafeEqualityLiteral(right.value)) ||
|
|
1374
|
+
((right?.type === `ref` || isSafeCoalesceOperand(right)) &&
|
|
1375
|
+
left?.type === `val` &&
|
|
1376
|
+
isSafeEqualityLiteral(left.value))
|
|
1377
|
+
) {
|
|
1378
|
+
return compiled
|
|
1379
|
+
}
|
|
1380
|
+
const lower =
|
|
1381
|
+
left?.type === `func` && left.name === `lower`
|
|
1382
|
+
? left
|
|
1383
|
+
: right?.type === `func` && right.name === `lower`
|
|
1384
|
+
? right
|
|
1385
|
+
: undefined
|
|
1386
|
+
const literal = lower === left ? right : left
|
|
1387
|
+
if (
|
|
1388
|
+
lower?.args.length === 1 &&
|
|
1389
|
+
lower.args[0]?.type === `ref` &&
|
|
1390
|
+
literal?.type === `val` &&
|
|
1391
|
+
typeof literal.value === `string` &&
|
|
1392
|
+
[...literal.value].every((char) => char.charCodeAt(0) <= 0x7f) &&
|
|
1393
|
+
!literal.value.includes(`\u0000`)
|
|
1394
|
+
) {
|
|
1395
|
+
const lowerSql = compileSqlExpression(lower, `index-expression`).sql
|
|
1396
|
+
return {
|
|
1397
|
+
supported: true,
|
|
1398
|
+
sql: `(${compiled.sql} OR (${lowerSql} >= ? AND ${lowerSql} GLOB '*[^ -~]*'))`,
|
|
1399
|
+
params: [...compiled.params, literal.value],
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (
|
|
1405
|
+
expression.name === `in` &&
|
|
1406
|
+
expression.args.length === 2 &&
|
|
1407
|
+
right?.type === `val` &&
|
|
1408
|
+
Array.isArray(right.value)
|
|
1409
|
+
) {
|
|
1410
|
+
if (right.value.length === 0) return compiled
|
|
1411
|
+
if (left?.type === `ref`) {
|
|
1412
|
+
if (right.value.every((value) => typeof value === `bigint`))
|
|
1413
|
+
return compiled
|
|
1414
|
+
if (right.value.every((value) => typeof value === `string`)) {
|
|
1415
|
+
// Both sides pass through SQLite's JSON decoder; one binding keeps
|
|
1416
|
+
// large string scopes within the host parameter limit, even in OR.
|
|
1417
|
+
return {
|
|
1418
|
+
supported: true,
|
|
1419
|
+
sql: `(${compileSqlExpression(left, `index-expression`).sql} IN (SELECT value FROM json_each(?)))`,
|
|
1420
|
+
params: [JSON.stringify(right.value)],
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (
|
|
1426
|
+
expression.name === `like` &&
|
|
1427
|
+
expression.args.length === 2 &&
|
|
1428
|
+
left?.type === `ref` &&
|
|
1429
|
+
right?.type === `val` &&
|
|
1430
|
+
typeof right.value === `string` &&
|
|
1431
|
+
/^[\x20-\x7e]*%$/.test(right.value) &&
|
|
1432
|
+
!right.value.slice(0, -1).includes(`%`) &&
|
|
1433
|
+
!right.value.includes(`_`)
|
|
1434
|
+
) {
|
|
1435
|
+
return compiled
|
|
1436
|
+
}
|
|
1437
|
+
return undefined
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1053
1440
|
function compileOrderByClauses(
|
|
1054
1441
|
orderBy: IR.OrderBy | undefined,
|
|
1055
1442
|
): CompiledSqlFragment {
|
|
@@ -1175,6 +1562,39 @@ function mergeObjectRows<T extends object>(existing: unknown, incoming: T): T {
|
|
|
1175
1562
|
return incoming
|
|
1176
1563
|
}
|
|
1177
1564
|
|
|
1565
|
+
function* batches<T>(
|
|
1566
|
+
changes: ReadonlyArray<T>,
|
|
1567
|
+
maxSize: number,
|
|
1568
|
+
): Generator<Array<T>> {
|
|
1569
|
+
for (let offset = 0; offset < changes.length; offset += maxSize) {
|
|
1570
|
+
yield changes.slice(offset, offset + maxSize)
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function lastMetadataByKey<
|
|
1575
|
+
T extends { type: `set` | `delete`; value?: unknown },
|
|
1576
|
+
>(
|
|
1577
|
+
changes: ReadonlyArray<T>,
|
|
1578
|
+
keyOf: (change: T) => string,
|
|
1579
|
+
undefinedSetIsNull = false,
|
|
1580
|
+
): Array<T> {
|
|
1581
|
+
const latest = new Map<string, T>()
|
|
1582
|
+
for (const change of changes) {
|
|
1583
|
+
const key = keyOf(change)
|
|
1584
|
+
const previous = latest.get(key)
|
|
1585
|
+
if (
|
|
1586
|
+
previous?.type === `set` &&
|
|
1587
|
+
(previous.value !== undefined || !undefinedSetIsNull) &&
|
|
1588
|
+
(serializePersistedRowValue(previous.value) as string | undefined) ===
|
|
1589
|
+
undefined
|
|
1590
|
+
) {
|
|
1591
|
+
throw new TypeError(`Metadata value cannot be bound to SQLite`)
|
|
1592
|
+
}
|
|
1593
|
+
latest.set(key, change)
|
|
1594
|
+
}
|
|
1595
|
+
return [...latest.values()]
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1178
1598
|
function buildIndexName(collectionId: string, signature: string): string {
|
|
1179
1599
|
const sanitizedSignature = signature
|
|
1180
1600
|
.toLowerCase()
|
|
@@ -1672,58 +2092,134 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1672
2092
|
await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`)
|
|
1673
2093
|
}
|
|
1674
2094
|
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
-
)
|
|
2095
|
+
const rowMutations = new Map<string, PersistedTx[`mutations`][number]>()
|
|
2096
|
+
for (const mutation of tx.mutations) {
|
|
2097
|
+
const key = encodePersistedStorageKey(mutation.key)
|
|
2098
|
+
const previous = rowMutations.get(key)
|
|
2099
|
+
if (!previous) {
|
|
2100
|
+
rowMutations.set(key, mutation)
|
|
2101
|
+
continue
|
|
1693
2102
|
}
|
|
1694
2103
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2104
|
+
// A superseded action still had to serialize successfully.
|
|
2105
|
+
serializePersistedRowValue(previous.value)
|
|
2106
|
+
if (
|
|
2107
|
+
previous.type !== `delete` &&
|
|
2108
|
+
previous.metadataChanged === true &&
|
|
2109
|
+
previous.metadata !== undefined
|
|
1699
2110
|
) {
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
)
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2111
|
+
serializePersistedRowValue(previous.metadata)
|
|
2112
|
+
}
|
|
2113
|
+
if (mutation.type === `delete`) {
|
|
2114
|
+
rowMutations.set(key, mutation)
|
|
2115
|
+
continue
|
|
2116
|
+
}
|
|
2117
|
+
const value =
|
|
2118
|
+
mutation.type === `insert` || previous.type === `delete`
|
|
2119
|
+
? mutation.value
|
|
2120
|
+
: mergeObjectRows(previous.value, mutation.value)
|
|
2121
|
+
const metadataChanged =
|
|
2122
|
+
mutation.metadataChanged === true ||
|
|
2123
|
+
previous.type === `delete` ||
|
|
2124
|
+
previous.metadataChanged === true
|
|
2125
|
+
const metadata =
|
|
2126
|
+
mutation.metadataChanged === true
|
|
2127
|
+
? mutation.metadata
|
|
2128
|
+
: previous.type === `delete`
|
|
2129
|
+
? undefined
|
|
2130
|
+
: previous.metadata
|
|
2131
|
+
const next = { key: mutation.key, value, metadataChanged, metadata }
|
|
2132
|
+
rowMutations.set(
|
|
2133
|
+
key,
|
|
2134
|
+
mutation.type === `insert` || previous.type !== `update`
|
|
2135
|
+
? { type: `insert`, ...next }
|
|
2136
|
+
: { type: `update`, ...next },
|
|
2137
|
+
)
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
for (const batch of batches(
|
|
2141
|
+
[...rowMutations.values()],
|
|
2142
|
+
this.replacementBatchSize,
|
|
2143
|
+
)) {
|
|
2144
|
+
const writes = batch.filter((mutation) => mutation.type !== `delete`)
|
|
2145
|
+
const deletes = batch.filter((mutation) => mutation.type === `delete`)
|
|
2146
|
+
const readKeys = writes
|
|
2147
|
+
.filter(
|
|
2148
|
+
(mutation) =>
|
|
2149
|
+
mutation.type === `update` || mutation.metadataChanged !== true,
|
|
1707
2150
|
)
|
|
2151
|
+
.map((mutation) => encodePersistedStorageKey(mutation.key))
|
|
2152
|
+
const existingRows =
|
|
2153
|
+
readKeys.length > 0
|
|
2154
|
+
? await transactionDriver.query<{
|
|
2155
|
+
key: string
|
|
2156
|
+
value: string
|
|
2157
|
+
metadata: string | null
|
|
2158
|
+
}>(
|
|
2159
|
+
`SELECT key, value, metadata
|
|
2160
|
+
FROM ${collectionTableSql}
|
|
2161
|
+
WHERE key IN (${readKeys.map(() => `?`).join(`, `)})`,
|
|
2162
|
+
readKeys,
|
|
2163
|
+
)
|
|
2164
|
+
: []
|
|
2165
|
+
const existing = new Map(existingRows.map((row) => [row.key, row]))
|
|
2166
|
+
const writeKeys = writes.map((mutation) =>
|
|
2167
|
+
encodePersistedStorageKey(mutation.key),
|
|
2168
|
+
)
|
|
2169
|
+
const deleteKeys = deletes.map((mutation) =>
|
|
2170
|
+
encodePersistedStorageKey(mutation.key),
|
|
2171
|
+
)
|
|
2172
|
+
|
|
2173
|
+
if (tracksPersistedKeySet && writeKeys.length > 0) {
|
|
1708
2174
|
await transactionDriver.run(
|
|
1709
2175
|
`INSERT INTO collection_expected_keys (collection_id, key)
|
|
1710
|
-
VALUES ${
|
|
1711
|
-
|
|
2176
|
+
VALUES ${writeKeys.map(() => `(?, ?)`).join(`, `)}
|
|
2177
|
+
ON CONFLICT(collection_id, key) DO NOTHING`,
|
|
2178
|
+
writeKeys.flatMap((key) => [collectionId, key]),
|
|
1712
2179
|
)
|
|
2180
|
+
}
|
|
2181
|
+
if (tracksPersistedKeySet && deleteKeys.length > 0) {
|
|
2182
|
+
await transactionDriver.run(
|
|
2183
|
+
`DELETE FROM collection_expected_keys
|
|
2184
|
+
WHERE collection_id = ? AND key IN (${deleteKeys.map(() => `?`).join(`, `)})`,
|
|
2185
|
+
[collectionId, ...deleteKeys],
|
|
2186
|
+
)
|
|
2187
|
+
}
|
|
2188
|
+
if (deleteKeys.length > 0) {
|
|
2189
|
+
await transactionDriver.run(
|
|
2190
|
+
`DELETE FROM ${collectionTableSql}
|
|
2191
|
+
WHERE key IN (${deleteKeys.map(() => `?`).join(`, `)})`,
|
|
2192
|
+
deleteKeys,
|
|
2193
|
+
)
|
|
2194
|
+
}
|
|
2195
|
+
if (writeKeys.length > 0) {
|
|
1713
2196
|
await transactionDriver.run(
|
|
1714
2197
|
`INSERT INTO ${collectionTableSql} (key, value, metadata, row_version)
|
|
1715
|
-
VALUES ${
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2198
|
+
VALUES ${writeKeys.map(() => `(?, ?, ?, ?)`).join(`, `)}
|
|
2199
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
2200
|
+
value = excluded.value,
|
|
2201
|
+
metadata = excluded.metadata,
|
|
2202
|
+
row_version = excluded.row_version`,
|
|
2203
|
+
writes.flatMap((mutation, index) => {
|
|
2204
|
+
const previous = existing.get(writeKeys[index]!)
|
|
2205
|
+
const previousValue = previous?.value
|
|
2206
|
+
? deserializePersistedRowValue(previous.value)
|
|
2207
|
+
: undefined
|
|
2208
|
+
const previousMetadata =
|
|
2209
|
+
previous?.metadata != null
|
|
2210
|
+
? deserializePersistedRowValue(previous.metadata)
|
|
1723
2211
|
: undefined
|
|
2212
|
+
const value =
|
|
2213
|
+
mutation.type === `update`
|
|
2214
|
+
? mergeObjectRows(previousValue, mutation.value)
|
|
2215
|
+
: mutation.value
|
|
2216
|
+
const metadata =
|
|
2217
|
+
mutation.metadataChanged === true
|
|
2218
|
+
? mutation.metadata
|
|
2219
|
+
: previousMetadata
|
|
1724
2220
|
return [
|
|
1725
|
-
|
|
1726
|
-
serializePersistedRowValue(
|
|
2221
|
+
writeKeys[index]!,
|
|
2222
|
+
serializePersistedRowValue(value),
|
|
1727
2223
|
metadata === undefined
|
|
1728
2224
|
? null
|
|
1729
2225
|
: serializePersistedRowValue(metadata),
|
|
@@ -1731,143 +2227,85 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1731
2227
|
]
|
|
1732
2228
|
}),
|
|
1733
2229
|
)
|
|
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
2230
|
await transactionDriver.run(
|
|
1748
|
-
`DELETE FROM ${
|
|
1749
|
-
WHERE key
|
|
1750
|
-
|
|
2231
|
+
`DELETE FROM ${tombstoneTableSql}
|
|
2232
|
+
WHERE key IN (${writeKeys.map(() => `?`).join(`, `)})`,
|
|
2233
|
+
writeKeys,
|
|
1751
2234
|
)
|
|
2235
|
+
}
|
|
2236
|
+
if (deletes.length > 0) {
|
|
1752
2237
|
await transactionDriver.run(
|
|
1753
2238
|
`INSERT INTO ${tombstoneTableSql} (key, value, row_version, deleted_at)
|
|
1754
|
-
VALUES (?, ?, ?, ?)
|
|
2239
|
+
VALUES ${deleteKeys.map(() => `(?, ?, ?, ?)`).join(`, `)}
|
|
1755
2240
|
ON CONFLICT(key) DO UPDATE SET
|
|
1756
2241
|
value = excluded.value,
|
|
1757
2242
|
row_version = excluded.row_version,
|
|
1758
2243
|
deleted_at = excluded.deleted_at`,
|
|
1759
|
-
[
|
|
1760
|
-
|
|
2244
|
+
deletes.flatMap((mutation, index) => [
|
|
2245
|
+
deleteKeys[index]!,
|
|
1761
2246
|
serializePersistedRowValue(mutation.value),
|
|
1762
2247
|
nextRowVersion,
|
|
1763
2248
|
new Date().toISOString(),
|
|
1764
|
-
],
|
|
2249
|
+
]),
|
|
1765
2250
|
)
|
|
1766
|
-
continue
|
|
1767
2251
|
}
|
|
2252
|
+
}
|
|
1768
2253
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
2254
|
+
for (const batch of batches(
|
|
2255
|
+
lastMetadataByKey(
|
|
2256
|
+
tx.rowMetadataMutations ?? [],
|
|
2257
|
+
(mutation) => encodePersistedStorageKey(mutation.key),
|
|
2258
|
+
true,
|
|
2259
|
+
),
|
|
2260
|
+
this.replacementBatchSize,
|
|
2261
|
+
)) {
|
|
2262
|
+
const keys = batch.map((mutation) =>
|
|
2263
|
+
encodePersistedStorageKey(mutation.key),
|
|
1778
2264
|
)
|
|
1779
|
-
const existingValue = existingRows[0]?.value
|
|
1780
|
-
? deserializePersistedRowValue(existingRows[0].value)
|
|
1781
|
-
: undefined
|
|
1782
|
-
const existingMetadata =
|
|
1783
|
-
existingRows[0]?.metadata != null
|
|
1784
|
-
? deserializePersistedRowValue(existingRows[0].metadata)
|
|
1785
|
-
: undefined
|
|
1786
|
-
const mergedValue =
|
|
1787
|
-
mutation.type === `update`
|
|
1788
|
-
? mergeObjectRows(existingValue, mutation.value)
|
|
1789
|
-
: mutation.value
|
|
1790
|
-
const nextMetadata =
|
|
1791
|
-
mutation.metadataChanged === true
|
|
1792
|
-
? mutation.metadata
|
|
1793
|
-
: existingMetadata
|
|
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
|
-
}
|
|
1803
2265
|
await transactionDriver.run(
|
|
1804
|
-
`
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
value = excluded.value,
|
|
1808
|
-
metadata = excluded.metadata,
|
|
1809
|
-
row_version = excluded.row_version`,
|
|
2266
|
+
`UPDATE ${collectionTableSql}
|
|
2267
|
+
SET metadata = CASE key ${keys.map(() => `WHEN ? THEN ?`).join(` `)} END
|
|
2268
|
+
WHERE key IN (${keys.map(() => `?`).join(`, `)})`,
|
|
1810
2269
|
[
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2270
|
+
...batch.flatMap((mutation, index) => [
|
|
2271
|
+
keys[index]!,
|
|
2272
|
+
mutation.type === `delete` || mutation.value === undefined
|
|
2273
|
+
? null
|
|
2274
|
+
: serializePersistedRowValue(mutation.value),
|
|
2275
|
+
]),
|
|
2276
|
+
...keys,
|
|
1817
2277
|
],
|
|
1818
2278
|
)
|
|
1819
|
-
await transactionDriver.run(
|
|
1820
|
-
`DELETE FROM ${tombstoneTableSql}
|
|
1821
|
-
WHERE key = ?`,
|
|
1822
|
-
[encodedKey],
|
|
1823
|
-
)
|
|
1824
2279
|
}
|
|
1825
2280
|
|
|
1826
|
-
for (const
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
)
|
|
1837
|
-
} else {
|
|
1838
|
-
await transactionDriver.run(
|
|
1839
|
-
`UPDATE ${collectionTableSql}
|
|
1840
|
-
SET metadata = ?
|
|
1841
|
-
WHERE key = ?`,
|
|
1842
|
-
[
|
|
1843
|
-
rowMetadataMutation.value === undefined
|
|
1844
|
-
? null
|
|
1845
|
-
: serializePersistedRowValue(rowMetadataMutation.value),
|
|
1846
|
-
encodedKey,
|
|
1847
|
-
],
|
|
1848
|
-
)
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
for (const metadataMutation of tx.collectionMetadataMutations ?? []) {
|
|
1853
|
-
if (metadataMutation.type === `delete`) {
|
|
2281
|
+
for (const batch of batches(
|
|
2282
|
+
lastMetadataByKey(
|
|
2283
|
+
tx.collectionMetadataMutations ?? [],
|
|
2284
|
+
(mutation) => mutation.key,
|
|
2285
|
+
),
|
|
2286
|
+
this.replacementBatchSize,
|
|
2287
|
+
)) {
|
|
2288
|
+
const deletes = batch.filter((mutation) => mutation.type === `delete`)
|
|
2289
|
+
const sets = batch.filter((mutation) => mutation.type === `set`)
|
|
2290
|
+
if (deletes.length > 0) {
|
|
1854
2291
|
await transactionDriver.run(
|
|
1855
2292
|
`DELETE FROM collection_metadata
|
|
1856
|
-
WHERE collection_id = ? AND key
|
|
1857
|
-
[collectionId,
|
|
2293
|
+
WHERE collection_id = ? AND key IN (${deletes.map(() => `?`).join(`, `)})`,
|
|
2294
|
+
[collectionId, ...deletes.map((mutation) => mutation.key)],
|
|
1858
2295
|
)
|
|
1859
|
-
}
|
|
2296
|
+
}
|
|
2297
|
+
if (sets.length > 0) {
|
|
1860
2298
|
await transactionDriver.run(
|
|
1861
2299
|
`INSERT INTO collection_metadata (collection_id, key, value, updated_at)
|
|
1862
|
-
VALUES (?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
|
|
2300
|
+
VALUES ${sets.map(() => `(?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))`).join(`, `)}
|
|
1863
2301
|
ON CONFLICT(collection_id, key) DO UPDATE SET
|
|
1864
2302
|
value = excluded.value,
|
|
1865
2303
|
updated_at = excluded.updated_at`,
|
|
1866
|
-
[
|
|
2304
|
+
sets.flatMap((mutation) => [
|
|
1867
2305
|
collectionId,
|
|
1868
|
-
|
|
1869
|
-
serializePersistedRowValue(
|
|
1870
|
-
],
|
|
2306
|
+
mutation.key,
|
|
2307
|
+
serializePersistedRowValue(mutation.value),
|
|
2308
|
+
]),
|
|
1871
2309
|
)
|
|
1872
2310
|
}
|
|
1873
2311
|
}
|
|
@@ -2419,17 +2857,22 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2419
2857
|
driver: SQLiteDriver = this.driver,
|
|
2420
2858
|
): Promise<Array<InMemoryRow<string | number, Record<string, unknown>>>> {
|
|
2421
2859
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2860
|
+
// Compile even when SQL cannot safely prefilter: invalid bindings must
|
|
2861
|
+
// reject before the in-memory fallback reads rows.
|
|
2422
2862
|
const whereCompiled = options.where
|
|
2423
2863
|
? compileSqlExpression(options.where)
|
|
2424
|
-
: { supported:
|
|
2864
|
+
: { supported: false, sql: ``, params: [] as Array<SqliteSupportedValue> }
|
|
2865
|
+
const safePrefilter = options.where
|
|
2866
|
+
? compileSafeSqlPrefilter(options.where, whereCompiled)
|
|
2867
|
+
: undefined
|
|
2425
2868
|
const orderByCompiled = compileOrderByClauses(options.orderBy)
|
|
2426
2869
|
|
|
2427
2870
|
const queryParams: Array<SqliteSupportedValue> = []
|
|
2428
2871
|
let sql = `SELECT key, value, metadata, row_version FROM ${collectionTableSql}`
|
|
2429
2872
|
|
|
2430
|
-
if (
|
|
2431
|
-
sql = `${sql} WHERE ${
|
|
2432
|
-
queryParams.push(...
|
|
2873
|
+
if (safePrefilter) {
|
|
2874
|
+
sql = `${sql} WHERE ${safePrefilter.sql}`
|
|
2875
|
+
queryParams.push(...safePrefilter.params)
|
|
2433
2876
|
}
|
|
2434
2877
|
|
|
2435
2878
|
if (options.orderBy && orderByCompiled.supported) {
|
|
@@ -2437,6 +2880,14 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2437
2880
|
queryParams.push(...orderByCompiled.params)
|
|
2438
2881
|
}
|
|
2439
2882
|
|
|
2883
|
+
if (
|
|
2884
|
+
queryParams.length >
|
|
2885
|
+
(driver.maxBoundParameters ?? this.driver.maxBoundParameters ?? 999)
|
|
2886
|
+
) {
|
|
2887
|
+
sql = `SELECT key, value, metadata, row_version FROM ${collectionTableSql}`
|
|
2888
|
+
queryParams.length = 0
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2440
2891
|
const storedRows = await driver.query<StoredSqliteRow>(sql, queryParams)
|
|
2441
2892
|
const parsedRows = decodeStoredSqliteRows(storedRows)
|
|
2442
2893
|
|