@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.
@@ -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([
@@ -117,7 +117,10 @@ const persistedTaggedValueTypes = /* @__PURE__ */ new Set([
117
117
  `date`,
118
118
  `nan`,
119
119
  `infinity`,
120
- `-infinity`
120
+ `-infinity`,
121
+ `string`,
122
+ `Temporal.Instant`,
123
+ `Temporal.PlainDate`
121
124
  ]);
122
125
  const orderByObjectIds = /* @__PURE__ */ new WeakMap();
123
126
  let nextOrderByObjectId = 1;
@@ -155,6 +158,8 @@ function encodePersistedJsonValue(value) {
155
158
  if (typeof value === `bigint`) {
156
159
  return sqliteValue.serializeSQLiteBigInt(value);
157
160
  }
161
+ const temporal = sqliteValue.serializeSQLiteTemporal(value);
162
+ if (temporal) return temporal;
158
163
  if (value instanceof Date) {
159
164
  return {
160
165
  [sqliteValue.PERSISTED_TYPE_TAG]: `date`,
@@ -194,6 +199,12 @@ function encodePersistedJsonValue(value) {
194
199
  encodedRecord[key] = encodedValue;
195
200
  }
196
201
  }
202
+ if (typeof recordValue[sqliteValue.PERSISTED_TYPE_TAG] === `string`) {
203
+ encodedRecord[sqliteValue.PERSISTED_TYPE_TAG] = {
204
+ [sqliteValue.PERSISTED_TYPE_TAG]: `string`,
205
+ [sqliteValue.PERSISTED_VALUE_TAG]: recordValue[sqliteValue.PERSISTED_TYPE_TAG]
206
+ };
207
+ }
197
208
  return encodedRecord;
198
209
  }
199
210
  return value;
@@ -204,6 +215,14 @@ function decodePersistedJsonValue(value) {
204
215
  }
205
216
  if (isPersistedTaggedValue(value)) {
206
217
  switch (value[sqliteValue.PERSISTED_TYPE_TAG]) {
218
+ case `string`:
219
+ return value[sqliteValue.PERSISTED_VALUE_TAG];
220
+ case `Temporal.Instant`:
221
+ case `Temporal.PlainDate`:
222
+ return sqliteValue.reviveSQLiteTemporal(
223
+ value[sqliteValue.PERSISTED_TYPE_TAG],
224
+ value[sqliteValue.PERSISTED_VALUE_TAG]
225
+ );
207
226
  case `bigint`:
208
227
  return BigInt(value[sqliteValue.PERSISTED_VALUE_TAG]);
209
228
  case `date`: {
@@ -263,7 +282,8 @@ function toSqliteParameterValue(value) {
263
282
  if (value instanceof Date) {
264
283
  return value.toISOString();
265
284
  }
266
- return serializePersistedRowValue(value);
285
+ const temporal = sqliteValue.serializeSQLiteTemporal(value);
286
+ return temporal?.order ?? serializePersistedRowValue(value);
267
287
  }
268
288
  function toSqliteLiteral(value) {
269
289
  if (value === null) {
@@ -272,6 +292,9 @@ function toSqliteLiteral(value) {
272
292
  if (typeof value === `number`) {
273
293
  return Number.isFinite(value) ? String(value) : `NULL`;
274
294
  }
295
+ if (value.includes(`\0`)) {
296
+ return `json_extract(${toSqliteLiteral(JSON.stringify(value))}, '$')`;
297
+ }
275
298
  return `'${value.replace(/'/g, `''`)}'`;
276
299
  }
277
300
  function toSqliteExpressionLiteral(value) {
@@ -347,6 +370,9 @@ function compareOrderByValues(left, right, compareOptions) {
347
370
  if (left instanceof Date && right instanceof Date) {
348
371
  return left.getTime() - right.getTime();
349
372
  }
373
+ if (sqliteValue.sqliteTemporalKind(left) && sqliteValue.sqliteTemporalKind(right)) {
374
+ return db.compareTemporalValues(left, right);
375
+ }
350
376
  const leftIsObject = typeof left === `object`;
351
377
  const rightIsObject = typeof right === `object`;
352
378
  if (leftIsObject || rightIsObject) {
@@ -381,6 +407,33 @@ function createJsonPath(path) {
381
407
  }
382
408
  return jsonPath;
383
409
  }
410
+ const MAX_INDEXED_NUMERIC_PATH_SEGMENTS = 3;
411
+ function hasDeepNumericPath(expression) {
412
+ if (expression.type === `ref`) {
413
+ return db.IR.getPropRefPropertyPath(expression).filter(
414
+ (segment) => /^[0-9]+$/.test(String(segment))
415
+ ).length > MAX_INDEXED_NUMERIC_PATH_SEGMENTS;
416
+ }
417
+ return expression.type === `func` && expression.args.some(hasDeepNumericPath);
418
+ }
419
+ function createJsonPathVariants(path) {
420
+ const canonical = createJsonPath(path);
421
+ if (!canonical) return null;
422
+ let paths = [`$`];
423
+ for (const segment of path) {
424
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)) {
425
+ paths = paths.map((prefix) => `${prefix}.${segment}`);
426
+ } else if (/^[0-9]+$/.test(segment)) {
427
+ if (paths.length >= 2 ** MAX_INDEXED_NUMERIC_PATH_SEGMENTS)
428
+ return [canonical];
429
+ paths = paths.flatMap((prefix) => [
430
+ `${prefix}[${segment}]`,
431
+ `${prefix}."${segment}"`
432
+ ]);
433
+ }
434
+ }
435
+ return paths;
436
+ }
384
437
  function getLiteralValueKind(value) {
385
438
  if (typeof value === `bigint`) {
386
439
  return `bigint`;
@@ -426,16 +479,23 @@ function compileRefExpressionSql(jsonPath) {
426
479
  const typePathSql = toSqliteLiteral(typePath);
427
480
  const taggedValuePathSql = toSqliteLiteral(taggedValuePath);
428
481
  const jsonPathSql = toSqliteLiteral(jsonPath);
429
- return {
430
- supported: true,
431
- sql: `(CASE json_extract(value, ${typePathSql})
482
+ const typeSql = `json_extract(value, ${typePathSql})`;
483
+ const orderPathSql = toSqliteLiteral(`${jsonPath}.order`);
484
+ const sql = `(CASE ${typeSql}
432
485
  WHEN 'bigint' THEN CAST(json_extract(value, ${taggedValuePathSql}) AS NUMERIC)
433
486
  WHEN 'date' THEN json_extract(value, ${taggedValuePathSql})
487
+ WHEN 'string' THEN json_extract(value, ${taggedValuePathSql})
488
+ WHEN 'Temporal.Instant' THEN json_extract(value, ${orderPathSql})
489
+ WHEN 'Temporal.PlainDate' THEN json_extract(value, ${orderPathSql})
434
490
  WHEN 'nan' THEN NULL
435
491
  WHEN 'infinity' THEN NULL
436
492
  WHEN '-infinity' THEN NULL
437
493
  ELSE json_extract(value, ${jsonPathSql})
438
- END)`,
494
+ END)`;
495
+ return {
496
+ supported: true,
497
+ sql,
498
+ identitySql: `(CASE WHEN ${typeSql} IN ('Temporal.Instant', 'Temporal.PlainDate') THEN ${typeSql} || ':' || json_extract(value, ${taggedValuePathSql}) ELSE ${sql} END)`,
439
499
  params: [],
440
500
  valueKind: `unknown`
441
501
  };
@@ -463,6 +523,11 @@ function decodeStoredSqliteRows(storedRows) {
463
523
  function stableStringify(value) {
464
524
  return serializePersistedRowValue(value);
465
525
  }
526
+ function joinSqlDisjunction(parts) {
527
+ if (parts.length === 1) return parts[0];
528
+ const middle = Math.floor(parts.length / 2);
529
+ return `(${joinSqlDisjunction(parts.slice(0, middle))} OR ${joinSqlDisjunction(parts.slice(middle))})`;
530
+ }
466
531
  function argumentCompilationContext(parentName, argumentIndex, argument, parentContext) {
467
532
  if (parentContext === `index-expression`) return `index-expression`;
468
533
  switch (parentName) {
@@ -488,30 +553,59 @@ function argumentCompilationContext(parentName, argumentIndex, argument, parentC
488
553
  return `index-expression`;
489
554
  }
490
555
  }
556
+ function hasUnpairedSurrogate(value) {
557
+ return /[\uD800-\uDFFF]/u.test(value);
558
+ }
559
+ function isSafeEqualityLiteral(value) {
560
+ return typeof value === `string` || typeof value === `boolean` || typeof value === `bigint` || sqliteValue.sqliteTemporalKind(value) !== void 0;
561
+ }
562
+ function isSafeCoalesceOperand(expression) {
563
+ 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));
564
+ }
491
565
  function compileSqlExpression(expression, context = `predicate`) {
492
566
  if (expression.type === `val`) {
493
- const valueKind = getLiteralValueKind(expression.value);
567
+ if (context === `predicate` && typeof expression.value === `string` && hasUnpairedSurrogate(expression.value)) {
568
+ return {
569
+ supported: true,
570
+ sql: `json_extract(?, '$')`,
571
+ params: [JSON.stringify(expression.value)]
572
+ };
573
+ }
574
+ const temporal = sqliteValue.serializeSQLiteTemporal(expression.value);
575
+ const value = temporal?.order ?? toSqliteParameterValue(expression.value);
494
576
  return {
577
+ identitySql: temporal ? toSqliteLiteral(sqliteValue.sqliteTemporalIdentity(temporal)) : void 0,
495
578
  supported: true,
496
- sql: context === `index-expression` ? toSqliteExpressionLiteral(expression.value) : `?`,
497
- params: context === `predicate` ? [toSqliteParameterValue(expression.value)] : [],
498
- valueKind
579
+ sql: context === `index-expression` ? toSqliteExpressionLiteral(
580
+ typeof expression.value === `bigint` ? expression.value : value
581
+ ) : `?`,
582
+ params: context === `predicate` ? [value] : [],
583
+ valueKind: getLiteralValueKind(expression.value)
499
584
  };
500
585
  }
501
586
  if (expression.type === `ref`) {
502
- const jsonPath = createJsonPath(
587
+ const jsonPaths = createJsonPathVariants(
503
588
  db.IR.getPropRefPropertyPath(expression).map(String)
504
589
  );
505
- if (!jsonPath) {
590
+ if (!jsonPaths) {
506
591
  return {
507
592
  supported: false,
508
593
  sql: ``,
509
594
  params: []
510
595
  };
511
596
  }
512
- return compileRefExpressionSql(jsonPath);
597
+ if (jsonPaths.length === 1) return compileRefExpressionSql(jsonPaths[0]);
598
+ const variants = jsonPaths.map(compileRefExpressionSql);
599
+ return {
600
+ supported: true,
601
+ identitySql: `COALESCE(${variants.map((variant) => variant.identitySql).join(`, `)})`,
602
+ sql: `COALESCE(${variants.map((variant) => variant.sql).join(`, `)})`,
603
+ params: [],
604
+ valueKind: `unknown`
605
+ };
513
606
  }
514
- const compiledArgs = expression.args.map(
607
+ const args = expression.name === `in` ? expression.args.slice(0, 1) : expression.args;
608
+ const compiledArgs = args.map(
515
609
  (arg, index) => compileSqlExpression(
516
610
  arg,
517
611
  argumentCompilationContext(expression.name, index, arg, context)
@@ -548,38 +642,37 @@ function compileSqlExpression(expression, context = `predicate`) {
548
642
  lt: `<`,
549
643
  lte: `<=`
550
644
  };
645
+ const comparison = compileComparisonSql(
646
+ operatorByName[expression.name],
647
+ expression.args[0],
648
+ expression.args[1],
649
+ argSql[0],
650
+ argSql[1],
651
+ valueKind,
652
+ getCompiledValueKind(compiledArgs[0]),
653
+ getCompiledValueKind(compiledArgs[1])
654
+ );
655
+ const needsIdentity = expression.name === `eq` && expression.args.every(
656
+ (arg) => arg.type !== `val` || sqliteValue.sqliteTemporalKind(arg.value) || context === `index-expression` && typeof arg.value === `string`
657
+ );
551
658
  return {
552
659
  supported: true,
553
- sql: compileComparisonSql(
554
- operatorByName[expression.name],
555
- expression.args[0],
556
- expression.args[1],
557
- argSql[0],
558
- argSql[1],
559
- valueKind,
560
- getCompiledValueKind(compiledArgs[0]),
561
- getCompiledValueKind(compiledArgs[1])
562
- ),
563
- params
564
- };
565
- }
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 `),
660
+ sql: needsIdentity ? `(${comparison} AND (${compiledArgs[0].identitySql ?? argSql[0]} = ${compiledArgs[1].identitySql ?? argSql[1]}))` : comparison,
573
661
  params
574
662
  };
575
663
  }
664
+ case `and`:
576
665
  case `or`: {
577
- if (argSql.length < 2) {
578
- return { supported: false, sql: ``, params: [] };
666
+ if (argSql.length === 0) {
667
+ return {
668
+ supported: true,
669
+ sql: expression.name === `and` ? `(1 = 1)` : `(0 = 1)`,
670
+ params: []
671
+ };
579
672
  }
580
673
  return {
581
674
  supported: true,
582
- sql: argSql.map((sql) => `(${sql})`).join(` OR `),
675
+ sql: argSql.map((sql) => `(${sql})`).join(expression.name === `and` ? ` AND ` : ` OR `),
583
676
  params
584
677
  };
585
678
  }
@@ -609,44 +702,34 @@ function compileSqlExpression(expression, context = `predicate`) {
609
702
  if (!leftSql) {
610
703
  return { supported: false, sql: ``, params: [] };
611
704
  }
612
- if (context === `index-expression`) {
705
+ const values = listValue.map((value) => {
706
+ const temporal = sqliteValue.serializeSQLiteTemporal(value);
613
707
  return {
614
- supported: true,
615
- sql: `(${leftSql} IN (${listValue.map((value) => toSqliteExpressionLiteral(value)).join(`, `)}))`,
616
- params: leftParams
708
+ value: typeof value === `bigint` ? sqliteValue.assertSQLiteBigIntInRange(value) : temporal?.order ?? toSqliteParameterValue(value),
709
+ identity: temporal ? sqliteValue.sqliteTemporalIdentity(temporal) : void 0
617
710
  };
711
+ });
712
+ let identity = ``;
713
+ if (listValue.some(
714
+ (value) => sqliteValue.sqliteTemporalKind(value) || context === `index-expression` && typeof value === `string`
715
+ )) {
716
+ const leftIdentity = compiledArgs[0]?.identitySql ?? leftSql;
717
+ identity = context === `index-expression` ? ` AND (${joinSqlDisjunction(values.map(({ value, identity: nativeIdentity }) => `(${leftSql} = ${toSqliteExpressionLiteral(value)} AND ${leftIdentity} = ${toSqliteExpressionLiteral(nativeIdentity ?? value)})`))})` : ` AND ((${leftSql}, ${leftIdentity}) IN (VALUES ${[...new Set(values.map(({ value, identity: nativeIdentity }) => `(${toSqliteExpressionLiteral(value)}, ${toSqliteExpressionLiteral(nativeIdentity ?? value)})`))].join(`, `)}))`;
618
718
  }
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
- }
719
+ if (context === `index-expression`) {
635
720
  return {
636
721
  supported: true,
637
- sql: `(${chunkClauses.join(` OR `)})`,
638
- params: batchedParams
722
+ sql: `(${leftSql} IN (${values.map(({ value }) => toSqliteExpressionLiteral(value)).join(`, `)})${identity})`,
723
+ params: leftParams
639
724
  };
640
725
  }
641
- const listParams = [];
642
- const listValueSql = listValue.map((value) => {
643
- listParams.push(toSqliteParameterValue(value));
644
- return typeof value === `bigint` ? `CAST(? AS NUMERIC)` : `?`;
645
- });
726
+ const jsonList = `[${values.map(
727
+ ({ value }) => typeof value === `bigint` ? sqliteValue.assertSQLiteBigIntInRange(value).toString() : JSON.stringify(value)
728
+ ).join(`,`)}]`;
646
729
  return {
647
730
  supported: true,
648
- sql: `(${leftSql} IN (${listValueSql.join(`, `)}))`,
649
- params: [...leftParams, ...listParams]
731
+ sql: `(${leftSql} IN (SELECT value FROM json_each(?))${identity})`,
732
+ params: [...leftParams, jsonList]
650
733
  };
651
734
  }
652
735
  case `like`:
@@ -673,7 +756,12 @@ function compileSqlExpression(expression, context = `predicate`) {
673
756
  case `concat`:
674
757
  return { supported: true, sql: `(${argSql.join(` || `)})`, params };
675
758
  case `coalesce`:
676
- return { supported: true, sql: `COALESCE(${argSql.join(`, `)})`, params };
759
+ return {
760
+ supported: true,
761
+ sql: `COALESCE(${argSql.join(`, `)})`,
762
+ identitySql: `COALESCE(${compiledArgs.map((arg) => arg.identitySql ?? arg.sql).join(`, `)})`,
763
+ params
764
+ };
677
765
  case `add`:
678
766
  return { supported: true, sql: `(${argSql[0]} + ${argSql[1]})`, params };
679
767
  case `subtract`:
@@ -710,6 +798,131 @@ function compileSqlExpression(expression, context = `predicate`) {
710
798
  };
711
799
  }
712
800
  }
801
+ function nativeComparisonRef(expression) {
802
+ if (expression.type !== `func` || expression.args.length !== 2)
803
+ return void 0;
804
+ const [left, right] = expression.args;
805
+ if (expression.name === `in`) {
806
+ return left?.type === `ref` && right?.type === `val` && Array.isArray(right.value) && right.value.length > 0 && right.value.every((value) => sqliteValue.sqliteTemporalKind(value)) ? left : void 0;
807
+ }
808
+ if (![`eq`, `gt`, `gte`, `lt`, `lte`].includes(expression.name))
809
+ return void 0;
810
+ if (left?.type === `ref` && right?.type === `val` && sqliteValue.sqliteTemporalKind(right.value))
811
+ return left;
812
+ if (right?.type === `ref` && left?.type === `val` && sqliteValue.sqliteTemporalKind(left.value))
813
+ return right;
814
+ return void 0;
815
+ }
816
+ function compileNativeRefGuard(ref) {
817
+ const paths = createJsonPathVariants(
818
+ db.IR.getPropRefPropertyPath(ref).map(String)
819
+ );
820
+ const kinds = paths.map(
821
+ (path) => `json_extract(value, ${toSqliteLiteral(`${path}.${sqliteValue.PERSISTED_TYPE_TAG}`)})`
822
+ );
823
+ const kind = kinds.length === 1 ? kinds[0] : `COALESCE(${kinds.join(`, `)})`;
824
+ return `${kind} IN ('Temporal.Instant', 'Temporal.PlainDate')`;
825
+ }
826
+ function compileSafeSqlPrefilter(expression, compiled) {
827
+ const direct = () => {
828
+ const candidate = compiled ?? compileSqlExpression(expression);
829
+ return candidate.supported ? candidate : void 0;
830
+ };
831
+ if (expression.type === `val`) {
832
+ return typeof expression.value === `boolean` || expression.value == null ? direct() : void 0;
833
+ }
834
+ if (expression.type === `ref`) return void 0;
835
+ if (expression.name === `and` || expression.name === `or`) {
836
+ if (expression.args.length === 0) return direct();
837
+ const candidates = expression.args.map(
838
+ (argument) => compileSafeSqlPrefilter(argument)
839
+ );
840
+ if (expression.name === `or` && candidates.some((candidate) => !candidate))
841
+ return void 0;
842
+ const selected = candidates.filter(
843
+ (candidate) => candidate !== void 0
844
+ );
845
+ if (selected.length === 0) return void 0;
846
+ return {
847
+ supported: true,
848
+ sql: selected.map(({ sql }) => `(${sql})`).join(expression.name === `and` ? ` AND ` : ` OR `),
849
+ params: selected.flatMap(({ params }) => params)
850
+ };
851
+ }
852
+ if (hasDeepNumericPath(expression)) return void 0;
853
+ compiled ??= compileSqlExpression(expression);
854
+ if (!compiled.supported) return void 0;
855
+ const [left, right] = expression.args;
856
+ if (expression.name === `not` && expression.args.length === 1 && left && nativeComparisonRef(left)) {
857
+ return { ...compiled, sql: `COALESCE(${compiled.sql}, 1)` };
858
+ }
859
+ if (expression.args.length === 2) {
860
+ const nativeRef = nativeComparisonRef(expression);
861
+ if (nativeRef) {
862
+ if (expression.name === `eq` || expression.name === `in`) return compiled;
863
+ const fieldSql = compileSqlExpression(nativeRef, `index-expression`).sql;
864
+ return { ...compiled, sql: `(${compiled.sql} OR ${fieldSql} IS NULL)` };
865
+ }
866
+ if (expression.name === `eq` && left?.type === `ref` && right?.type === `ref`) {
867
+ return {
868
+ ...compiled,
869
+ sql: `(CASE WHEN ${compileNativeRefGuard(left)} AND ${compileNativeRefGuard(right)} THEN ${compiled.sql} ELSE 1 END)`
870
+ };
871
+ }
872
+ if ([`gt`, `gte`, `lt`, `lte`].includes(expression.name) && (left?.type === `ref` || right?.type === `ref`)) {
873
+ const field = left?.type === `ref` ? left : right?.type === `ref` ? right : void 0;
874
+ const literal = field === left ? right : left;
875
+ if (field && literal?.type === `val` && (typeof literal.value === `number` && Number.isSafeInteger(literal.value) || typeof literal.value === `bigint`)) {
876
+ const fieldSql = compileSqlExpression(field, `index-expression`).sql;
877
+ const literalSql = compileSqlExpression(
878
+ literal,
879
+ typeof literal.value === `bigint` ? `index-expression` : `predicate`
880
+ );
881
+ const greaterSide = field === left && [`gt`, `gte`].includes(expression.name) || field === right && [`lt`, `lte`].includes(expression.name);
882
+ const unsafeBigInt = typeof literal.value === `bigint` && !Number.isSafeInteger(Number(literal.value));
883
+ const roundedNumberCandidates = unsafeBigInt ? literal.value > 0n ? ` OR ${fieldSql} >= ${Number.MAX_SAFE_INTEGER}` : ` OR ${fieldSql} <= -${Number.MAX_SAFE_INTEGER}` : ``;
884
+ return {
885
+ supported: true,
886
+ sql: greaterSide ? `(${fieldSql} >= ${literalSql.sql} OR ${fieldSql} IS NULL${roundedNumberCandidates})` : `(${fieldSql} <= ${literalSql.sql} OR ${fieldSql} IS NULL OR ${fieldSql} >= ''${roundedNumberCandidates})`,
887
+ params: literalSql.params
888
+ };
889
+ }
890
+ }
891
+ if (expression.name === `eq`) {
892
+ if ((left?.type === `ref` || isSafeCoalesceOperand(left)) && right?.type === `val` && isSafeEqualityLiteral(right.value) || (right?.type === `ref` || isSafeCoalesceOperand(right)) && left?.type === `val` && isSafeEqualityLiteral(left.value)) {
893
+ return compiled;
894
+ }
895
+ const lower = left?.type === `func` && left.name === `lower` ? left : right?.type === `func` && right.name === `lower` ? right : void 0;
896
+ const literal = lower === left ? right : left;
897
+ 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`)) {
898
+ const lowerSql = compileSqlExpression(lower, `index-expression`).sql;
899
+ return {
900
+ supported: true,
901
+ sql: `(${compiled.sql} OR (${lowerSql} >= ? AND ${lowerSql} GLOB '*[^ -~]*'))`,
902
+ params: [...compiled.params, literal.value]
903
+ };
904
+ }
905
+ }
906
+ }
907
+ if (expression.name === `in` && expression.args.length === 2 && right?.type === `val` && Array.isArray(right.value)) {
908
+ if (right.value.length === 0) return compiled;
909
+ if (left?.type === `ref`) {
910
+ if (right.value.every((value) => typeof value === `bigint`))
911
+ return compiled;
912
+ if (right.value.every((value) => typeof value === `string`)) {
913
+ return {
914
+ supported: true,
915
+ sql: `(${compileSqlExpression(left, `index-expression`).sql} IN (SELECT value FROM json_each(?)))`,
916
+ params: [JSON.stringify(right.value)]
917
+ };
918
+ }
919
+ }
920
+ }
921
+ 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(`_`)) {
922
+ return compiled;
923
+ }
924
+ return void 0;
925
+ }
713
926
  function compileOrderByClauses(orderBy) {
714
927
  if (!orderBy || orderBy.length === 0) {
715
928
  return {
@@ -799,6 +1012,23 @@ function mergeObjectRows(existing, incoming) {
799
1012
  }
800
1013
  return incoming;
801
1014
  }
1015
+ function* batches(changes, maxSize) {
1016
+ for (let offset = 0; offset < changes.length; offset += maxSize) {
1017
+ yield changes.slice(offset, offset + maxSize);
1018
+ }
1019
+ }
1020
+ function lastMetadataByKey(changes, keyOf, undefinedSetIsNull = false) {
1021
+ const latest = /* @__PURE__ */ new Map();
1022
+ for (const change of changes) {
1023
+ const key = keyOf(change);
1024
+ const previous = latest.get(key);
1025
+ if (previous?.type === `set` && (previous.value !== void 0 || !undefinedSetIsNull) && serializePersistedRowValue(previous.value) === void 0) {
1026
+ throw new TypeError(`Metadata value cannot be bound to SQLite`);
1027
+ }
1028
+ latest.set(key, change);
1029
+ }
1030
+ return [...latest.values()];
1031
+ }
802
1032
  function buildIndexName(collectionId, signature) {
803
1033
  const sanitizedSignature = signature.toLowerCase().replace(/[^a-z0-9]+/g, `_`).replace(/^_+|_+$/g, ``).slice(0, 24);
804
1034
  const hashSource = `${collectionId}:${signature}`;
@@ -1118,158 +1348,172 @@ class SQLiteCorePersistenceAdapter {
1118
1348
  await transactionDriver.run(`DELETE FROM ${collectionTableSql}`);
1119
1349
  await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`);
1120
1350
  }
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
- );
1351
+ const rowMutations = /* @__PURE__ */ new Map();
1352
+ for (const mutation of tx.mutations) {
1353
+ const key = persisted.encodePersistedStorageKey(mutation.key);
1354
+ const previous = rowMutations.get(key);
1355
+ if (!previous) {
1356
+ rowMutations.set(key, mutation);
1357
+ continue;
1358
+ }
1359
+ serializePersistedRowValue(previous.value);
1360
+ if (previous.type !== `delete` && previous.metadataChanged === true && previous.metadata !== void 0) {
1361
+ serializePersistedRowValue(previous.metadata);
1362
+ }
1363
+ if (mutation.type === `delete`) {
1364
+ rowMutations.set(key, mutation);
1365
+ continue;
1132
1366
  }
1133
- for (let start = 0; start < tx.mutations.length; start += this.replacementBatchSize) {
1134
- const mutations = tx.mutations.slice(
1135
- start,
1136
- start + this.replacementBatchSize
1367
+ const value = mutation.type === `insert` || previous.type === `delete` ? mutation.value : mergeObjectRows(previous.value, mutation.value);
1368
+ const metadataChanged = mutation.metadataChanged === true || previous.type === `delete` || previous.metadataChanged === true;
1369
+ const metadata = mutation.metadataChanged === true ? mutation.metadata : previous.type === `delete` ? void 0 : previous.metadata;
1370
+ const next = { key: mutation.key, value, metadataChanged, metadata };
1371
+ rowMutations.set(
1372
+ key,
1373
+ mutation.type === `insert` || previous.type !== `update` ? { type: `insert`, ...next } : { type: `update`, ...next }
1374
+ );
1375
+ }
1376
+ for (const batch of batches(
1377
+ [...rowMutations.values()],
1378
+ this.replacementBatchSize
1379
+ )) {
1380
+ const writes = batch.filter((mutation) => mutation.type !== `delete`);
1381
+ const deletes = batch.filter((mutation) => mutation.type === `delete`);
1382
+ const readKeys = writes.filter(
1383
+ (mutation) => mutation.type === `update` || mutation.metadataChanged !== true
1384
+ ).map((mutation) => persisted.encodePersistedStorageKey(mutation.key));
1385
+ const existingRows = readKeys.length > 0 ? await transactionDriver.query(
1386
+ `SELECT key, value, metadata
1387
+ FROM ${collectionTableSql}
1388
+ WHERE key IN (${readKeys.map(() => `?`).join(`, `)})`,
1389
+ readKeys
1390
+ ) : [];
1391
+ const existing = new Map(existingRows.map((row) => [row.key, row]));
1392
+ const writeKeys = writes.map(
1393
+ (mutation) => persisted.encodePersistedStorageKey(mutation.key)
1394
+ );
1395
+ const deleteKeys = deletes.map(
1396
+ (mutation) => persisted.encodePersistedStorageKey(mutation.key)
1397
+ );
1398
+ if (tracksPersistedKeySet && writeKeys.length > 0) {
1399
+ await transactionDriver.run(
1400
+ `INSERT INTO collection_expected_keys (collection_id, key)
1401
+ VALUES ${writeKeys.map(() => `(?, ?)`).join(`, `)}
1402
+ ON CONFLICT(collection_id, key) DO NOTHING`,
1403
+ writeKeys.flatMap((key) => [collectionId, key])
1137
1404
  );
1138
- const keys = replacementKeys.slice(
1139
- start,
1140
- start + this.replacementBatchSize
1405
+ }
1406
+ if (tracksPersistedKeySet && deleteKeys.length > 0) {
1407
+ await transactionDriver.run(
1408
+ `DELETE FROM collection_expected_keys
1409
+ WHERE collection_id = ? AND key IN (${deleteKeys.map(() => `?`).join(`, `)})`,
1410
+ [collectionId, ...deleteKeys]
1141
1411
  );
1412
+ }
1413
+ if (deleteKeys.length > 0) {
1142
1414
  await transactionDriver.run(
1143
- `INSERT INTO collection_expected_keys (collection_id, key)
1144
- VALUES ${keys.map(() => `(?, ?)`).join(`, `)}`,
1145
- keys.flatMap((key) => [collectionId, key])
1415
+ `DELETE FROM ${collectionTableSql}
1416
+ WHERE key IN (${deleteKeys.map(() => `?`).join(`, `)})`,
1417
+ deleteKeys
1146
1418
  );
1419
+ }
1420
+ if (writeKeys.length > 0) {
1147
1421
  await transactionDriver.run(
1148
1422
  `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;
1423
+ VALUES ${writeKeys.map(() => `(?, ?, ?, ?)`).join(`, `)}
1424
+ ON CONFLICT(key) DO UPDATE SET
1425
+ value = excluded.value,
1426
+ metadata = excluded.metadata,
1427
+ row_version = excluded.row_version`,
1428
+ writes.flatMap((mutation, index) => {
1429
+ const previous = existing.get(writeKeys[index]);
1430
+ const previousValue = previous?.value ? deserializePersistedRowValue(previous.value) : void 0;
1431
+ const previousMetadata = previous?.metadata != null ? deserializePersistedRowValue(previous.metadata) : void 0;
1432
+ const value = mutation.type === `update` ? mergeObjectRows(previousValue, mutation.value) : mutation.value;
1433
+ const metadata = mutation.metadataChanged === true ? mutation.metadata : previousMetadata;
1153
1434
  return [
1154
- key,
1155
- serializePersistedRowValue(mutation.value),
1435
+ writeKeys[index],
1436
+ serializePersistedRowValue(value),
1156
1437
  metadata === void 0 ? null : serializePersistedRowValue(metadata),
1157
1438
  nextRowVersion
1158
1439
  ];
1159
1440
  })
1160
1441
  );
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
1442
  await transactionDriver.run(
1174
- `DELETE FROM ${collectionTableSql}
1175
- WHERE key = ?`,
1176
- [encodedKey]
1443
+ `DELETE FROM ${tombstoneTableSql}
1444
+ WHERE key IN (${writeKeys.map(() => `?`).join(`, `)})`,
1445
+ writeKeys
1177
1446
  );
1447
+ }
1448
+ if (deletes.length > 0) {
1178
1449
  await transactionDriver.run(
1179
1450
  `INSERT INTO ${tombstoneTableSql} (key, value, row_version, deleted_at)
1180
- VALUES (?, ?, ?, ?)
1451
+ VALUES ${deleteKeys.map(() => `(?, ?, ?, ?)`).join(`, `)}
1181
1452
  ON CONFLICT(key) DO UPDATE SET
1182
1453
  value = excluded.value,
1183
1454
  row_version = excluded.row_version,
1184
1455
  deleted_at = excluded.deleted_at`,
1185
- [
1186
- encodedKey,
1456
+ deletes.flatMap((mutation, index) => [
1457
+ deleteKeys[index],
1187
1458
  serializePersistedRowValue(mutation.value),
1188
1459
  nextRowVersion,
1189
1460
  (/* @__PURE__ */ new Date()).toISOString()
1190
- ]
1461
+ ])
1191
1462
  );
1192
- continue;
1193
1463
  }
1194
- const existingRows = await transactionDriver.query(
1195
- `SELECT value, metadata
1196
- FROM ${collectionTableSql}
1197
- WHERE key = ?
1198
- LIMIT 1`,
1199
- [encodedKey]
1464
+ }
1465
+ for (const batch of batches(
1466
+ lastMetadataByKey(
1467
+ tx.rowMetadataMutations ?? [],
1468
+ (mutation) => persisted.encodePersistedStorageKey(mutation.key),
1469
+ true
1470
+ ),
1471
+ this.replacementBatchSize
1472
+ )) {
1473
+ const keys = batch.map(
1474
+ (mutation) => persisted.encodePersistedStorageKey(mutation.key)
1200
1475
  );
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
1476
  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`,
1477
+ `UPDATE ${collectionTableSql}
1478
+ SET metadata = CASE key ${keys.map(() => `WHEN ? THEN ?`).join(` `)} END
1479
+ WHERE key IN (${keys.map(() => `?`).join(`, `)})`,
1220
1480
  [
1221
- encodedKey,
1222
- serializePersistedRowValue(mergedValue),
1223
- nextMetadata === void 0 ? null : serializePersistedRowValue(nextMetadata),
1224
- nextRowVersion
1481
+ ...batch.flatMap((mutation, index) => [
1482
+ keys[index],
1483
+ mutation.type === `delete` || mutation.value === void 0 ? null : serializePersistedRowValue(mutation.value)
1484
+ ]),
1485
+ ...keys
1225
1486
  ]
1226
1487
  );
1227
- await transactionDriver.run(
1228
- `DELETE FROM ${tombstoneTableSql}
1229
- WHERE key = ?`,
1230
- [encodedKey]
1231
- );
1232
1488
  }
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`) {
1489
+ for (const batch of batches(
1490
+ lastMetadataByKey(
1491
+ tx.collectionMetadataMutations ?? [],
1492
+ (mutation) => mutation.key
1493
+ ),
1494
+ this.replacementBatchSize
1495
+ )) {
1496
+ const deletes = batch.filter((mutation) => mutation.type === `delete`);
1497
+ const sets = batch.filter((mutation) => mutation.type === `set`);
1498
+ if (deletes.length > 0) {
1256
1499
  await transactionDriver.run(
1257
1500
  `DELETE FROM collection_metadata
1258
- WHERE collection_id = ? AND key = ?`,
1259
- [collectionId, metadataMutation.key]
1501
+ WHERE collection_id = ? AND key IN (${deletes.map(() => `?`).join(`, `)})`,
1502
+ [collectionId, ...deletes.map((mutation) => mutation.key)]
1260
1503
  );
1261
- } else {
1504
+ }
1505
+ if (sets.length > 0) {
1262
1506
  await transactionDriver.run(
1263
1507
  `INSERT INTO collection_metadata (collection_id, key, value, updated_at)
1264
- VALUES (?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
1508
+ VALUES ${sets.map(() => `(?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))`).join(`, `)}
1265
1509
  ON CONFLICT(collection_id, key) DO UPDATE SET
1266
1510
  value = excluded.value,
1267
1511
  updated_at = excluded.updated_at`,
1268
- [
1512
+ sets.flatMap((mutation) => [
1269
1513
  collectionId,
1270
- metadataMutation.key,
1271
- serializePersistedRowValue(metadataMutation.value)
1272
- ]
1514
+ mutation.key,
1515
+ serializePersistedRowValue(mutation.value)
1516
+ ])
1273
1517
  );
1274
1518
  }
1275
1519
  }
@@ -1682,18 +1926,23 @@ class SQLiteCorePersistenceAdapter {
1682
1926
  }
1683
1927
  async loadSubsetInternal(tableMapping, options, driver = this.driver) {
1684
1928
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1685
- const whereCompiled = options.where ? compileSqlExpression(options.where) : { supported: true, sql: ``, params: [] };
1929
+ const whereCompiled = options.where ? compileSqlExpression(options.where) : { supported: false, sql: ``, params: [] };
1930
+ const safePrefilter = options.where ? compileSafeSqlPrefilter(options.where, whereCompiled) : void 0;
1686
1931
  const orderByCompiled = compileOrderByClauses(options.orderBy);
1687
1932
  const queryParams = [];
1688
1933
  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);
1934
+ if (safePrefilter) {
1935
+ sql = `${sql} WHERE ${safePrefilter.sql}`;
1936
+ queryParams.push(...safePrefilter.params);
1692
1937
  }
1693
1938
  if (options.orderBy && orderByCompiled.supported) {
1694
1939
  sql = `${sql} ORDER BY ${orderByCompiled.sql}, key ASC`;
1695
1940
  queryParams.push(...orderByCompiled.params);
1696
1941
  }
1942
+ if (queryParams.length > (driver.maxBoundParameters ?? this.driver.maxBoundParameters ?? 999)) {
1943
+ sql = `SELECT key, value, metadata, row_version FROM ${collectionTableSql}`;
1944
+ queryParams.length = 0;
1945
+ }
1697
1946
  const storedRows = await driver.query(sql, queryParams);
1698
1947
  const parsedRows = decodeStoredSqliteRows(storedRows);
1699
1948
  const filteredRows = this.applyInMemoryWhere(parsedRows, options.where);