@crvouga/sqlite-mem 1.12.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -67,6 +67,8 @@ Hot / large files: `parser/parser.ts`, `executor/select.ts`, `executor/dml.ts`.
67
67
  - **`Database`** (API) vs **`DatabaseState`** (engine storage).
68
68
  - Throw **`SqliteError`** with an `ErrorCategory`. Missing SQL must fail loud via `unsupported()` — the inventory gate fails if the oracle exposes an unimplemented builtin/module.
69
69
  - Fast-path helpers (`tryExecuteSimpleSelect`, `tryFastInsert`, `tryIndexedTableRows`, …): return `null` → fall through. Update **both** paths when semantics change.
70
+ - **Dual-path invariant:** residual `WHERE` filtering is skipped on an index/hash access path only when `whereFullyCovered(where, coveredColumns, …)` is true (see [`src/planner/access.ts`](src/planner/access.ts)). Any change to WHERE/join/insert semantics requires updating both fast and full paths **and** the fast≡full property tests under `tests/fuzz/fast-path.test.ts`.
71
+ - Internal invariants use always-on asserts in [`src/runtime/assert.ts`](src/runtime/assert.ts) (`assert`, `assertUnreachable`, `assertRowShape`).
70
72
  - TypeScript: `strict` + `noUncheckedIndexedAccess`. Imports use `.ts` extensions. Biome: 2-space, double quotes, 120 columns.
71
73
  - IEEE `-0` is canonicalized to `+0` on bind, affinity, and arithmetic. Keep determinism invariants (see README).
72
74
 
@@ -49,6 +49,10 @@ export declare class ExecutionEnv {
49
49
  triggerScope: ScopeRow | null;
50
50
  /** Cap SELECT output rows (used by Statement.get). */
51
51
  maxRows: number;
52
+ /** When true, skip simple-select / simple-join fast paths (tests only). */
53
+ forceFullSelect: boolean;
54
+ /** When true, skip tryFastInsert (tests only). */
55
+ forceFullInsert: boolean;
52
56
  includeNamedRows: boolean;
53
57
  includeValues: boolean;
54
58
  /** Source text of the statement currently executing (for sqlite_master.sql). */
package/dist/index.js CHANGED
@@ -2765,6 +2765,7 @@ var IndexStore = class _IndexStore {
2765
2765
  entries;
2766
2766
  keyValues;
2767
2767
  sortedKeys;
2768
+ mapsShared = false;
2768
2769
  frozen = false;
2769
2770
  constructor(name = "index", entries, keyValues) {
2770
2771
  this.name = name;
@@ -2818,10 +2819,22 @@ var IndexStore = class _IndexStore {
2818
2819
  if (values.some((value) => value === null)) return [];
2819
2820
  const prefix = serializeIndexEntry(values);
2820
2821
  const exact = this.entries.get(prefix);
2821
- const needle = `${prefix}|`;
2822
2822
  const rowids = exact ? [...exact] : [];
2823
- for (const [key, ids] of this.entries) {
2824
- if (key.startsWith(needle)) rowids.push(...ids);
2823
+ const keys = this.orderedKeys();
2824
+ let start = 0;
2825
+ let end = keys.length;
2826
+ while (start < end) {
2827
+ const mid = start + end >>> 1;
2828
+ const kv = this.keyValues.get(keys[mid]);
2829
+ if (!kv || prefixKeyLess(kv, values)) start = mid + 1;
2830
+ else end = mid;
2831
+ }
2832
+ for (let i = start; i < keys.length; i++) {
2833
+ const kv = this.keyValues.get(keys[i]);
2834
+ if (!kv || !prefixKeyMatches(kv, values)) break;
2835
+ const key = keys[i];
2836
+ if (key === prefix) continue;
2837
+ rowids.push(...this.entries.get(key) ?? []);
2825
2838
  }
2826
2839
  return rowids;
2827
2840
  }
@@ -2830,9 +2843,12 @@ var IndexStore = class _IndexStore {
2830
2843
  * `op` applies to the first key component.
2831
2844
  */
2832
2845
  rangeLookup(op, bound, bound2) {
2833
- const rowids = [];
2834
2846
  const keys = this.orderedKeys();
2835
- for (const key of keys) {
2847
+ const rowids = [];
2848
+ const start = lowerBoundKeyValues(keys, this.keyValues, bound, op === ">" ? "gt" : op === ">=" ? "ge" : "any");
2849
+ const end = op === "<" || op === "<=" ? upperBoundKeyValues(keys, this.keyValues, bound, op === "<" ? "lt" : "le") : op === "between" && bound2 !== void 0 ? upperBoundKeyValues(keys, this.keyValues, bound2, "le") : keys.length;
2850
+ for (let i = start; i < end; i++) {
2851
+ const key = keys[i];
2836
2852
  const values = this.keyValues.get(key);
2837
2853
  if (!values || values[0] === void 0 || values[0] === null) continue;
2838
2854
  const cmp = compareSerializedOrder(values[0], bound);
@@ -2889,7 +2905,12 @@ var IndexStore = class _IndexStore {
2889
2905
  this.sortedKeys = null;
2890
2906
  }
2891
2907
  clone() {
2892
- return new _IndexStore(this.name, this.entries, this.keyValues);
2908
+ const copy = new _IndexStore(this.name);
2909
+ copy.entries = this.entries;
2910
+ copy.keyValues = this.keyValues;
2911
+ copy.sortedKeys = this.sortedKeys;
2912
+ copy.mapsShared = true;
2913
+ return copy;
2893
2914
  }
2894
2915
  freeze() {
2895
2916
  this.frozen = true;
@@ -2945,6 +2966,18 @@ var IndexStore = class _IndexStore {
2945
2966
  }
2946
2967
  assertMutable() {
2947
2968
  if (this.frozen) throw new SqliteError("internal: cannot mutate a frozen index", "other");
2969
+ this.forkMaps();
2970
+ }
2971
+ forkMaps() {
2972
+ if (!this.mapsShared) return;
2973
+ const entries = /* @__PURE__ */ new Map();
2974
+ for (const [key, rowids] of this.entries) entries.set(key, [...rowids]);
2975
+ const keyValues = /* @__PURE__ */ new Map();
2976
+ for (const [key, values] of this.keyValues) keyValues.set(key, [...values]);
2977
+ this.entries = entries;
2978
+ this.keyValues = keyValues;
2979
+ this.mapsShared = false;
2980
+ this.sortedKeys = null;
2948
2981
  }
2949
2982
  };
2950
2983
  function serializeIndexKey(values) {
@@ -2983,6 +3016,53 @@ function serializeValue(value) {
2983
3016
  function compareSerializedOrder(left, right) {
2984
3017
  return compareSql(left, right) ?? 0;
2985
3018
  }
3019
+ function prefixKeyLess(keyValues, prefix) {
3020
+ for (let i = 0; i < prefix.length; i++) {
3021
+ const cmp = compareSerializedOrder(keyValues[i] ?? null, prefix[i]);
3022
+ if (cmp !== 0) return cmp < 0;
3023
+ }
3024
+ return false;
3025
+ }
3026
+ function prefixKeyMatches(keyValues, prefix) {
3027
+ for (let i = 0; i < prefix.length; i++) {
3028
+ if (compareSerializedOrder(keyValues[i] ?? null, prefix[i]) !== 0) return false;
3029
+ }
3030
+ return true;
3031
+ }
3032
+ function lowerBoundKeyValues(keys, keyValues, bound, mode) {
3033
+ let lo = 0;
3034
+ let hi = keys.length;
3035
+ while (lo < hi) {
3036
+ const mid = lo + hi >>> 1;
3037
+ const value = keyValues.get(keys[mid])?.[0];
3038
+ if (value === void 0 || value === null) {
3039
+ lo = mid + 1;
3040
+ continue;
3041
+ }
3042
+ const cmp = compareSerializedOrder(value, bound);
3043
+ const before = mode === "gt" ? cmp <= 0 : mode === "ge" ? cmp < 0 : false;
3044
+ if (before) lo = mid + 1;
3045
+ else hi = mid;
3046
+ }
3047
+ return lo;
3048
+ }
3049
+ function upperBoundKeyValues(keys, keyValues, bound, mode) {
3050
+ let lo = 0;
3051
+ let hi = keys.length;
3052
+ while (lo < hi) {
3053
+ const mid = lo + hi >>> 1;
3054
+ const value = keyValues.get(keys[mid])?.[0];
3055
+ if (value === void 0 || value === null) {
3056
+ lo = mid + 1;
3057
+ continue;
3058
+ }
3059
+ const cmp = compareSerializedOrder(value, bound);
3060
+ const after = mode === "lt" ? cmp >= 0 : cmp > 0;
3061
+ if (after) hi = mid;
3062
+ else lo = mid + 1;
3063
+ }
3064
+ return lo;
3065
+ }
2986
3066
  function sameRowid(left, right) {
2987
3067
  return typeof left === "bigint" || typeof right === "bigint" ? BigInt(left) === BigInt(right) : left === right;
2988
3068
  }
@@ -3366,6 +3446,9 @@ function parseFts5Query(input) {
3366
3446
  }
3367
3447
  if (p.kind === "TERM") {
3368
3448
  next();
3449
+ if (p.prefix && (p.value === "" || p.value.includes("*"))) {
3450
+ throw new SqliteError(`unknown special query: ${p.value}`, "other");
3451
+ }
3369
3452
  return { type: "term", value: p.value, prefix: p.prefix, column: null, columns: null };
3370
3453
  }
3371
3454
  throw new SqliteError(`fts5: syntax error near "${displayTok(p)}"`, "syntax");
@@ -3532,17 +3615,7 @@ function displayTok(t) {
3532
3615
  }
3533
3616
  }
3534
3617
  function parseFts3Query(input) {
3535
- try {
3536
- return parseFts5Query(input);
3537
- } catch {
3538
- const terms = input.split(/\s+/).filter(Boolean).filter((t) => !/^(AND|OR|NOT|NEAR)$/i.test(t));
3539
- if (terms.length === 0) return { type: "true" };
3540
- const nodes = terms.map((t) => {
3541
- const prefix = t.endsWith("*");
3542
- return { type: "term", value: prefix ? t.slice(0, -1) : t, prefix, column: null, columns: null };
3543
- });
3544
- return nodes.length === 1 ? nodes[0] : { type: "and", children: nodes };
3545
- }
3618
+ return parseFts5Query(input);
3546
3619
  }
3547
3620
 
3548
3621
  // src/vtable/fts/porter.ts
@@ -4593,6 +4666,26 @@ var FtsVocabVirtualTable = class _FtsVocabVirtualTable {
4593
4666
  }
4594
4667
  };
4595
4668
 
4669
+ // src/runtime/assert.ts
4670
+ function assert(condition, message) {
4671
+ if (!condition) {
4672
+ throw new SqliteError(`internal assertion failed: ${message}`, "other");
4673
+ }
4674
+ }
4675
+ function assertUnreachable(value, message = "unreachable") {
4676
+ throw new SqliteError(`internal assertion failed: ${message} (${String(value)})`, "other");
4677
+ }
4678
+ function assertRowShape(table, row) {
4679
+ assert(row.values.length === table.columns.length, `row.values.length !== table.columns.length for ${table.name}`);
4680
+ }
4681
+ var SQLITE_MAX_LENGTH = 2147483647;
4682
+ function assertBlobLength(length, _feature) {
4683
+ if (!Number.isFinite(length) || length < 0) return;
4684
+ if (length >= SQLITE_MAX_LENGTH) {
4685
+ throw new SqliteError(`string or blob too big`, "other", "SQLITE_TOOBIG");
4686
+ }
4687
+ }
4688
+
4596
4689
  // src/types/collation.ts
4597
4690
  function normalizeForCollation(value, name) {
4598
4691
  const collation = builtinCollation(name);
@@ -4769,6 +4862,7 @@ var Table = class _Table {
4769
4862
  this.scanCache = null;
4770
4863
  }
4771
4864
  commitRow(row) {
4865
+ assertRowShape(this, row);
4772
4866
  this.indexEquality(row);
4773
4867
  this.invalidateScan();
4774
4868
  }
@@ -4991,16 +5085,11 @@ var Table = class _Table {
4991
5085
  copy.nextRowid = this.nextRowid;
4992
5086
  copy.maximumRowid = this.maximumRowid;
4993
5087
  if (this.slab) {
4994
- let max = null;
4995
- for (const row of this.slab.scan()) {
4996
- copy.rows.set(row.rowid, cloneRow(row));
4997
- if (max === null || compareRowids(row.rowid, max) > 0) max = row.rowid;
4998
- }
4999
- copy.maximumRowid = max ?? void 0;
5088
+ for (const row of this.slab.scan()) copy.rows.set(row.rowid, row);
5000
5089
  } else {
5001
- for (const [rowid, row] of this.rows) copy.rows.set(rowid, cloneRow(row));
5090
+ for (const [rowid, row] of this.rows) copy.rows.set(rowid, row);
5002
5091
  }
5003
- for (const [clusterKey, row] of this.clusteredRows) copy.clusteredRows.set(clusterKey, cloneRow(row));
5092
+ for (const [clusterKey, row] of this.clusteredRows) copy.clusteredRows.set(clusterKey, row);
5004
5093
  return copy;
5005
5094
  }
5006
5095
  freeze() {
@@ -9297,6 +9386,7 @@ var scalarFunctions = {
9297
9386
  requireArgs3("randomblob", args, 1);
9298
9387
  if (args[0] === null) return null;
9299
9388
  const length = Math.max(0, Math.trunc(numeric(args[0])));
9389
+ assertBlobLength(length, "randomblob");
9300
9390
  const out = new Uint8Array(length);
9301
9391
  let offset = 0;
9302
9392
  while (offset < length) {
@@ -9311,6 +9401,7 @@ var scalarFunctions = {
9311
9401
  requireArgs3("zeroblob", args, 1);
9312
9402
  if (args[0] === null) return null;
9313
9403
  const length = Math.max(0, Math.trunc(numeric(args[0])));
9404
+ assertBlobLength(length, "zeroblob");
9314
9405
  return new Uint8Array(length);
9315
9406
  },
9316
9407
  hex(args) {
@@ -9676,6 +9767,10 @@ var ExecutionEnv = class {
9676
9767
  triggerScope = null;
9677
9768
  /** Cap SELECT output rows (used by Statement.get). */
9678
9769
  maxRows = Number.POSITIVE_INFINITY;
9770
+ /** When true, skip simple-select / simple-join fast paths (tests only). */
9771
+ forceFullSelect = false;
9772
+ /** When true, skip tryFastInsert (tests only). */
9773
+ forceFullInsert = false;
9679
9774
  includeNamedRows = true;
9680
9775
  includeValues = true;
9681
9776
  /** Source text of the statement currently executing (for sqlite_master.sql). */
@@ -9696,6 +9791,8 @@ var ExecutionEnv = class {
9696
9791
  this.triggerDepth = 0;
9697
9792
  this.triggerScope = null;
9698
9793
  this.maxRows = Number.POSITIVE_INFINITY;
9794
+ this.forceFullSelect = false;
9795
+ this.forceFullInsert = false;
9699
9796
  this.includeNamedRows = true;
9700
9797
  this.includeValues = true;
9701
9798
  this.statementSql = null;
@@ -9987,10 +10084,19 @@ function evalBinary(op, leftExpr, rightExpr, ctx) {
9987
10084
  if (left === null || right === null) return null;
9988
10085
  switch (op) {
9989
10086
  case "+":
10087
+ if (storageClassOf(left) === "integer" && storageClassOf(right) === "integer") {
10088
+ return integerArithmetic("+", left, right);
10089
+ }
9990
10090
  return asNumber(numberValue(left) + numberValue(right));
9991
10091
  case "-":
10092
+ if (storageClassOf(left) === "integer" && storageClassOf(right) === "integer") {
10093
+ return integerArithmetic("-", left, right);
10094
+ }
9992
10095
  return asNumber(numberValue(left) - numberValue(right));
9993
10096
  case "*":
10097
+ if (storageClassOf(left) === "integer" && storageClassOf(right) === "integer") {
10098
+ return integerArithmetic("*", left, right);
10099
+ }
9994
10100
  return asNumber(numberValue(left) * numberValue(right));
9995
10101
  case "/": {
9996
10102
  const divisor = numberValue(right);
@@ -10027,6 +10133,17 @@ function evalBinary(op, leftExpr, rightExpr, ctx) {
10027
10133
  function safeIntegerResult(value) {
10028
10134
  return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(Number.MIN_SAFE_INTEGER) ? Number(value) : value;
10029
10135
  }
10136
+ var I64_MIN = -(2n ** 63n);
10137
+ var I64_MAX = 2n ** 63n - 1n;
10138
+ function integerArithmetic(op, left, right) {
10139
+ const a = integerValue(left);
10140
+ const b = integerValue(right);
10141
+ const result = op === "+" ? a + b : op === "-" ? a - b : a * b;
10142
+ if (result < I64_MIN || result > I64_MAX) {
10143
+ return asSqlReal(Number(result));
10144
+ }
10145
+ return safeIntegerResult(result);
10146
+ }
10030
10147
  function compareRowValues(op, leftExprs, rightExprs, ctx) {
10031
10148
  if (leftExprs.length !== rightExprs.length) {
10032
10149
  throw new SqliteError("row value misused", "misuse");
@@ -11255,6 +11372,11 @@ function valuesEqual(left, right) {
11255
11372
  return false;
11256
11373
  }
11257
11374
 
11375
+ // src/runtime/catch.ts
11376
+ function isExpectedFastPathMiss(error, categories = ["no_such_table"]) {
11377
+ return error instanceof SqliteError && categories.includes(error.category);
11378
+ }
11379
+
11258
11380
  // src/planner/access.ts
11259
11381
  function conjunctions(expr) {
11260
11382
  if (expr.type === "binary" && expr.op === "AND") {
@@ -11318,7 +11440,8 @@ function lookupTableRows(from, where, env) {
11318
11440
  let table;
11319
11441
  try {
11320
11442
  table = db.getTable(from.name);
11321
- } catch {
11443
+ } catch (error) {
11444
+ if (!isExpectedFastPathMiss(error)) throw error;
11322
11445
  return null;
11323
11446
  }
11324
11447
  const parts = conjunctions(where);
@@ -11326,8 +11449,9 @@ function lookupTableRows(from, where, env) {
11326
11449
  const evalConst = (expr) => {
11327
11450
  try {
11328
11451
  return evalExpr(expr, env.createEvalContext(null));
11329
- } catch {
11330
- return void 0;
11452
+ } catch (error) {
11453
+ if (error instanceof SqliteError) return void 0;
11454
+ throw error;
11331
11455
  }
11332
11456
  };
11333
11457
  const resolved = [];
@@ -11474,7 +11598,8 @@ function tryIndexedOrder(from, order, env, limit) {
11474
11598
  let table;
11475
11599
  try {
11476
11600
  table = db.getTable(from.name);
11477
- } catch {
11601
+ } catch (error) {
11602
+ if (!isExpectedFastPathMiss(error)) throw error;
11478
11603
  return null;
11479
11604
  }
11480
11605
  const col = order.expr.name.toLowerCase();
@@ -11708,7 +11833,8 @@ function buildSqliteMaster(state) {
11708
11833
  function tryExecuteSimpleSelect(stmt, env) {
11709
11834
  if (stmt.with || stmt.compound || stmt.distinct || stmt.groupBy.length > 0 || stmt.having || stmt.windows.length > 0)
11710
11835
  return null;
11711
- if (stmt.orderBy.length > 0) return null;
11836
+ if (stmt.orderBy.length > 1) return null;
11837
+ if (stmt.orderBy.length === 1 && stmt.where) return null;
11712
11838
  if (stmt.from?.type !== "table") return null;
11713
11839
  for (const column of stmt.columns) {
11714
11840
  if (column.type === "star") continue;
@@ -11724,7 +11850,8 @@ function tryExecuteSimpleSelect(stmt, env) {
11724
11850
  let table;
11725
11851
  try {
11726
11852
  table = db.getTable(from.name);
11727
- } catch {
11853
+ } catch (error) {
11854
+ if (!isExpectedFastPathMiss(error)) throw error;
11728
11855
  return null;
11729
11856
  }
11730
11857
  if (table.withoutRowid) return null;
@@ -11749,8 +11876,9 @@ function tryExecuteSimpleSelect(stmt, env) {
11749
11876
  let value;
11750
11877
  try {
11751
11878
  value = evalIndependent(eq.valueExpr, env);
11752
- } catch {
11753
- return null;
11879
+ } catch (error) {
11880
+ if (error instanceof SqliteError) return null;
11881
+ throw error;
11754
11882
  }
11755
11883
  const column = eq.column.toLowerCase();
11756
11884
  const affinity = isRowidName2(column) ? "INTEGER" : table.columns.find((item) => (item.nameLower ?? item.name.toLowerCase()) === column)?.affinity;
@@ -11767,19 +11895,26 @@ function tryExecuteSimpleSelect(stmt, env) {
11767
11895
  offset = Math.max(0, Number(offsetValue));
11768
11896
  const n = Number(limitValue);
11769
11897
  if (n >= 0) limit = Math.min(limit, n);
11770
- } catch {
11771
- return null;
11898
+ } catch (error) {
11899
+ if (error instanceof SqliteError) return null;
11900
+ throw error;
11772
11901
  }
11773
11902
  }
11774
11903
  const columns = projectNames(stmt.columns, table);
11775
11904
  if (limit <= 0) return pack(env, columns, []);
11776
11905
  let source = table.scan();
11777
11906
  let alreadyFiltered = false;
11778
- if (stmt.where) {
11907
+ if (stmt.orderBy.length === 1 && stmt.orderBy[0]) {
11908
+ const take = limit < Number.MAX_SAFE_INTEGER ? offset + limit : void 0;
11909
+ const ordered = tryIndexedOrder(from, { expr: stmt.orderBy[0].expr, dir: stmt.orderBy[0].dir ?? "ASC" }, env, take);
11910
+ if (!ordered) return null;
11911
+ source = ordered.rows;
11912
+ alreadyFiltered = true;
11913
+ } else if (stmt.where) {
11779
11914
  const indexed = lookupTableRows(from, stmt.where, env);
11780
11915
  if (indexed) {
11781
11916
  source = indexed.rows;
11782
- alreadyFiltered = true;
11917
+ alreadyFiltered = whereFullyCovered(stmt.where, indexed.coveredColumns, alias, table.name);
11783
11918
  }
11784
11919
  }
11785
11920
  const projectors = projectionFns(stmt.columns, table);
@@ -11825,7 +11960,8 @@ function tryExecuteSimpleJoin(stmt, env) {
11825
11960
  try {
11826
11961
  leftTable = leftDb.getTable(leftRef.name);
11827
11962
  rightTable = rightDb.getTable(rightRef.name);
11828
- } catch {
11963
+ } catch (error) {
11964
+ if (!isExpectedFastPathMiss(error)) throw error;
11829
11965
  return null;
11830
11966
  }
11831
11967
  if (leftTable.withoutRowid || rightTable.withoutRowid) return null;
@@ -11910,7 +12046,7 @@ function evalIndependent(expr, env) {
11910
12046
  if (expr.type === "null") return null;
11911
12047
  if (expr.type === "parameter") return env.getBoundParameter(expr.name);
11912
12048
  if (expr.type === "unary" && expr.op === "-" && expr.expr.type === "literal" && typeof expr.expr.value === "number") {
11913
- return -expr.expr.value;
12049
+ return canonicalizeNumber(-expr.expr.value);
11914
12050
  }
11915
12051
  throw new Error("not independent");
11916
12052
  }
@@ -11930,7 +12066,7 @@ function applyAffinityLocal(value, affinity) {
11930
12066
  }
11931
12067
  function executeSelect2(stmt, env, parent) {
11932
12068
  env.selectRunner = executeSelect2;
11933
- if (!parent && !stmt.with && !stmt.compound) {
12069
+ if (!parent && !stmt.with && !stmt.compound && !env.forceFullSelect) {
11934
12070
  const simple = tryExecuteSimpleSelect(stmt, env) ?? tryExecuteSimpleJoin(stmt, env);
11935
12071
  if (simple) return simple;
11936
12072
  }
@@ -13524,7 +13660,7 @@ function executeInsertCore(stmt, env) {
13524
13660
  const totalBefore = env.state.totalChanges;
13525
13661
  const view = writableView(stmt.table, "INSERT", env);
13526
13662
  if (view) return executeViewInsert(stmt, view, env, totalBefore);
13527
- const fast = tryFastInsert(stmt, env);
13663
+ const fast = env.forceFullInsert ? null : tryFastInsert(stmt, env);
13528
13664
  if (fast) return fast;
13529
13665
  let table = env.state.getWritableTable(stmt.table);
13530
13666
  const columnNames = stmt.columns ?? table.columns.map((column) => column.name);
@@ -13727,7 +13863,8 @@ function tryFastInsert(stmt, env) {
13727
13863
  let table;
13728
13864
  try {
13729
13865
  table = env.state.getWritableTable(plan.tableName);
13730
- } catch {
13866
+ } catch (error) {
13867
+ if (!isExpectedFastPathMiss(error)) throw error;
13731
13868
  return null;
13732
13869
  }
13733
13870
  if (env.state.databaseForTable(table).triggers.size > 0) return null;
@@ -13742,7 +13879,8 @@ function buildFastInsertPlan(stmt, env) {
13742
13879
  let table;
13743
13880
  try {
13744
13881
  table = env.state.getTable(stmt.table);
13745
- } catch {
13882
+ } catch (error) {
13883
+ if (!isExpectedFastPathMiss(error)) throw error;
13746
13884
  return null;
13747
13885
  }
13748
13886
  if (!table.isUnconstrained()) return null;
@@ -14330,16 +14468,91 @@ function assertForeignKeyValues(values, constraint, env, excludeParent) {
14330
14468
  if (values.some((value) => value === null)) return;
14331
14469
  const parent = env.state.getTable(constraint.refTable);
14332
14470
  const parentColumns = constraint.refColumns ?? parent.columns.filter((column) => column.primaryKey).map((column) => column.name);
14333
- if (![...parent.scan()].some((candidate) => {
14334
- if (excludeParent && candidate.rowid === excludeParent.rowid) return false;
14335
- return values.every((value, index) => {
14336
- const parentColumn = parentColumns[index];
14337
- return parentColumn !== void 0 && compareSql(value, parent.cell(candidate, normalizeColumnName(parentColumn))) === 0;
14338
- });
14339
- })) {
14471
+ if (findParentRowsForFk(parent, parentColumns, values, env, excludeParent).length === 0) {
14340
14472
  throw new SqliteError("FOREIGN KEY constraint failed", "constraint_foreign", "SQLITE_CONSTRAINT_FOREIGNKEY");
14341
14473
  }
14342
14474
  }
14475
+ function parentRowMatchesFk(parent, parentColumns, values, excludeParent, candidate) {
14476
+ if (excludeParent && candidate.rowid === excludeParent.rowid) return false;
14477
+ return values.every((value, index) => {
14478
+ const parentColumn = parentColumns[index];
14479
+ return parentColumn !== void 0 && compareSql(value, parent.cell(candidate, normalizeColumnName(parentColumn))) === 0;
14480
+ });
14481
+ }
14482
+ function findParentRowsForFk(parent, parentColumns, values, env, excludeParent) {
14483
+ const db = env.state.databaseForTable(parent);
14484
+ const pk = parent.integerPkColumn();
14485
+ if (parentColumns.length === 1) {
14486
+ const col = parentColumns[0].toLowerCase();
14487
+ if (col === "rowid" || col === "_rowid_" || col === "oid" || pk && col === (pk.nameLower ?? pk.name.toLowerCase())) {
14488
+ const row = parent.getByKey(values[0]);
14489
+ if (row && parentRowMatchesFk(parent, parentColumns, values, excludeParent, row)) return [row];
14490
+ return [];
14491
+ }
14492
+ }
14493
+ const normalizedValues = values.map((value, index) => {
14494
+ const colName = parentColumns[index];
14495
+ const column = parent.columns.find((item) => (item.nameLower ?? item.name.toLowerCase()) === colName.toLowerCase());
14496
+ return normalizeForCollation(value, column?.collate ?? "BINARY");
14497
+ });
14498
+ for (const indexName of parent.indexes) {
14499
+ const index = db.indexes.get(indexName.toLowerCase());
14500
+ if (!index?.unique || index.where || index.columns.some((column) => column.expr)) continue;
14501
+ if (index.columns.length !== parentColumns.length) continue;
14502
+ if (!index.columns.every((column, idx) => column.name.toLowerCase() === parentColumns[idx].toLowerCase()))
14503
+ continue;
14504
+ const rows = [];
14505
+ for (const rowid of index.store.lookup(normalizedValues)) {
14506
+ const candidate = parent.get(rowid);
14507
+ if (candidate && parentRowMatchesFk(parent, parentColumns, values, excludeParent, candidate))
14508
+ rows.push(candidate);
14509
+ }
14510
+ return rows;
14511
+ }
14512
+ return [...parent.scan()].filter(
14513
+ (candidate) => parentRowMatchesFk(parent, parentColumns, values, excludeParent, candidate)
14514
+ );
14515
+ }
14516
+ function findChildRowsForFk(child, childColumns, parentColumns, parent, parentRow, env) {
14517
+ const refValues = parentColumns.map((name) => parent.cell(parentRow, normalizeColumnName(name)));
14518
+ if (refValues.some((value) => value === null)) return [];
14519
+ if (childColumns.length === 1) {
14520
+ const col = childColumns[0].toLowerCase();
14521
+ const matches = child.lookupEquality(col, refValues[0]);
14522
+ if (matches) {
14523
+ return matches.filter(
14524
+ (candidate) => foreignKeyMatches(childColumns, candidate, parentColumns, parentRow, child, parent)
14525
+ );
14526
+ }
14527
+ }
14528
+ const db = env.state.databaseForTable(child);
14529
+ const normalizedValues = refValues.map((value, index) => {
14530
+ const colName = childColumns[index];
14531
+ const column = child.columns.find((item) => (item.nameLower ?? item.name.toLowerCase()) === colName.toLowerCase());
14532
+ return normalizeForCollation(value, column?.collate ?? "BINARY");
14533
+ });
14534
+ for (const indexName of child.indexes) {
14535
+ const index = db.indexes.get(indexName.toLowerCase());
14536
+ if (!index || index.where || index.columns.some((column) => column.expr)) continue;
14537
+ if (index.columns.length < childColumns.length) continue;
14538
+ if (!childColumns.every((name, idx) => index.columns[idx]?.name.toLowerCase() === name.toLowerCase())) {
14539
+ continue;
14540
+ }
14541
+ const lookupValues = normalizedValues.slice(0, index.columns.length);
14542
+ const rowids = lookupValues.length === index.columns.length ? index.store.lookup(lookupValues) : index.store.lookupPrefix(lookupValues);
14543
+ const rows = [];
14544
+ for (const rowid of rowids) {
14545
+ const candidate = child.get(rowid);
14546
+ if (candidate && foreignKeyMatches(childColumns, candidate, parentColumns, parentRow, child, parent)) {
14547
+ rows.push(candidate);
14548
+ }
14549
+ }
14550
+ return rows;
14551
+ }
14552
+ return [...child.scan()].filter(
14553
+ (candidate) => foreignKeyMatches(childColumns, candidate, parentColumns, parentRow, child, parent)
14554
+ );
14555
+ }
14343
14556
  function checkDeferredForeignKeys(env) {
14344
14557
  if (!env.state.foreignKeysEnabled) return;
14345
14558
  for (const table of env.state.tables.values()) {
@@ -14362,8 +14575,8 @@ function applyReferentialDelete(parent, row, env) {
14362
14575
  child = env.state.ensureWritableTable(child);
14363
14576
  const target = child;
14364
14577
  const referenced = constraint.refColumns ?? parentPk;
14365
- const matches = [...target.scan()].filter(
14366
- (candidate) => !(target === parent && candidate.rowid === row.rowid) && foreignKeyMatches(constraint.columns, candidate, referenced, row, target, parent)
14578
+ const matches = findChildRowsForFk(target, constraint.columns, referenced, parent, row, env).filter(
14579
+ (candidate) => !(target === parent && candidate.rowid === row.rowid)
14367
14580
  );
14368
14581
  for (const candidate of matches) {
14369
14582
  if (constraint.onDelete === "CASCADE") {
@@ -14409,9 +14622,7 @@ function applyReferentialUpdate(parent, before, after, env) {
14409
14622
  const oldValues = referenced.map((name) => parent.cell(before, normalizeColumnName(name)));
14410
14623
  const newValues = referenced.map((name) => parent.cell(after, normalizeColumnName(name)));
14411
14624
  if (oldValues.every((value, index) => compareSql(value, newValues[index] ?? null) === 0)) continue;
14412
- const matches = [...target.scan()].filter(
14413
- (candidate) => foreignKeyMatches(constraint.columns, candidate, referenced, before, target, parent)
14414
- );
14625
+ const matches = findChildRowsForFk(target, constraint.columns, referenced, parent, before, env);
14415
14626
  for (const candidate of matches) {
14416
14627
  if (constraint.onUpdate === "CASCADE") {
14417
14628
  const updated = updateOne(
@@ -14801,6 +15012,8 @@ function executeStatement(stmt, env) {
14801
15012
  case "vacuum":
14802
15013
  env.state.recordChange(0);
14803
15014
  return emptyResult(0, env.state.lastInsertRowid);
15015
+ default:
15016
+ return assertUnreachable(stmt);
14804
15017
  }
14805
15018
  }
14806
15019
  function executeReindex(stmt, env) {