@tanstack/db-sqlite-persistence-core 0.4.1 → 0.4.4
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/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/persisted.cjs +47 -52
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/remote-subset-wire.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.cjs +303 -162
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/persisted.js +49 -54
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-wire.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.js +304 -163
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.ts +1 -2
- package/src/persisted.ts +80 -75
- package/src/remote-subset-wire.ts +1 -6
- package/src/sqlite-core-adapter.ts +478 -214
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IR, toBooleanPredicate, compileSingleRowExpression } from "@tanstack/db";
|
|
2
2
|
import { InvalidPersistedCollectionConfigError, InvalidPersistedStorageKeyEncodingError } from "./errors.js";
|
|
3
3
|
import { encodePersistedStorageKey, createPersistedTableName, decodePersistedStorageKey, SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY } from "./persisted.js";
|
|
4
|
-
import { PERSISTED_TYPE_TAG, PERSISTED_VALUE_TAG, serializeSQLiteBigInt
|
|
5
|
-
const REPLACEMENT_BATCH_SIZE =
|
|
4
|
+
import { assertSQLiteBigIntInRange, PERSISTED_TYPE_TAG, PERSISTED_VALUE_TAG, serializeSQLiteBigInt } from "./sqlite-value.js";
|
|
5
|
+
const REPLACEMENT_BATCH_SIZE = 125;
|
|
6
6
|
class SharedPersistenceScheduler {
|
|
7
7
|
constructor() {
|
|
8
8
|
this.regularQueue = [];
|
|
@@ -94,6 +94,7 @@ function observeSharedLogicalSchedulingSupport(driver, onSupport) {
|
|
|
94
94
|
return promise;
|
|
95
95
|
};
|
|
96
96
|
return {
|
|
97
|
+
maxBoundParameters: driver.maxBoundParameters,
|
|
97
98
|
exec: (sql) => observe(driver.exec(sql)),
|
|
98
99
|
query: (sql, params = []) => observe(driver.query(sql, params)),
|
|
99
100
|
run: (sql, params = []) => observe(driver.run(sql, params)),
|
|
@@ -107,7 +108,6 @@ const DEFAULT_SCHEMA_VERSION = 1;
|
|
|
107
108
|
const DEFAULT_PULL_SINCE_RELOAD_THRESHOLD = 128;
|
|
108
109
|
const DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS = 1e3;
|
|
109
110
|
const DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
110
|
-
const SQLITE_MAX_IN_BATCH_SIZE = 900;
|
|
111
111
|
const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
112
112
|
const FORBIDDEN_SQL_FRAGMENT_PATTERN = /(;|--|\/\*)/;
|
|
113
113
|
const persistedTaggedValueTypes = /* @__PURE__ */ new Set([
|
|
@@ -379,6 +379,33 @@ function createJsonPath(path) {
|
|
|
379
379
|
}
|
|
380
380
|
return jsonPath;
|
|
381
381
|
}
|
|
382
|
+
const MAX_INDEXED_NUMERIC_PATH_SEGMENTS = 3;
|
|
383
|
+
function hasDeepNumericPath(expression) {
|
|
384
|
+
if (expression.type === `ref`) {
|
|
385
|
+
return IR.getPropRefPropertyPath(expression).filter(
|
|
386
|
+
(segment) => /^[0-9]+$/.test(String(segment))
|
|
387
|
+
).length > MAX_INDEXED_NUMERIC_PATH_SEGMENTS;
|
|
388
|
+
}
|
|
389
|
+
return expression.type === `func` && expression.args.some(hasDeepNumericPath);
|
|
390
|
+
}
|
|
391
|
+
function createJsonPathVariants(path) {
|
|
392
|
+
const canonical = createJsonPath(path);
|
|
393
|
+
if (!canonical) return null;
|
|
394
|
+
let paths = [`$`];
|
|
395
|
+
for (const segment of path) {
|
|
396
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)) {
|
|
397
|
+
paths = paths.map((prefix) => `${prefix}.${segment}`);
|
|
398
|
+
} else if (/^[0-9]+$/.test(segment)) {
|
|
399
|
+
if (paths.length >= 2 ** MAX_INDEXED_NUMERIC_PATH_SEGMENTS)
|
|
400
|
+
return [canonical];
|
|
401
|
+
paths = paths.flatMap((prefix) => [
|
|
402
|
+
`${prefix}[${segment}]`,
|
|
403
|
+
`${prefix}."${segment}"`
|
|
404
|
+
]);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return paths;
|
|
408
|
+
}
|
|
382
409
|
function getLiteralValueKind(value) {
|
|
383
410
|
if (typeof value === `bigint`) {
|
|
384
411
|
return `bigint`;
|
|
@@ -486,8 +513,24 @@ function argumentCompilationContext(parentName, argumentIndex, argument, parentC
|
|
|
486
513
|
return `index-expression`;
|
|
487
514
|
}
|
|
488
515
|
}
|
|
516
|
+
function hasUnpairedSurrogate(value) {
|
|
517
|
+
return /[\uD800-\uDFFF]/u.test(value);
|
|
518
|
+
}
|
|
519
|
+
function isSafeEqualityLiteral(value) {
|
|
520
|
+
return typeof value === `string` || typeof value === `boolean` || typeof value === `bigint`;
|
|
521
|
+
}
|
|
522
|
+
function isSafeCoalesceOperand(expression) {
|
|
523
|
+
return expression?.type === `func` && expression.name === `coalesce` && expression.args.length === 2 && expression.args[0]?.type === `ref` && expression.args[1]?.type === `val` && isSafeEqualityLiteral(expression.args[1].value) && !(typeof expression.args[1].value === `string` && hasUnpairedSurrogate(expression.args[1].value));
|
|
524
|
+
}
|
|
489
525
|
function compileSqlExpression(expression, context = `predicate`) {
|
|
490
526
|
if (expression.type === `val`) {
|
|
527
|
+
if (context === `predicate` && typeof expression.value === `string` && hasUnpairedSurrogate(expression.value)) {
|
|
528
|
+
return {
|
|
529
|
+
supported: true,
|
|
530
|
+
sql: `json_extract(?, '$')`,
|
|
531
|
+
params: [JSON.stringify(expression.value)]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
491
534
|
const valueKind = getLiteralValueKind(expression.value);
|
|
492
535
|
return {
|
|
493
536
|
supported: true,
|
|
@@ -497,17 +540,23 @@ function compileSqlExpression(expression, context = `predicate`) {
|
|
|
497
540
|
};
|
|
498
541
|
}
|
|
499
542
|
if (expression.type === `ref`) {
|
|
500
|
-
const
|
|
543
|
+
const jsonPaths = createJsonPathVariants(
|
|
501
544
|
IR.getPropRefPropertyPath(expression).map(String)
|
|
502
545
|
);
|
|
503
|
-
if (!
|
|
546
|
+
if (!jsonPaths) {
|
|
504
547
|
return {
|
|
505
548
|
supported: false,
|
|
506
549
|
sql: ``,
|
|
507
550
|
params: []
|
|
508
551
|
};
|
|
509
552
|
}
|
|
510
|
-
return compileRefExpressionSql(
|
|
553
|
+
if (jsonPaths.length === 1) return compileRefExpressionSql(jsonPaths[0]);
|
|
554
|
+
return {
|
|
555
|
+
supported: true,
|
|
556
|
+
sql: `COALESCE(${jsonPaths.map((path) => compileRefExpressionSql(path).sql).join(`, `)})`,
|
|
557
|
+
params: [],
|
|
558
|
+
valueKind: `unknown`
|
|
559
|
+
};
|
|
511
560
|
}
|
|
512
561
|
const compiledArgs = expression.args.map(
|
|
513
562
|
(arg, index) => compileSqlExpression(
|
|
@@ -561,23 +610,18 @@ function compileSqlExpression(expression, context = `predicate`) {
|
|
|
561
610
|
params
|
|
562
611
|
};
|
|
563
612
|
}
|
|
564
|
-
case `and`:
|
|
565
|
-
if (argSql.length < 2) {
|
|
566
|
-
return { supported: false, sql: ``, params: [] };
|
|
567
|
-
}
|
|
568
|
-
return {
|
|
569
|
-
supported: true,
|
|
570
|
-
sql: argSql.map((sql) => `(${sql})`).join(` AND `),
|
|
571
|
-
params
|
|
572
|
-
};
|
|
573
|
-
}
|
|
613
|
+
case `and`:
|
|
574
614
|
case `or`: {
|
|
575
|
-
if (argSql.length
|
|
576
|
-
return {
|
|
615
|
+
if (argSql.length === 0) {
|
|
616
|
+
return {
|
|
617
|
+
supported: true,
|
|
618
|
+
sql: expression.name === `and` ? `(1 = 1)` : `(0 = 1)`,
|
|
619
|
+
params: []
|
|
620
|
+
};
|
|
577
621
|
}
|
|
578
622
|
return {
|
|
579
623
|
supported: true,
|
|
580
|
-
sql: argSql.map((sql) => `(${sql})`).join(` OR `),
|
|
624
|
+
sql: argSql.map((sql) => `(${sql})`).join(expression.name === `and` ? ` AND ` : ` OR `),
|
|
581
625
|
params
|
|
582
626
|
};
|
|
583
627
|
}
|
|
@@ -614,37 +658,13 @@ function compileSqlExpression(expression, context = `predicate`) {
|
|
|
614
658
|
params: leftParams
|
|
615
659
|
};
|
|
616
660
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
for (let startIndex = 0; startIndex < listValue.length; startIndex += SQLITE_MAX_IN_BATCH_SIZE) {
|
|
621
|
-
const chunkValues = listValue.slice(
|
|
622
|
-
startIndex,
|
|
623
|
-
startIndex + SQLITE_MAX_IN_BATCH_SIZE
|
|
624
|
-
);
|
|
625
|
-
const chunkParams = [];
|
|
626
|
-
const chunkValueSql = chunkValues.map((value) => {
|
|
627
|
-
chunkParams.push(toSqliteParameterValue(value));
|
|
628
|
-
return typeof value === `bigint` ? `CAST(? AS NUMERIC)` : `?`;
|
|
629
|
-
});
|
|
630
|
-
chunkClauses.push(`(${leftSql} IN (${chunkValueSql.join(`, `)}))`);
|
|
631
|
-
batchedParams.push(...leftParams, ...chunkParams);
|
|
632
|
-
}
|
|
633
|
-
return {
|
|
634
|
-
supported: true,
|
|
635
|
-
sql: `(${chunkClauses.join(` OR `)})`,
|
|
636
|
-
params: batchedParams
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
const listParams = [];
|
|
640
|
-
const listValueSql = listValue.map((value) => {
|
|
641
|
-
listParams.push(toSqliteParameterValue(value));
|
|
642
|
-
return typeof value === `bigint` ? `CAST(? AS NUMERIC)` : `?`;
|
|
643
|
-
});
|
|
661
|
+
const jsonList = `[${listValue.map(
|
|
662
|
+
(value) => typeof value === `bigint` ? assertSQLiteBigIntInRange(value).toString() : JSON.stringify(toSqliteParameterValue(value))
|
|
663
|
+
).join(`,`)}]`;
|
|
644
664
|
return {
|
|
645
665
|
supported: true,
|
|
646
|
-
sql: `(${leftSql} IN (
|
|
647
|
-
params: [...leftParams,
|
|
666
|
+
sql: `(${leftSql} IN (SELECT value FROM json_each(?)))`,
|
|
667
|
+
params: [...leftParams, jsonList]
|
|
648
668
|
};
|
|
649
669
|
}
|
|
650
670
|
case `like`:
|
|
@@ -708,6 +728,91 @@ function compileSqlExpression(expression, context = `predicate`) {
|
|
|
708
728
|
};
|
|
709
729
|
}
|
|
710
730
|
}
|
|
731
|
+
function compileSafeSqlPrefilter(expression, compiled) {
|
|
732
|
+
const direct = () => {
|
|
733
|
+
const candidate = compiled ?? compileSqlExpression(expression);
|
|
734
|
+
return candidate.supported ? candidate : void 0;
|
|
735
|
+
};
|
|
736
|
+
if (expression.type === `val`) {
|
|
737
|
+
return typeof expression.value === `boolean` || expression.value == null ? direct() : void 0;
|
|
738
|
+
}
|
|
739
|
+
if (expression.type === `ref`) return void 0;
|
|
740
|
+
if (expression.name === `and` || expression.name === `or`) {
|
|
741
|
+
if (expression.args.length === 0) return direct();
|
|
742
|
+
const candidates = expression.args.map(
|
|
743
|
+
(argument) => compileSafeSqlPrefilter(argument)
|
|
744
|
+
);
|
|
745
|
+
if (expression.name === `or` && candidates.some((candidate) => !candidate))
|
|
746
|
+
return void 0;
|
|
747
|
+
const selected = candidates.filter(
|
|
748
|
+
(candidate) => candidate !== void 0
|
|
749
|
+
);
|
|
750
|
+
if (selected.length === 0) return void 0;
|
|
751
|
+
return {
|
|
752
|
+
supported: true,
|
|
753
|
+
sql: selected.map(({ sql }) => `(${sql})`).join(expression.name === `and` ? ` AND ` : ` OR `),
|
|
754
|
+
params: selected.flatMap(({ params }) => params)
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
if (hasDeepNumericPath(expression)) return void 0;
|
|
758
|
+
compiled ??= compileSqlExpression(expression);
|
|
759
|
+
if (!compiled.supported) return void 0;
|
|
760
|
+
const [left, right] = expression.args;
|
|
761
|
+
if (expression.args.length === 2) {
|
|
762
|
+
if ([`gt`, `gte`, `lt`, `lte`].includes(expression.name) && (left?.type === `ref` || right?.type === `ref`)) {
|
|
763
|
+
const field = left?.type === `ref` ? left : right?.type === `ref` ? right : void 0;
|
|
764
|
+
const literal = field === left ? right : left;
|
|
765
|
+
if (field && literal?.type === `val` && (typeof literal.value === `number` && Number.isSafeInteger(literal.value) || typeof literal.value === `bigint`)) {
|
|
766
|
+
const fieldSql = compileSqlExpression(field, `index-expression`).sql;
|
|
767
|
+
const literalSql = compileSqlExpression(
|
|
768
|
+
literal,
|
|
769
|
+
typeof literal.value === `bigint` ? `index-expression` : `predicate`
|
|
770
|
+
);
|
|
771
|
+
const greaterSide = field === left && [`gt`, `gte`].includes(expression.name) || field === right && [`lt`, `lte`].includes(expression.name);
|
|
772
|
+
const unsafeBigInt = typeof literal.value === `bigint` && !Number.isSafeInteger(Number(literal.value));
|
|
773
|
+
const roundedNumberCandidates = unsafeBigInt ? literal.value > 0n ? ` OR ${fieldSql} >= ${Number.MAX_SAFE_INTEGER}` : ` OR ${fieldSql} <= -${Number.MAX_SAFE_INTEGER}` : ``;
|
|
774
|
+
return {
|
|
775
|
+
supported: true,
|
|
776
|
+
sql: greaterSide ? `(${fieldSql} >= ${literalSql.sql} OR ${fieldSql} IS NULL${roundedNumberCandidates})` : `(${fieldSql} <= ${literalSql.sql} OR ${fieldSql} IS NULL OR ${fieldSql} >= ''${roundedNumberCandidates})`,
|
|
777
|
+
params: literalSql.params
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (expression.name === `eq`) {
|
|
782
|
+
if ((left?.type === `ref` || isSafeCoalesceOperand(left)) && right?.type === `val` && isSafeEqualityLiteral(right.value) || (right?.type === `ref` || isSafeCoalesceOperand(right)) && left?.type === `val` && isSafeEqualityLiteral(left.value)) {
|
|
783
|
+
return compiled;
|
|
784
|
+
}
|
|
785
|
+
const lower = left?.type === `func` && left.name === `lower` ? left : right?.type === `func` && right.name === `lower` ? right : void 0;
|
|
786
|
+
const literal = lower === left ? right : left;
|
|
787
|
+
if (lower?.args.length === 1 && lower.args[0]?.type === `ref` && literal?.type === `val` && typeof literal.value === `string` && [...literal.value].every((char) => char.charCodeAt(0) <= 127) && !literal.value.includes(`\0`)) {
|
|
788
|
+
const lowerSql = compileSqlExpression(lower, `index-expression`).sql;
|
|
789
|
+
return {
|
|
790
|
+
supported: true,
|
|
791
|
+
sql: `(${compiled.sql} OR (${lowerSql} >= ? AND ${lowerSql} GLOB '*[^ -~]*'))`,
|
|
792
|
+
params: [...compiled.params, literal.value]
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (expression.name === `in` && expression.args.length === 2 && right?.type === `val` && Array.isArray(right.value)) {
|
|
798
|
+
if (right.value.length === 0) return compiled;
|
|
799
|
+
if (left?.type === `ref`) {
|
|
800
|
+
if (right.value.every((value) => typeof value === `bigint`))
|
|
801
|
+
return compiled;
|
|
802
|
+
if (right.value.every((value) => typeof value === `string`)) {
|
|
803
|
+
return {
|
|
804
|
+
supported: true,
|
|
805
|
+
sql: `(${compileSqlExpression(left, `index-expression`).sql} IN (SELECT value FROM json_each(?)))`,
|
|
806
|
+
params: [JSON.stringify(right.value)]
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (expression.name === `like` && expression.args.length === 2 && left?.type === `ref` && right?.type === `val` && typeof right.value === `string` && /^[\x20-\x7e]*%$/.test(right.value) && !right.value.slice(0, -1).includes(`%`) && !right.value.includes(`_`)) {
|
|
812
|
+
return compiled;
|
|
813
|
+
}
|
|
814
|
+
return void 0;
|
|
815
|
+
}
|
|
711
816
|
function compileOrderByClauses(orderBy) {
|
|
712
817
|
if (!orderBy || orderBy.length === 0) {
|
|
713
818
|
return {
|
|
@@ -797,6 +902,23 @@ function mergeObjectRows(existing, incoming) {
|
|
|
797
902
|
}
|
|
798
903
|
return incoming;
|
|
799
904
|
}
|
|
905
|
+
function* batches(changes, maxSize) {
|
|
906
|
+
for (let offset = 0; offset < changes.length; offset += maxSize) {
|
|
907
|
+
yield changes.slice(offset, offset + maxSize);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function lastMetadataByKey(changes, keyOf, undefinedSetIsNull = false) {
|
|
911
|
+
const latest = /* @__PURE__ */ new Map();
|
|
912
|
+
for (const change of changes) {
|
|
913
|
+
const key = keyOf(change);
|
|
914
|
+
const previous = latest.get(key);
|
|
915
|
+
if (previous?.type === `set` && (previous.value !== void 0 || !undefinedSetIsNull) && serializePersistedRowValue(previous.value) === void 0) {
|
|
916
|
+
throw new TypeError(`Metadata value cannot be bound to SQLite`);
|
|
917
|
+
}
|
|
918
|
+
latest.set(key, change);
|
|
919
|
+
}
|
|
920
|
+
return [...latest.values()];
|
|
921
|
+
}
|
|
800
922
|
function buildIndexName(collectionId, signature) {
|
|
801
923
|
const sanitizedSignature = signature.toLowerCase().replace(/[^a-z0-9]+/g, `_`).replace(/^_+|_+$/g, ``).slice(0, 24);
|
|
802
924
|
const hashSource = `${collectionId}:${signature}`;
|
|
@@ -1116,158 +1238,172 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1116
1238
|
await transactionDriver.run(`DELETE FROM ${collectionTableSql}`);
|
|
1117
1239
|
await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`);
|
|
1118
1240
|
}
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
);
|
|
1241
|
+
const rowMutations = /* @__PURE__ */ new Map();
|
|
1242
|
+
for (const mutation of tx.mutations) {
|
|
1243
|
+
const key = encodePersistedStorageKey(mutation.key);
|
|
1244
|
+
const previous = rowMutations.get(key);
|
|
1245
|
+
if (!previous) {
|
|
1246
|
+
rowMutations.set(key, mutation);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
serializePersistedRowValue(previous.value);
|
|
1250
|
+
if (previous.type !== `delete` && previous.metadataChanged === true && previous.metadata !== void 0) {
|
|
1251
|
+
serializePersistedRowValue(previous.metadata);
|
|
1130
1252
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1253
|
+
if (mutation.type === `delete`) {
|
|
1254
|
+
rowMutations.set(key, mutation);
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const value = mutation.type === `insert` || previous.type === `delete` ? mutation.value : mergeObjectRows(previous.value, mutation.value);
|
|
1258
|
+
const metadataChanged = mutation.metadataChanged === true || previous.type === `delete` || previous.metadataChanged === true;
|
|
1259
|
+
const metadata = mutation.metadataChanged === true ? mutation.metadata : previous.type === `delete` ? void 0 : previous.metadata;
|
|
1260
|
+
const next = { key: mutation.key, value, metadataChanged, metadata };
|
|
1261
|
+
rowMutations.set(
|
|
1262
|
+
key,
|
|
1263
|
+
mutation.type === `insert` || previous.type !== `update` ? { type: `insert`, ...next } : { type: `update`, ...next }
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
for (const batch of batches(
|
|
1267
|
+
[...rowMutations.values()],
|
|
1268
|
+
this.replacementBatchSize
|
|
1269
|
+
)) {
|
|
1270
|
+
const writes = batch.filter((mutation) => mutation.type !== `delete`);
|
|
1271
|
+
const deletes = batch.filter((mutation) => mutation.type === `delete`);
|
|
1272
|
+
const readKeys = writes.filter(
|
|
1273
|
+
(mutation) => mutation.type === `update` || mutation.metadataChanged !== true
|
|
1274
|
+
).map((mutation) => encodePersistedStorageKey(mutation.key));
|
|
1275
|
+
const existingRows = readKeys.length > 0 ? await transactionDriver.query(
|
|
1276
|
+
`SELECT key, value, metadata
|
|
1277
|
+
FROM ${collectionTableSql}
|
|
1278
|
+
WHERE key IN (${readKeys.map(() => `?`).join(`, `)})`,
|
|
1279
|
+
readKeys
|
|
1280
|
+
) : [];
|
|
1281
|
+
const existing = new Map(existingRows.map((row) => [row.key, row]));
|
|
1282
|
+
const writeKeys = writes.map(
|
|
1283
|
+
(mutation) => encodePersistedStorageKey(mutation.key)
|
|
1284
|
+
);
|
|
1285
|
+
const deleteKeys = deletes.map(
|
|
1286
|
+
(mutation) => encodePersistedStorageKey(mutation.key)
|
|
1287
|
+
);
|
|
1288
|
+
if (tracksPersistedKeySet && writeKeys.length > 0) {
|
|
1289
|
+
await transactionDriver.run(
|
|
1290
|
+
`INSERT INTO collection_expected_keys (collection_id, key)
|
|
1291
|
+
VALUES ${writeKeys.map(() => `(?, ?)`).join(`, `)}
|
|
1292
|
+
ON CONFLICT(collection_id, key) DO NOTHING`,
|
|
1293
|
+
writeKeys.flatMap((key) => [collectionId, key])
|
|
1135
1294
|
);
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1295
|
+
}
|
|
1296
|
+
if (tracksPersistedKeySet && deleteKeys.length > 0) {
|
|
1297
|
+
await transactionDriver.run(
|
|
1298
|
+
`DELETE FROM collection_expected_keys
|
|
1299
|
+
WHERE collection_id = ? AND key IN (${deleteKeys.map(() => `?`).join(`, `)})`,
|
|
1300
|
+
[collectionId, ...deleteKeys]
|
|
1139
1301
|
);
|
|
1302
|
+
}
|
|
1303
|
+
if (deleteKeys.length > 0) {
|
|
1140
1304
|
await transactionDriver.run(
|
|
1141
|
-
`
|
|
1142
|
-
|
|
1143
|
-
|
|
1305
|
+
`DELETE FROM ${collectionTableSql}
|
|
1306
|
+
WHERE key IN (${deleteKeys.map(() => `?`).join(`, `)})`,
|
|
1307
|
+
deleteKeys
|
|
1144
1308
|
);
|
|
1309
|
+
}
|
|
1310
|
+
if (writeKeys.length > 0) {
|
|
1145
1311
|
await transactionDriver.run(
|
|
1146
1312
|
`INSERT INTO ${collectionTableSql} (key, value, metadata, row_version)
|
|
1147
|
-
VALUES ${
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1313
|
+
VALUES ${writeKeys.map(() => `(?, ?, ?, ?)`).join(`, `)}
|
|
1314
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
1315
|
+
value = excluded.value,
|
|
1316
|
+
metadata = excluded.metadata,
|
|
1317
|
+
row_version = excluded.row_version`,
|
|
1318
|
+
writes.flatMap((mutation, index) => {
|
|
1319
|
+
const previous = existing.get(writeKeys[index]);
|
|
1320
|
+
const previousValue = previous?.value ? deserializePersistedRowValue(previous.value) : void 0;
|
|
1321
|
+
const previousMetadata = previous?.metadata != null ? deserializePersistedRowValue(previous.metadata) : void 0;
|
|
1322
|
+
const value = mutation.type === `update` ? mergeObjectRows(previousValue, mutation.value) : mutation.value;
|
|
1323
|
+
const metadata = mutation.metadataChanged === true ? mutation.metadata : previousMetadata;
|
|
1151
1324
|
return [
|
|
1152
|
-
|
|
1153
|
-
serializePersistedRowValue(
|
|
1325
|
+
writeKeys[index],
|
|
1326
|
+
serializePersistedRowValue(value),
|
|
1154
1327
|
metadata === void 0 ? null : serializePersistedRowValue(metadata),
|
|
1155
1328
|
nextRowVersion
|
|
1156
1329
|
];
|
|
1157
1330
|
})
|
|
1158
1331
|
);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
for (const mutation of batchReplacement ? [] : tx.mutations) {
|
|
1162
|
-
const encodedKey = encodePersistedStorageKey(mutation.key);
|
|
1163
|
-
if (mutation.type === `delete`) {
|
|
1164
|
-
if (tracksPersistedKeySet) {
|
|
1165
|
-
await transactionDriver.run(
|
|
1166
|
-
`DELETE FROM collection_expected_keys
|
|
1167
|
-
WHERE collection_id = ? AND key = ?`,
|
|
1168
|
-
[collectionId, encodedKey]
|
|
1169
|
-
);
|
|
1170
|
-
}
|
|
1171
1332
|
await transactionDriver.run(
|
|
1172
|
-
`DELETE FROM ${
|
|
1173
|
-
WHERE key
|
|
1174
|
-
|
|
1333
|
+
`DELETE FROM ${tombstoneTableSql}
|
|
1334
|
+
WHERE key IN (${writeKeys.map(() => `?`).join(`, `)})`,
|
|
1335
|
+
writeKeys
|
|
1175
1336
|
);
|
|
1337
|
+
}
|
|
1338
|
+
if (deletes.length > 0) {
|
|
1176
1339
|
await transactionDriver.run(
|
|
1177
1340
|
`INSERT INTO ${tombstoneTableSql} (key, value, row_version, deleted_at)
|
|
1178
|
-
VALUES (?, ?, ?, ?)
|
|
1341
|
+
VALUES ${deleteKeys.map(() => `(?, ?, ?, ?)`).join(`, `)}
|
|
1179
1342
|
ON CONFLICT(key) DO UPDATE SET
|
|
1180
1343
|
value = excluded.value,
|
|
1181
1344
|
row_version = excluded.row_version,
|
|
1182
1345
|
deleted_at = excluded.deleted_at`,
|
|
1183
|
-
[
|
|
1184
|
-
|
|
1346
|
+
deletes.flatMap((mutation, index) => [
|
|
1347
|
+
deleteKeys[index],
|
|
1185
1348
|
serializePersistedRowValue(mutation.value),
|
|
1186
1349
|
nextRowVersion,
|
|
1187
1350
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
1188
|
-
]
|
|
1351
|
+
])
|
|
1189
1352
|
);
|
|
1190
|
-
continue;
|
|
1191
1353
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1354
|
+
}
|
|
1355
|
+
for (const batch of batches(
|
|
1356
|
+
lastMetadataByKey(
|
|
1357
|
+
tx.rowMetadataMutations ?? [],
|
|
1358
|
+
(mutation) => encodePersistedStorageKey(mutation.key),
|
|
1359
|
+
true
|
|
1360
|
+
),
|
|
1361
|
+
this.replacementBatchSize
|
|
1362
|
+
)) {
|
|
1363
|
+
const keys = batch.map(
|
|
1364
|
+
(mutation) => encodePersistedStorageKey(mutation.key)
|
|
1198
1365
|
);
|
|
1199
|
-
const existingValue = existingRows[0]?.value ? deserializePersistedRowValue(existingRows[0].value) : void 0;
|
|
1200
|
-
const existingMetadata = existingRows[0]?.metadata != null ? deserializePersistedRowValue(existingRows[0].metadata) : void 0;
|
|
1201
|
-
const mergedValue = mutation.type === `update` ? mergeObjectRows(existingValue, mutation.value) : mutation.value;
|
|
1202
|
-
const nextMetadata = mutation.metadataChanged === true ? mutation.metadata : existingMetadata;
|
|
1203
|
-
if (tracksPersistedKeySet) {
|
|
1204
|
-
await transactionDriver.run(
|
|
1205
|
-
`INSERT INTO collection_expected_keys (collection_id, key)
|
|
1206
|
-
VALUES (?, ?)
|
|
1207
|
-
ON CONFLICT(collection_id, key) DO NOTHING`,
|
|
1208
|
-
[collectionId, encodedKey]
|
|
1209
|
-
);
|
|
1210
|
-
}
|
|
1211
1366
|
await transactionDriver.run(
|
|
1212
|
-
`
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
value = excluded.value,
|
|
1216
|
-
metadata = excluded.metadata,
|
|
1217
|
-
row_version = excluded.row_version`,
|
|
1367
|
+
`UPDATE ${collectionTableSql}
|
|
1368
|
+
SET metadata = CASE key ${keys.map(() => `WHEN ? THEN ?`).join(` `)} END
|
|
1369
|
+
WHERE key IN (${keys.map(() => `?`).join(`, `)})`,
|
|
1218
1370
|
[
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1371
|
+
...batch.flatMap((mutation, index) => [
|
|
1372
|
+
keys[index],
|
|
1373
|
+
mutation.type === `delete` || mutation.value === void 0 ? null : serializePersistedRowValue(mutation.value)
|
|
1374
|
+
]),
|
|
1375
|
+
...keys
|
|
1223
1376
|
]
|
|
1224
1377
|
);
|
|
1225
|
-
await transactionDriver.run(
|
|
1226
|
-
`DELETE FROM ${tombstoneTableSql}
|
|
1227
|
-
WHERE key = ?`,
|
|
1228
|
-
[encodedKey]
|
|
1229
|
-
);
|
|
1230
1378
|
}
|
|
1231
|
-
for (const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
await transactionDriver.run(
|
|
1242
|
-
`UPDATE ${collectionTableSql}
|
|
1243
|
-
SET metadata = ?
|
|
1244
|
-
WHERE key = ?`,
|
|
1245
|
-
[
|
|
1246
|
-
rowMetadataMutation.value === void 0 ? null : serializePersistedRowValue(rowMetadataMutation.value),
|
|
1247
|
-
encodedKey
|
|
1248
|
-
]
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
for (const metadataMutation of tx.collectionMetadataMutations ?? []) {
|
|
1253
|
-
if (metadataMutation.type === `delete`) {
|
|
1379
|
+
for (const batch of batches(
|
|
1380
|
+
lastMetadataByKey(
|
|
1381
|
+
tx.collectionMetadataMutations ?? [],
|
|
1382
|
+
(mutation) => mutation.key
|
|
1383
|
+
),
|
|
1384
|
+
this.replacementBatchSize
|
|
1385
|
+
)) {
|
|
1386
|
+
const deletes = batch.filter((mutation) => mutation.type === `delete`);
|
|
1387
|
+
const sets = batch.filter((mutation) => mutation.type === `set`);
|
|
1388
|
+
if (deletes.length > 0) {
|
|
1254
1389
|
await transactionDriver.run(
|
|
1255
1390
|
`DELETE FROM collection_metadata
|
|
1256
|
-
WHERE collection_id = ? AND key
|
|
1257
|
-
[collectionId,
|
|
1391
|
+
WHERE collection_id = ? AND key IN (${deletes.map(() => `?`).join(`, `)})`,
|
|
1392
|
+
[collectionId, ...deletes.map((mutation) => mutation.key)]
|
|
1258
1393
|
);
|
|
1259
|
-
}
|
|
1394
|
+
}
|
|
1395
|
+
if (sets.length > 0) {
|
|
1260
1396
|
await transactionDriver.run(
|
|
1261
1397
|
`INSERT INTO collection_metadata (collection_id, key, value, updated_at)
|
|
1262
|
-
VALUES (?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
|
|
1398
|
+
VALUES ${sets.map(() => `(?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))`).join(`, `)}
|
|
1263
1399
|
ON CONFLICT(collection_id, key) DO UPDATE SET
|
|
1264
1400
|
value = excluded.value,
|
|
1265
1401
|
updated_at = excluded.updated_at`,
|
|
1266
|
-
[
|
|
1402
|
+
sets.flatMap((mutation) => [
|
|
1267
1403
|
collectionId,
|
|
1268
|
-
|
|
1269
|
-
serializePersistedRowValue(
|
|
1270
|
-
]
|
|
1404
|
+
mutation.key,
|
|
1405
|
+
serializePersistedRowValue(mutation.value)
|
|
1406
|
+
])
|
|
1271
1407
|
);
|
|
1272
1408
|
}
|
|
1273
1409
|
}
|
|
@@ -1680,18 +1816,23 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1680
1816
|
}
|
|
1681
1817
|
async loadSubsetInternal(tableMapping, options, driver = this.driver) {
|
|
1682
1818
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName);
|
|
1683
|
-
const whereCompiled = options.where ? compileSqlExpression(options.where) : { supported:
|
|
1819
|
+
const whereCompiled = options.where ? compileSqlExpression(options.where) : { supported: false, sql: ``, params: [] };
|
|
1820
|
+
const safePrefilter = options.where ? compileSafeSqlPrefilter(options.where, whereCompiled) : void 0;
|
|
1684
1821
|
const orderByCompiled = compileOrderByClauses(options.orderBy);
|
|
1685
1822
|
const queryParams = [];
|
|
1686
1823
|
let sql = `SELECT key, value, metadata, row_version FROM ${collectionTableSql}`;
|
|
1687
|
-
if (
|
|
1688
|
-
sql = `${sql} WHERE ${
|
|
1689
|
-
queryParams.push(...
|
|
1824
|
+
if (safePrefilter) {
|
|
1825
|
+
sql = `${sql} WHERE ${safePrefilter.sql}`;
|
|
1826
|
+
queryParams.push(...safePrefilter.params);
|
|
1690
1827
|
}
|
|
1691
1828
|
if (options.orderBy && orderByCompiled.supported) {
|
|
1692
1829
|
sql = `${sql} ORDER BY ${orderByCompiled.sql}, key ASC`;
|
|
1693
1830
|
queryParams.push(...orderByCompiled.params);
|
|
1694
1831
|
}
|
|
1832
|
+
if (queryParams.length > (driver.maxBoundParameters ?? this.driver.maxBoundParameters ?? 999)) {
|
|
1833
|
+
sql = `SELECT key, value, metadata, row_version FROM ${collectionTableSql}`;
|
|
1834
|
+
queryParams.length = 0;
|
|
1835
|
+
}
|
|
1695
1836
|
const storedRows = await driver.query(sql, queryParams);
|
|
1696
1837
|
const parsedRows = decodeStoredSqliteRows(storedRows);
|
|
1697
1838
|
const filteredRows = this.applyInMemoryWhere(parsedRows, options.where);
|