@crvouga/sqlite-mem 1.6.0 → 1.8.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/dist/index.js CHANGED
@@ -1037,7 +1037,7 @@ var Parser = class {
1037
1037
  if (this.at("SELECT") || this.at("WITH") || this.at("VALUES")) {
1038
1038
  const select = this.at("SELECT") || this.at("WITH") ? this.parseSelectStmt() : this.parseValuesAsSelect();
1039
1039
  this.expect("RPAREN");
1040
- const alias3 = this.optionalAlias() ?? this.syntaxError("subquery in FROM requires an alias");
1040
+ const alias3 = this.optionalAlias() ?? `__subq_${this.pos}`;
1041
1041
  return { type: "subquery", select, alias: alias3 };
1042
1042
  }
1043
1043
  const item = this.parseFromItem();
@@ -1264,13 +1264,15 @@ var Parser = class {
1264
1264
  // ── DELETE ──────────────────────────────────────────────────────────────
1265
1265
  parseDeleteStmt(withClause = this.parseOptionalWith()) {
1266
1266
  this.expect("DELETE");
1267
+ const or = this.parseOrConflict();
1267
1268
  this.expect("FROM");
1268
1269
  const table = this.parseTableName();
1269
1270
  const alias = this.optionalAlias();
1270
1271
  let where = null;
1271
1272
  if (this.match("WHERE")) where = this.parseExpr();
1272
1273
  const returning = this.parseReturning();
1273
- return { type: "delete", with: withClause, table, alias, where, returning };
1274
+ const orMode = or === null ? null : or === "REPLACE" ? "replace" : or === "IGNORE" ? "ignore" : or === "ABORT" ? "abort" : or === "ROLLBACK" ? "rollback" : "fail";
1275
+ return { type: "delete", with: withClause, or: orMode, table, alias, where, returning };
1274
1276
  }
1275
1277
  // ── CREATE ──────────────────────────────────────────────────────────────
1276
1278
  parseCreateStmt() {
@@ -1559,9 +1561,9 @@ var Parser = class {
1559
1561
  } while (this.match("COMMA"));
1560
1562
  this.expect("RPAREN");
1561
1563
  }
1562
- const { onDelete, onUpdate } = this.parseFkActions();
1564
+ const { onDelete, onUpdate, match } = this.parseFkActions();
1563
1565
  const { deferrable, initiallyDeferred } = this.parseDeferrable();
1564
- return { type: "references", table, columns, onDelete, onUpdate, deferrable, initiallyDeferred };
1566
+ return { type: "references", table, columns, onDelete, onUpdate, match, deferrable, initiallyDeferred };
1565
1567
  }
1566
1568
  parseTableConstraint() {
1567
1569
  let name = null;
@@ -1607,7 +1609,7 @@ var Parser = class {
1607
1609
  } while (this.match("COMMA"));
1608
1610
  this.expect("RPAREN");
1609
1611
  }
1610
- const { onDelete, onUpdate } = this.parseFkActions();
1612
+ const { onDelete, onUpdate, match } = this.parseFkActions();
1611
1613
  const { deferrable, initiallyDeferred } = this.parseDeferrable();
1612
1614
  return {
1613
1615
  type: "foreign_key",
@@ -1616,6 +1618,7 @@ var Parser = class {
1616
1618
  refColumns,
1617
1619
  onDelete,
1618
1620
  onUpdate,
1621
+ match,
1619
1622
  name,
1620
1623
  deferrable,
1621
1624
  initiallyDeferred
@@ -1677,21 +1680,24 @@ var Parser = class {
1677
1680
  parseFkActions() {
1678
1681
  let onDelete = null;
1679
1682
  let onUpdate = null;
1680
- while (this.match("MATCH")) {
1681
- this.parseIdent();
1682
- }
1683
+ let match = "SIMPLE";
1684
+ const readMatch = () => {
1685
+ while (this.match("MATCH")) {
1686
+ const word = this.parseIdent().toUpperCase();
1687
+ if (word === "FULL") match = "FULL";
1688
+ else if (word === "PARTIAL") match = "PARTIAL";
1689
+ else match = "SIMPLE";
1690
+ }
1691
+ };
1692
+ readMatch();
1683
1693
  while (this.at("ON")) {
1684
1694
  if (this.peek().kind === "DELETE") onDelete = this.parseFkAction("DELETE");
1685
1695
  else if (this.peek().kind === "UPDATE") onUpdate = this.parseFkAction("UPDATE");
1686
1696
  else this.syntaxError("expected ON DELETE or ON UPDATE");
1687
- while (this.match("MATCH")) {
1688
- this.parseIdent();
1689
- }
1690
- }
1691
- while (this.match("MATCH")) {
1692
- this.parseIdent();
1697
+ readMatch();
1693
1698
  }
1694
- return { onDelete, onUpdate };
1699
+ readMatch();
1700
+ return { onDelete, onUpdate, match };
1695
1701
  }
1696
1702
  parseDeferrable() {
1697
1703
  let deferrable = false;
@@ -2566,32 +2572,46 @@ function applyAffinity(value, affinity) {
2566
2572
  if (value === null) return null;
2567
2573
  switch (affinity) {
2568
2574
  case "TEXT":
2569
- if (value instanceof Uint8Array) return utf8Decode(value);
2575
+ if (value instanceof Uint8Array) return value;
2570
2576
  if (value instanceof SqlJsonText) return value.value;
2571
2577
  if (typeof value === "string") return value;
2572
- if (value instanceof SqlReal) return String(value.value);
2578
+ if (value instanceof SqlReal) return formatRealAsText(value.value);
2579
+ if (typeof value === "number") return String(canonicalizeNumber(value));
2580
+ if (typeof value === "bigint") return value.toString();
2573
2581
  return String(value);
2574
2582
  case "INTEGER": {
2583
+ if (value instanceof Uint8Array) return value;
2575
2584
  const n = coerceToNumber(value);
2576
2585
  if (n === null) return value;
2577
2586
  if (typeof value === "bigint") return value;
2578
2587
  return Number.isInteger(n) && Number.isSafeInteger(n) ? Math.trunc(n) : canonicalizeNumber(n);
2579
2588
  }
2580
2589
  case "REAL": {
2590
+ if (value instanceof Uint8Array) return value;
2581
2591
  const n = coerceToNumber(value);
2582
2592
  return n === null ? value : asSqlReal(n);
2583
2593
  }
2584
2594
  case "NUMERIC": {
2595
+ if (value instanceof Uint8Array) return value;
2585
2596
  const n = coerceToNumber(value);
2586
2597
  if (n === null) return value;
2587
2598
  if (Number.isInteger(n) && Number.isSafeInteger(n)) return Math.trunc(n);
2588
2599
  return canonicalizeNumber(n);
2589
2600
  }
2590
2601
  case "BLOB":
2591
- if (typeof value === "number") return canonicalizeNumber(value);
2592
2602
  return value;
2593
2603
  }
2594
2604
  }
2605
+ function formatRealAsText(value) {
2606
+ const n = canonicalizeNumber(value);
2607
+ if (Object.is(n, -0) || n === 0) return "0.0";
2608
+ if (Number.isInteger(n) && Number.isSafeInteger(n)) return `${n}.0`;
2609
+ const abs = Math.abs(n);
2610
+ if (abs >= 1e16 || abs > 0 && abs < 1e-4) {
2611
+ return n.toExponential(1).replace(/e([+-])(\d)$/, "e$10$2").replace(/e\+/, "e+");
2612
+ }
2613
+ return String(n);
2614
+ }
2595
2615
  function applyComparisonAffinity(left, right, leftAffinity, rightAffinity) {
2596
2616
  const leftNumeric = leftAffinity === "INTEGER" || leftAffinity === "REAL" || leftAffinity === "NUMERIC";
2597
2617
  const rightNumeric = rightAffinity === "INTEGER" || rightAffinity === "REAL" || rightAffinity === "NUMERIC";
@@ -5768,8 +5788,14 @@ function evalExpr(expr, ctx) {
5768
5788
  return truth === null ? null : booleanValue(!truth);
5769
5789
  }
5770
5790
  if (value === null) return null;
5771
- if (expr.op === "+") return asNumber(numberValue(value));
5772
- if (expr.op === "-") return asNumber(-numberValue(value));
5791
+ if (expr.op === "+") {
5792
+ const n = asNumber(numberValue(value));
5793
+ return storageClassOf(value) === "real" ? asSqlReal(n) : n;
5794
+ }
5795
+ if (expr.op === "-") {
5796
+ const n = asNumber(-numberValue(value));
5797
+ return storageClassOf(value) === "real" ? asSqlReal(n) : n;
5798
+ }
5773
5799
  return ~integerValue(value);
5774
5800
  }
5775
5801
  case "is_bool": {
@@ -8402,6 +8428,7 @@ var DatabaseState = class _DatabaseState {
8402
8428
  refColumns: constraint.columns,
8403
8429
  onDelete: constraint.onDelete,
8404
8430
  onUpdate: constraint.onUpdate,
8431
+ match: constraint.match,
8405
8432
  name: null,
8406
8433
  deferrable: constraint.deferrable,
8407
8434
  initiallyDeferred: constraint.initiallyDeferred
@@ -12588,7 +12615,7 @@ function executeInsertCore(stmt, env) {
12588
12615
  if (view) return executeViewInsert(stmt, view, env, totalBefore);
12589
12616
  const fast = tryFastInsert(stmt, env);
12590
12617
  if (fast) return fast;
12591
- const table = env.state.getWritableTable(stmt.table);
12618
+ let table = env.state.getWritableTable(stmt.table);
12592
12619
  const columnNames = stmt.columns ?? table.columns.map((column) => column.name);
12593
12620
  const rowidIndexes = columnNames.map((name, index) => isRowidName3(name) ? index : -1).filter((index) => index >= 0);
12594
12621
  if (rowidIndexes.length > 1) throw new SqliteError("duplicate column name: rowid", "other");
@@ -12624,116 +12651,141 @@ function executeInsertCore(stmt, env) {
12624
12651
  env.state.recordChange(changes, last);
12625
12652
  return emptyResult(changes, last);
12626
12653
  }
12627
- for (const source of sourceRows) {
12628
- if (source.length !== columnNames.length)
12629
- throw new SqliteError(
12630
- stmt.columns ? `${source.length} values for ${columnNames.length} columns` : `table ${table.name} has ${columnNames.length} columns but ${source.length} values were supplied`,
12631
- "other"
12632
- );
12633
- const values = /* @__PURE__ */ new Map();
12634
- for (const column of table.columns) {
12635
- if (column.generated) continue;
12636
- const suppliedIndex = suppliedIndexes[table.columns.indexOf(column)] ?? -1;
12637
- const value = suppliedIndex >= 0 ? source[suppliedIndex] ?? null : column.defaultExpr ? evalExpr(column.defaultExpr, env.createEvalContext()) : null;
12638
- values.set(column.nameLower ?? column.name.toLowerCase(), storeColumnValue(table, column, value));
12654
+ const abortMode = insertAbortResolution(stmt.mode) === "abort";
12655
+ const undoInserted = [];
12656
+ const undoRemoved = [];
12657
+ const undoStatement = () => {
12658
+ if (!abortMode) return;
12659
+ for (let i = undoInserted.length - 1; i >= 0; i--) {
12660
+ const row = table.rows.get(undoInserted[i]);
12661
+ if (row) removeOne(table, row, env);
12662
+ }
12663
+ for (let i = undoRemoved.length - 1; i >= 0; i--) {
12664
+ const row = undoRemoved[i];
12665
+ table = env.state.ensureWritableTable(table);
12666
+ table.rows.set(row.rowid, row);
12667
+ if (table.indexes.length > 0) addIndexes(table, row, env);
12639
12668
  }
12640
- for (const column of table.columns) {
12641
- if (!column.generated) continue;
12642
- if (columnNames.some((name) => name.toLowerCase() === column.name.toLowerCase())) {
12643
- throw new SqliteError(`cannot INSERT into generated column "${column.name}"`, "misuse");
12644
- }
12645
- if (column.generated.stored) {
12646
- const genCtx = env.createEvalContext({
12647
- cells: table.columns.filter((c) => !c.generated || c === column).flatMap((c) => {
12648
- if (c.generated && c !== column) return [];
12649
- if (c === column) return [];
12650
- return [
12651
- {
12652
- table: table.name,
12653
- name: c.name,
12654
- value: values.get(normalizeColumnName(c.name)) ?? null,
12655
- affinity: c.affinity,
12656
- collate: c.collate
12657
- }
12658
- ];
12659
- })
12660
- });
12661
- const computed = evalExpr(column.generated.expr, genCtx);
12662
- values.set(normalizeColumnName(column.name), storeColumnValue(table, column, computed));
12663
- } else {
12664
- values.set(normalizeColumnName(column.name), null);
12669
+ };
12670
+ try {
12671
+ for (const source of sourceRows) {
12672
+ if (source.length !== columnNames.length)
12673
+ throw new SqliteError(
12674
+ stmt.columns ? `${source.length} values for ${columnNames.length} columns` : `table ${table.name} has ${columnNames.length} columns but ${source.length} values were supplied`,
12675
+ "other"
12676
+ );
12677
+ const values = /* @__PURE__ */ new Map();
12678
+ for (const column of table.columns) {
12679
+ if (column.generated) continue;
12680
+ const suppliedIndex = suppliedIndexes[table.columns.indexOf(column)] ?? -1;
12681
+ const value = suppliedIndex >= 0 ? source[suppliedIndex] ?? null : column.defaultExpr ? evalExpr(column.defaultExpr, env.createEvalContext()) : null;
12682
+ values.set(column.nameLower ?? column.name.toLowerCase(), storeColumnValue(table, column, value));
12665
12683
  }
12666
- }
12667
- if (fireInsertTriggers("BEFORE", table, values, null, env) === "ignore") continue;
12668
- const conflicts = conflictingRows(table, values, env);
12669
- if (conflicts.length > 0) {
12670
- if (stmt.upsert) {
12671
- const targetConflicts = conflictsForUpsert(table, values, env, stmt.upsert);
12672
- if (targetConflicts.length === 0) {
12684
+ for (const column of table.columns) {
12685
+ if (!column.generated) continue;
12686
+ if (columnNames.some((name) => name.toLowerCase() === column.name.toLowerCase())) {
12687
+ throw new SqliteError(`cannot INSERT into generated column "${column.name}"`, "misuse");
12688
+ }
12689
+ if (column.generated.stored) {
12690
+ const genCtx = env.createEvalContext({
12691
+ cells: table.columns.filter((c) => !c.generated || c === column).flatMap((c) => {
12692
+ if (c.generated && c !== column) return [];
12693
+ if (c === column) return [];
12694
+ return [
12695
+ {
12696
+ table: table.name,
12697
+ name: c.name,
12698
+ value: values.get(normalizeColumnName(c.name)) ?? null,
12699
+ affinity: c.affinity,
12700
+ collate: c.collate
12701
+ }
12702
+ ];
12703
+ })
12704
+ });
12705
+ const computed = evalExpr(column.generated.expr, genCtx);
12706
+ values.set(normalizeColumnName(column.name), storeColumnValue(table, column, computed));
12707
+ } else {
12708
+ values.set(normalizeColumnName(column.name), null);
12709
+ }
12710
+ }
12711
+ if (fireInsertTriggers("BEFORE", table, values, null, env) === "ignore") continue;
12712
+ const conflicts = conflictingRows(table, values, env);
12713
+ if (conflicts.length > 0) {
12714
+ if (stmt.upsert) {
12715
+ const targetConflicts = conflictsForUpsert(table, values, env, stmt.upsert);
12716
+ if (targetConflicts.length === 0) {
12717
+ throw new SqliteError(
12718
+ `UNIQUE constraint failed: ${table.name}`,
12719
+ "constraint_unique",
12720
+ "SQLITE_CONSTRAINT_UNIQUE"
12721
+ );
12722
+ }
12723
+ if (stmt.upsert.action === "nothing") continue;
12724
+ const row = targetConflicts[0];
12725
+ const scope = scopeFor(table, row, stmt.table, env);
12726
+ const excluded = {
12727
+ cells: table.columns.map((column) => ({
12728
+ table: "excluded",
12729
+ name: column.name,
12730
+ value: values.get(normalizeColumnName(column.name)) ?? null
12731
+ }))
12732
+ };
12733
+ const merged = { cells: [...scope.cells, ...excluded.cells], rowid: row.rowid, sourceTable: table.name };
12734
+ const ctx = env.createEvalContext(merged);
12735
+ if (stmt.upsert.action.where && isTruthySql(evalExpr(stmt.upsert.action.where, ctx)) !== true) continue;
12736
+ const updates = evaluateSet(stmt.upsert.action.set, table, ctx);
12737
+ const updated = updateOne(table, row, updates, env);
12738
+ changes += 1 + updated.cascaded;
12739
+ if (stmt.returning.length)
12740
+ returningRows.push(projectReturning(stmt.returning, scopeFor(table, updated.row, stmt.table, env), env));
12741
+ continue;
12742
+ }
12743
+ const mode = stmt.mode;
12744
+ if (mode === "insert_or_ignore") continue;
12745
+ if (mode === "replace" || mode === "insert_or_replace") {
12746
+ for (const row of conflicts) {
12747
+ if (abortMode) undoRemoved.push(cloneRow(row));
12748
+ removeOne(table, row, env);
12749
+ }
12750
+ } else {
12673
12751
  throw new SqliteError(
12674
12752
  `UNIQUE constraint failed: ${table.name}`,
12675
12753
  "constraint_unique",
12676
12754
  "SQLITE_CONSTRAINT_UNIQUE"
12677
12755
  );
12678
12756
  }
12679
- if (stmt.upsert.action === "nothing") continue;
12680
- const row = targetConflicts[0];
12681
- const scope = scopeFor(table, row, stmt.table, env);
12682
- const excluded = {
12683
- cells: table.columns.map((column) => ({
12684
- table: "excluded",
12685
- name: column.name,
12686
- value: values.get(normalizeColumnName(column.name)) ?? null
12687
- }))
12688
- };
12689
- const merged = { cells: [...scope.cells, ...excluded.cells], rowid: row.rowid, sourceTable: table.name };
12690
- const ctx = env.createEvalContext(merged);
12691
- if (stmt.upsert.action.where && isTruthySql(evalExpr(stmt.upsert.action.where, ctx)) !== true) continue;
12692
- const updates = evaluateSet(stmt.upsert.action.set, table, ctx);
12693
- const updated = updateOne(table, row, updates, env);
12694
- changes += 1 + updated.cascaded;
12695
- if (stmt.returning.length)
12696
- returningRows.push(projectReturning(stmt.returning, scopeFor(table, updated.row, stmt.table, env), env));
12697
- continue;
12698
12757
  }
12699
- const mode = stmt.mode;
12700
- if (mode === "insert_or_ignore") continue;
12701
- if (mode === "replace" || mode === "insert_or_replace") {
12702
- for (const row of conflicts) removeOne(table, row, env);
12703
- } else {
12704
- throw new SqliteError(
12705
- `UNIQUE constraint failed: ${table.name}`,
12706
- "constraint_unique",
12707
- "SQLITE_CONSTRAINT_UNIQUE"
12708
- );
12709
- }
12710
- }
12711
- let insertedRowid;
12712
- try {
12713
- const suppliedRowid = rowidIndexes.length ? asExplicitRowid(source[rowidIndexes[0]] ?? null) : void 0;
12714
- const rowid = table.insert({ values, rowid: suppliedRowid }, { prepared: true });
12715
- insertedRowid = rowid;
12716
- const row = table.rows.get(rowid);
12717
- validateRow(table, row, env);
12718
- if (table.indexes.length > 0) addIndexes(table, row, env);
12719
- if (env.state.foreignKeysEnabled) checkForeignKeys(table, row, env);
12720
- if (!table.withoutRowid) {
12721
- last = rowid;
12722
- env.state.lastInsertRowid = rowid;
12758
+ let insertedRowid;
12759
+ try {
12760
+ const suppliedRowid = rowidIndexes.length ? asExplicitRowid(source[rowidIndexes[0]] ?? null) : void 0;
12761
+ const rowid = table.insert({ values, rowid: suppliedRowid }, { prepared: true });
12762
+ insertedRowid = rowid;
12763
+ const row = table.rows.get(rowid);
12764
+ validateRow(table, row, env);
12765
+ if (table.indexes.length > 0) addIndexes(table, row, env);
12766
+ if (env.state.foreignKeysEnabled) checkForeignKeys(table, row, env);
12767
+ if (!table.withoutRowid) {
12768
+ last = rowid;
12769
+ env.state.lastInsertRowid = rowid;
12770
+ }
12771
+ fireInsertTriggers("AFTER", table, row.values, null, env);
12772
+ changes++;
12773
+ if (abortMode) undoInserted.push(rowid);
12774
+ if (stmt.returning.length)
12775
+ returningRows.push(projectReturning(stmt.returning, scopeFor(table, row, stmt.table, env), env));
12776
+ } catch (error) {
12777
+ if (insertedRowid !== void 0) {
12778
+ const inserted = table.rows.get(insertedRowid);
12779
+ if (inserted) removeOne(table, inserted, env);
12780
+ }
12781
+ if (stmt.mode === "insert_or_ignore" && error instanceof SqliteError && error.category.startsWith("constraint") && error.category !== "constraint_foreign")
12782
+ continue;
12783
+ throw error;
12723
12784
  }
12724
- fireInsertTriggers("AFTER", table, row.values, null, env);
12725
- changes++;
12726
- if (stmt.returning.length)
12727
- returningRows.push(projectReturning(stmt.returning, scopeFor(table, row, stmt.table, env), env));
12728
- } catch (error) {
12729
- if (insertedRowid !== void 0) {
12730
- const inserted = table.rows.get(insertedRowid);
12731
- if (inserted) removeOne(table, inserted, env);
12732
- }
12733
- if (stmt.mode === "insert_or_ignore" && error instanceof SqliteError && error.category.startsWith("constraint"))
12734
- continue;
12735
- throw error;
12736
12785
  }
12786
+ } catch (error) {
12787
+ undoStatement();
12788
+ throw error;
12737
12789
  }
12738
12790
  const reportedChanges = finalizeDmlChanges(totalBefore, changes, env, last);
12739
12791
  if (stmt.returning.length === 0) return emptyResult(reportedChanges, last);
@@ -12746,8 +12798,6 @@ function evaluateInsertSource(stmt, env) {
12746
12798
  return stmt.values.map(
12747
12799
  (items) => items.map((expr) => {
12748
12800
  if (expr.type === "parameter") return env.getBoundParameter(expr.name);
12749
- if (expr.type === "literal") return expr.value;
12750
- if (expr.type === "null") return null;
12751
12801
  return evalExpr(expr, ctx);
12752
12802
  })
12753
12803
  );
@@ -12805,7 +12855,7 @@ function buildFastInsertPlan(stmt, env) {
12805
12855
  const name = expr.name;
12806
12856
  slots.push({ key, affinity: column.affinity, read: (exec) => exec.getBoundParameter(name) });
12807
12857
  } else if (expr.type === "literal") {
12808
- const value = expr.value;
12858
+ const value = expr.forceReal && typeof expr.value === "number" ? asSqlReal(expr.value) : expr.value;
12809
12859
  slots.push({ key, affinity: column.affinity, read: () => value });
12810
12860
  } else if (expr.type === "null") {
12811
12861
  slots.push({ key, affinity: column.affinity, read: () => null });
@@ -12828,13 +12878,13 @@ function executeUpdateCore(stmt, env) {
12828
12878
  const totalBefore = env.state.totalChanges;
12829
12879
  const view = writableView(stmt.table, "UPDATE", env);
12830
12880
  if (view) return executeViewUpdate(stmt, view, env, totalBefore);
12831
- const table = env.state.getWritableTable(stmt.table);
12881
+ const scanTable = env.state.getTable(stmt.table);
12832
12882
  const alias = stmt.alias ?? stmt.table;
12833
12883
  const candidates = [];
12834
12884
  if (stmt.from) {
12835
12885
  const fromRows = scanFrom(stmt.from, env);
12836
- for (const row of table.scan()) {
12837
- const targetScope = scopeFor(table, row, alias, env);
12886
+ for (const row of scanTable.scan()) {
12887
+ const targetScope = scopeFor(scanTable, row, alias, env);
12838
12888
  let matchedScope = null;
12839
12889
  for (const fromRow of fromRows) {
12840
12890
  const joined = {
@@ -12848,47 +12898,68 @@ function executeUpdateCore(stmt, env) {
12848
12898
  }
12849
12899
  } else {
12850
12900
  const indexed = stmt.where === null ? null : tryIndexedTableRows({ type: "table", schema: null, name: stmt.table, alias: stmt.alias }, stmt.where, env);
12851
- const scanRows = indexed ? indexed.rows : table.scan();
12901
+ const scanRows = indexed ? indexed.rows : scanTable.scan();
12852
12902
  for (const row of scanRows) {
12853
- const scope = scopeFor(table, row, alias, env);
12903
+ const scope = scopeFor(scanTable, row, alias, env);
12854
12904
  if (stmt.where && isTruthySql(evalExpr(stmt.where, env.createEvalContext(scope))) !== true) continue;
12855
12905
  candidates.push({ row, scope });
12856
12906
  }
12857
12907
  }
12858
- const returningRows = [];
12859
- let changes = 0;
12860
- for (const { row, scope } of candidates) {
12861
- const ctx = env.createEvalContext(scope);
12862
- const updates = evaluateSet(stmt.set, table, ctx);
12863
- const newValues = mergedValues(table, row, updates);
12864
- const updatedColumns = new Set(
12865
- [...updates.keys()].map((key) => {
12866
- const column = table.columns.find((item) => normalizeColumnName(item.name) === key);
12867
- return column?.name ?? key;
12868
- })
12869
- );
12870
- if (fireUpdateTriggers("BEFORE", table, row, newValues, updatedColumns, env) === "ignore") continue;
12871
- try {
12872
- const updated = updateOne(table, row, updates, env);
12873
- fireUpdateTriggers("AFTER", table, row, updated.row.values, updatedColumns, env);
12874
- changes += 1 + updated.cascaded;
12875
- if (stmt.returning.length)
12876
- returningRows.push(projectReturning(stmt.returning, scopeFor(table, updated.row, alias, env), env));
12877
- } catch (error) {
12878
- if (stmt.or === "ignore" && error instanceof SqliteError && error.category.startsWith("constraint")) continue;
12879
- throw error;
12908
+ const applyUpdates = () => {
12909
+ let table = env.state.getWritableTable(stmt.table);
12910
+ const returningRows = [];
12911
+ let changes = 0;
12912
+ for (const { row, scope } of candidates) {
12913
+ const ctx = env.createEvalContext(scope);
12914
+ const updates = evaluateSet(stmt.set, table, ctx);
12915
+ const newValues = mergedValues(table, row, updates);
12916
+ const updatedColumns = new Set(
12917
+ [...updates.keys()].map((key) => {
12918
+ const column = table.columns.find((item) => normalizeColumnName(item.name) === key);
12919
+ return column?.name ?? key;
12920
+ })
12921
+ );
12922
+ if (fireUpdateTriggers("BEFORE", table, row, newValues, updatedColumns, env) === "ignore") continue;
12923
+ try {
12924
+ if (stmt.or === "replace") {
12925
+ for (const conflict of conflictingRows(table, newValues, env)) {
12926
+ if (conflict.rowid === row.rowid) continue;
12927
+ removeOne(table, conflict, env);
12928
+ table = env.state.getWritableTable(stmt.table);
12929
+ }
12930
+ }
12931
+ const updated = updateOne(table, row, updates, env);
12932
+ table = env.state.getWritableTable(stmt.table);
12933
+ fireUpdateTriggers("AFTER", table, row, updated.row.values, updatedColumns, env);
12934
+ changes += 1 + updated.cascaded;
12935
+ if (stmt.returning.length)
12936
+ returningRows.push(projectReturning(stmt.returning, scopeFor(table, updated.row, alias, env), env));
12937
+ } catch (error) {
12938
+ if (stmt.or === "ignore" && error instanceof SqliteError && error.category.startsWith("constraint") && error.category !== "constraint_foreign")
12939
+ continue;
12940
+ throw error;
12941
+ }
12880
12942
  }
12943
+ const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
12944
+ return valuesToResult(
12945
+ returningNames(stmt.returning, table),
12946
+ returningRows,
12947
+ reportedChanges,
12948
+ env.state.lastInsertRowid
12949
+ );
12950
+ };
12951
+ if (updateAbortResolution(stmt.or) === "abort" && candidates.length > 1) {
12952
+ return withStatementAtomicity(env, "abort", applyUpdates);
12881
12953
  }
12882
- const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
12883
- return valuesToResult(
12884
- returningNames(stmt.returning, table),
12885
- returningRows,
12886
- reportedChanges,
12887
- env.state.lastInsertRowid
12888
- );
12954
+ return applyUpdates();
12889
12955
  }
12890
12956
  function executeDelete(stmt, env) {
12891
- return withDmlCtes(stmt.with, env, () => executeDeleteCore(stmt, env));
12957
+ try {
12958
+ return withDmlCtes(stmt.with, env, () => executeDeleteCore(stmt, env));
12959
+ } catch (error) {
12960
+ handleConflictRollback(stmt.or === "rollback", error, env);
12961
+ throw error;
12962
+ }
12892
12963
  }
12893
12964
  function executeDeleteCore(stmt, env) {
12894
12965
  if (env.state.isVirtualTable(stmt.table)) {
@@ -12897,30 +12968,49 @@ function executeDeleteCore(stmt, env) {
12897
12968
  const totalBefore = env.state.totalChanges;
12898
12969
  const view = writableView(stmt.table, "DELETE", env);
12899
12970
  if (view) return executeViewDelete(stmt, view, env, totalBefore);
12900
- const table = env.state.getWritableTable(stmt.table);
12971
+ const scanTable = env.state.getTable(stmt.table);
12901
12972
  const selectedSource = stmt.where === null ? null : tryIndexedTableRows({ type: "table", schema: null, name: stmt.table, alias: stmt.alias }, stmt.where, env);
12902
- const selected = [...selectedSource ? selectedSource.rows : table.scan()].filter((row) => {
12973
+ const selected = [...selectedSource ? selectedSource.rows : scanTable.scan()].filter((row) => {
12903
12974
  if (!stmt.where) return true;
12904
- return isTruthySql(evalExpr(stmt.where, env.createEvalContext(scopeFor(table, row, stmt.alias ?? stmt.table, env)))) === true;
12975
+ return isTruthySql(
12976
+ evalExpr(stmt.where, env.createEvalContext(scopeFor(scanTable, row, stmt.alias ?? stmt.table, env)))
12977
+ ) === true;
12905
12978
  });
12906
- const returningRows = [];
12907
- let changes = 0;
12908
- for (const row of selected) {
12909
- if (fireDeleteTriggers("BEFORE", table, row, env) === "ignore") continue;
12910
- if (stmt.returning.length)
12911
- returningRows.push(projectReturning(stmt.returning, scopeFor(table, row, stmt.alias ?? stmt.table, env), env));
12912
- changes += applyReferentialDelete(table, row, env);
12913
- removeOne(table, row, env);
12914
- fireDeleteTriggers("AFTER", table, row, env);
12915
- changes++;
12979
+ const applyDeletes = () => {
12980
+ let table = env.state.getWritableTable(stmt.table);
12981
+ const returningRows = [];
12982
+ let changes = 0;
12983
+ for (const row of selected) {
12984
+ try {
12985
+ if (fireDeleteTriggers("BEFORE", table, row, env) === "ignore") continue;
12986
+ if (stmt.returning.length)
12987
+ returningRows.push(
12988
+ projectReturning(stmt.returning, scopeFor(table, row, stmt.alias ?? stmt.table, env), env)
12989
+ );
12990
+ changes += applyReferentialDelete(table, row, env);
12991
+ table = env.state.getWritableTable(stmt.table);
12992
+ removeOne(table, row, env);
12993
+ table = env.state.getWritableTable(stmt.table);
12994
+ fireDeleteTriggers("AFTER", table, row, env);
12995
+ changes++;
12996
+ } catch (error) {
12997
+ if (stmt.or === "ignore" && error instanceof SqliteError && error.category.startsWith("constraint") && error.category !== "constraint_foreign")
12998
+ continue;
12999
+ throw error;
13000
+ }
13001
+ }
13002
+ const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
13003
+ return valuesToResult(
13004
+ returningNames(stmt.returning, table),
13005
+ returningRows,
13006
+ reportedChanges,
13007
+ env.state.lastInsertRowid
13008
+ );
13009
+ };
13010
+ if (deleteAbortResolution(stmt.or) === "abort" && selected.length > 1) {
13011
+ return withStatementAtomicity(env, "abort", applyDeletes);
12916
13012
  }
12917
- const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
12918
- return valuesToResult(
12919
- returningNames(stmt.returning, table),
12920
- returningRows,
12921
- reportedChanges,
12922
- env.state.lastInsertRowid
12923
- );
13013
+ return applyDeletes();
12924
13014
  }
12925
13015
  function withDmlCtes(withClause, env, execute) {
12926
13016
  if (!withClause) return execute();
@@ -12938,6 +13028,40 @@ function handleConflictRollback(rollback, error, env) {
12938
13028
  env.transactions.rollback();
12939
13029
  }
12940
13030
  }
13031
+ var statementSavepointSeq = 0;
13032
+ var statementAtomicityDepth = 0;
13033
+ function withStatementAtomicity(env, resolution, run) {
13034
+ if (resolution === "fail") return run();
13035
+ if (statementAtomicityDepth > 0) return run();
13036
+ const name = `__mem_stmt_${++statementSavepointSeq}`;
13037
+ statementAtomicityDepth++;
13038
+ env.transactions.savepoint(name);
13039
+ try {
13040
+ const result = run();
13041
+ env.transactions.release(name);
13042
+ return result;
13043
+ } catch (error) {
13044
+ if (env.transactions.inTransaction) {
13045
+ try {
13046
+ env.transactions.rollback(name);
13047
+ env.transactions.release(name);
13048
+ } catch {
13049
+ }
13050
+ }
13051
+ throw error;
13052
+ } finally {
13053
+ statementAtomicityDepth--;
13054
+ }
13055
+ }
13056
+ function insertAbortResolution(mode) {
13057
+ return mode === "insert_or_fail" ? "fail" : "abort";
13058
+ }
13059
+ function updateAbortResolution(or) {
13060
+ return or === "fail" ? "fail" : "abort";
13061
+ }
13062
+ function deleteAbortResolution(or) {
13063
+ return or === "fail" ? "fail" : "abort";
13064
+ }
12941
13065
  function writableView(name, event, env) {
12942
13066
  const { schema, bare } = splitQualifiedName(name);
12943
13067
  const db = env.state.databaseForSchema(schema, name);
@@ -12947,7 +13071,6 @@ function writableView(name, event, env) {
12947
13071
  (trigger) => trigger.tableName.toLowerCase() === bare.toLowerCase() && trigger.event === event && trigger.timing === "INSTEAD"
12948
13072
  );
12949
13073
  if (!hasInsteadOf) {
12950
- env.state.getWritableTable(name);
12951
13074
  throw new SqliteError(`cannot modify ${bare} because it is a view`, "other");
12952
13075
  }
12953
13076
  const names = view.columns ?? executeSelect2(view.select, env).columns;
@@ -13064,6 +13187,7 @@ function evaluateSet(items, table, ctx) {
13064
13187
  return updates;
13065
13188
  }
13066
13189
  function updateOne(table, row, updates, env) {
13190
+ table = env.state.ensureWritableTable(table);
13067
13191
  const before = { rowid: row.rowid, values: new Map(row.values) };
13068
13192
  let after;
13069
13193
  let indexesAdded = false;
@@ -13135,6 +13259,7 @@ function removeIndexes(table, row, env) {
13135
13259
  }
13136
13260
  }
13137
13261
  function removeOne(table, row, env) {
13262
+ table = env.state.ensureWritableTable(table);
13138
13263
  removeIndexes(table, row, env);
13139
13264
  table.delete(row.rowid);
13140
13265
  }
@@ -13325,10 +13450,13 @@ function applyReferentialDelete(parent, row, env) {
13325
13450
  if (!env.state.foreignKeysEnabled) return 0;
13326
13451
  let changes = 0;
13327
13452
  const parentPk = parent.columns.filter((column) => column.primaryKey).map((column) => column.name);
13328
- for (const child of env.state.tables.values()) {
13453
+ for (const childKey of [...env.state.tables.keys()]) {
13454
+ let child = env.state.tables.get(childKey);
13455
+ if (!child) continue;
13329
13456
  for (const constraint of child.constraints) {
13330
13457
  if (constraint.type !== "foreign_key" || constraint.refTable.toLowerCase() !== parent.name.toLowerCase())
13331
13458
  continue;
13459
+ child = env.state.ensureWritableTable(child);
13332
13460
  const referenced = constraint.refColumns ?? parentPk;
13333
13461
  const matches = [...child.scan()].filter(
13334
13462
  (candidate) => !(child === parent && candidate.rowid === row.rowid) && foreignKeyMatches(constraint.columns, candidate, referenced, row)
@@ -13365,10 +13493,13 @@ function applyReferentialUpdate(parent, before, after, env) {
13365
13493
  if (!env.state.foreignKeysEnabled) return 0;
13366
13494
  let changes = 0;
13367
13495
  const parentPk = parent.columns.filter((column) => column.primaryKey).map((column) => column.name);
13368
- for (const child of env.state.tables.values()) {
13496
+ for (const childKey of [...env.state.tables.keys()]) {
13497
+ let child = env.state.tables.get(childKey);
13498
+ if (!child) continue;
13369
13499
  for (const constraint of child.constraints) {
13370
13500
  if (constraint.type !== "foreign_key" || constraint.refTable.toLowerCase() !== parent.name.toLowerCase())
13371
13501
  continue;
13502
+ child = env.state.ensureWritableTable(child);
13372
13503
  const referenced = constraint.refColumns ?? parentPk;
13373
13504
  const oldValues = referenced.map((name) => before.values.get(normalizeColumnName(name)) ?? null);
13374
13505
  const newValues = referenced.map((name) => after.values.get(normalizeColumnName(name)) ?? null);