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