@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.
@@ -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, assertSQLiteBigIntInRange } from "./sqlite-value.js";
5
- const REPLACEMENT_BATCH_SIZE = 100;
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 jsonPath = createJsonPath(
543
+ const jsonPaths = createJsonPathVariants(
501
544
  IR.getPropRefPropertyPath(expression).map(String)
502
545
  );
503
- if (!jsonPath) {
546
+ if (!jsonPaths) {
504
547
  return {
505
548
  supported: false,
506
549
  sql: ``,
507
550
  params: []
508
551
  };
509
552
  }
510
- return compileRefExpressionSql(jsonPath);
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 < 2) {
576
- return { supported: false, sql: ``, params: [] };
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
- if (listValue.length > SQLITE_MAX_IN_BATCH_SIZE) {
618
- const chunkClauses = [];
619
- const batchedParams = [];
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 (${listValueSql.join(`, `)}))`,
647
- params: [...leftParams, ...listParams]
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 replacementKeys = replacesPersistedBaseline ? tx.mutations.map(
1120
- (mutation) => encodePersistedStorageKey(mutation.key)
1121
- ) : [];
1122
- const batchReplacement = replacesPersistedBaseline && tx.mutations.length > 0 && tx.mutations.every((mutation) => mutation.type !== `delete`) && new Set(replacementKeys).size === replacementKeys.length;
1123
- if (batchReplacement) {
1124
- const finalRowMetadata = /* @__PURE__ */ new Map();
1125
- for (const mutation of tx.rowMetadataMutations ?? []) {
1126
- finalRowMetadata.set(
1127
- encodePersistedStorageKey(mutation.key),
1128
- mutation.type === `delete` ? void 0 : mutation.value
1129
- );
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
- for (let start = 0; start < tx.mutations.length; start += this.replacementBatchSize) {
1132
- const mutations = tx.mutations.slice(
1133
- start,
1134
- start + this.replacementBatchSize
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
- const keys = replacementKeys.slice(
1137
- start,
1138
- start + this.replacementBatchSize
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
- `INSERT INTO collection_expected_keys (collection_id, key)
1142
- VALUES ${keys.map(() => `(?, ?)`).join(`, `)}`,
1143
- keys.flatMap((key) => [collectionId, key])
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 ${keys.map(() => `(?, ?, ?, ?)`).join(`, `)}`,
1148
- mutations.flatMap((mutation, index) => {
1149
- const key = keys[index];
1150
- const metadata = finalRowMetadata.has(key) ? finalRowMetadata.get(key) : mutation.type !== `delete` && mutation.metadataChanged === true ? mutation.metadata : void 0;
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
- key,
1153
- serializePersistedRowValue(mutation.value),
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 ${collectionTableSql}
1173
- WHERE key = ?`,
1174
- [encodedKey]
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
- encodedKey,
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
- const existingRows = await transactionDriver.query(
1193
- `SELECT value, metadata
1194
- FROM ${collectionTableSql}
1195
- WHERE key = ?
1196
- LIMIT 1`,
1197
- [encodedKey]
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
- `INSERT INTO ${collectionTableSql} (key, value, metadata, row_version)
1213
- VALUES (?, ?, ?, ?)
1214
- ON CONFLICT(key) DO UPDATE SET
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
- encodedKey,
1220
- serializePersistedRowValue(mergedValue),
1221
- nextMetadata === void 0 ? null : serializePersistedRowValue(nextMetadata),
1222
- nextRowVersion
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 rowMetadataMutation of batchReplacement ? [] : tx.rowMetadataMutations ?? []) {
1232
- const encodedKey = encodePersistedStorageKey(rowMetadataMutation.key);
1233
- if (rowMetadataMutation.type === `delete`) {
1234
- await transactionDriver.run(
1235
- `UPDATE ${collectionTableSql}
1236
- SET metadata = NULL
1237
- WHERE key = ?`,
1238
- [encodedKey]
1239
- );
1240
- } else {
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, metadataMutation.key]
1391
+ WHERE collection_id = ? AND key IN (${deletes.map(() => `?`).join(`, `)})`,
1392
+ [collectionId, ...deletes.map((mutation) => mutation.key)]
1258
1393
  );
1259
- } else {
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
- metadataMutation.key,
1269
- serializePersistedRowValue(metadataMutation.value)
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: true, sql: ``, params: [] };
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 (options.where && whereCompiled.supported) {
1688
- sql = `${sql} WHERE ${whereCompiled.sql}`;
1689
- queryParams.push(...whereCompiled.params);
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);