@crvouga/sqlite-mem 1.6.0 → 1.7.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";
@@ -8402,6 +8422,7 @@ var DatabaseState = class _DatabaseState {
8402
8422
  refColumns: constraint.columns,
8403
8423
  onDelete: constraint.onDelete,
8404
8424
  onUpdate: constraint.onUpdate,
8425
+ match: constraint.match,
8405
8426
  name: null,
8406
8427
  deferrable: constraint.deferrable,
8407
8428
  initiallyDeferred: constraint.initiallyDeferred
@@ -12588,7 +12609,7 @@ function executeInsertCore(stmt, env) {
12588
12609
  if (view) return executeViewInsert(stmt, view, env, totalBefore);
12589
12610
  const fast = tryFastInsert(stmt, env);
12590
12611
  if (fast) return fast;
12591
- const table = env.state.getWritableTable(stmt.table);
12612
+ let table = env.state.getWritableTable(stmt.table);
12592
12613
  const columnNames = stmt.columns ?? table.columns.map((column) => column.name);
12593
12614
  const rowidIndexes = columnNames.map((name, index) => isRowidName3(name) ? index : -1).filter((index) => index >= 0);
12594
12615
  if (rowidIndexes.length > 1) throw new SqliteError("duplicate column name: rowid", "other");
@@ -12624,116 +12645,141 @@ function executeInsertCore(stmt, env) {
12624
12645
  env.state.recordChange(changes, last);
12625
12646
  return emptyResult(changes, last);
12626
12647
  }
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));
12648
+ const abortMode = insertAbortResolution(stmt.mode) === "abort";
12649
+ const undoInserted = [];
12650
+ const undoRemoved = [];
12651
+ const undoStatement = () => {
12652
+ if (!abortMode) return;
12653
+ for (let i = undoInserted.length - 1; i >= 0; i--) {
12654
+ const row = table.rows.get(undoInserted[i]);
12655
+ if (row) removeOne(table, row, env);
12656
+ }
12657
+ for (let i = undoRemoved.length - 1; i >= 0; i--) {
12658
+ const row = undoRemoved[i];
12659
+ table = env.state.ensureWritableTable(table);
12660
+ table.rows.set(row.rowid, row);
12661
+ if (table.indexes.length > 0) addIndexes(table, row, env);
12639
12662
  }
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);
12663
+ };
12664
+ try {
12665
+ for (const source of sourceRows) {
12666
+ if (source.length !== columnNames.length)
12667
+ throw new SqliteError(
12668
+ stmt.columns ? `${source.length} values for ${columnNames.length} columns` : `table ${table.name} has ${columnNames.length} columns but ${source.length} values were supplied`,
12669
+ "other"
12670
+ );
12671
+ const values = /* @__PURE__ */ new Map();
12672
+ for (const column of table.columns) {
12673
+ if (column.generated) continue;
12674
+ const suppliedIndex = suppliedIndexes[table.columns.indexOf(column)] ?? -1;
12675
+ const value = suppliedIndex >= 0 ? source[suppliedIndex] ?? null : column.defaultExpr ? evalExpr(column.defaultExpr, env.createEvalContext()) : null;
12676
+ values.set(column.nameLower ?? column.name.toLowerCase(), storeColumnValue(table, column, value));
12665
12677
  }
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) {
12678
+ for (const column of table.columns) {
12679
+ if (!column.generated) continue;
12680
+ if (columnNames.some((name) => name.toLowerCase() === column.name.toLowerCase())) {
12681
+ throw new SqliteError(`cannot INSERT into generated column "${column.name}"`, "misuse");
12682
+ }
12683
+ if (column.generated.stored) {
12684
+ const genCtx = env.createEvalContext({
12685
+ cells: table.columns.filter((c) => !c.generated || c === column).flatMap((c) => {
12686
+ if (c.generated && c !== column) return [];
12687
+ if (c === column) return [];
12688
+ return [
12689
+ {
12690
+ table: table.name,
12691
+ name: c.name,
12692
+ value: values.get(normalizeColumnName(c.name)) ?? null,
12693
+ affinity: c.affinity,
12694
+ collate: c.collate
12695
+ }
12696
+ ];
12697
+ })
12698
+ });
12699
+ const computed = evalExpr(column.generated.expr, genCtx);
12700
+ values.set(normalizeColumnName(column.name), storeColumnValue(table, column, computed));
12701
+ } else {
12702
+ values.set(normalizeColumnName(column.name), null);
12703
+ }
12704
+ }
12705
+ if (fireInsertTriggers("BEFORE", table, values, null, env) === "ignore") continue;
12706
+ const conflicts = conflictingRows(table, values, env);
12707
+ if (conflicts.length > 0) {
12708
+ if (stmt.upsert) {
12709
+ const targetConflicts = conflictsForUpsert(table, values, env, stmt.upsert);
12710
+ if (targetConflicts.length === 0) {
12711
+ throw new SqliteError(
12712
+ `UNIQUE constraint failed: ${table.name}`,
12713
+ "constraint_unique",
12714
+ "SQLITE_CONSTRAINT_UNIQUE"
12715
+ );
12716
+ }
12717
+ if (stmt.upsert.action === "nothing") continue;
12718
+ const row = targetConflicts[0];
12719
+ const scope = scopeFor(table, row, stmt.table, env);
12720
+ const excluded = {
12721
+ cells: table.columns.map((column) => ({
12722
+ table: "excluded",
12723
+ name: column.name,
12724
+ value: values.get(normalizeColumnName(column.name)) ?? null
12725
+ }))
12726
+ };
12727
+ const merged = { cells: [...scope.cells, ...excluded.cells], rowid: row.rowid, sourceTable: table.name };
12728
+ const ctx = env.createEvalContext(merged);
12729
+ if (stmt.upsert.action.where && isTruthySql(evalExpr(stmt.upsert.action.where, ctx)) !== true) continue;
12730
+ const updates = evaluateSet(stmt.upsert.action.set, table, ctx);
12731
+ const updated = updateOne(table, row, updates, env);
12732
+ changes += 1 + updated.cascaded;
12733
+ if (stmt.returning.length)
12734
+ returningRows.push(projectReturning(stmt.returning, scopeFor(table, updated.row, stmt.table, env), env));
12735
+ continue;
12736
+ }
12737
+ const mode = stmt.mode;
12738
+ if (mode === "insert_or_ignore") continue;
12739
+ if (mode === "replace" || mode === "insert_or_replace") {
12740
+ for (const row of conflicts) {
12741
+ if (abortMode) undoRemoved.push(cloneRow(row));
12742
+ removeOne(table, row, env);
12743
+ }
12744
+ } else {
12673
12745
  throw new SqliteError(
12674
12746
  `UNIQUE constraint failed: ${table.name}`,
12675
12747
  "constraint_unique",
12676
12748
  "SQLITE_CONSTRAINT_UNIQUE"
12677
12749
  );
12678
12750
  }
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
12751
  }
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;
12752
+ let insertedRowid;
12753
+ try {
12754
+ const suppliedRowid = rowidIndexes.length ? asExplicitRowid(source[rowidIndexes[0]] ?? null) : void 0;
12755
+ const rowid = table.insert({ values, rowid: suppliedRowid }, { prepared: true });
12756
+ insertedRowid = rowid;
12757
+ const row = table.rows.get(rowid);
12758
+ validateRow(table, row, env);
12759
+ if (table.indexes.length > 0) addIndexes(table, row, env);
12760
+ if (env.state.foreignKeysEnabled) checkForeignKeys(table, row, env);
12761
+ if (!table.withoutRowid) {
12762
+ last = rowid;
12763
+ env.state.lastInsertRowid = rowid;
12764
+ }
12765
+ fireInsertTriggers("AFTER", table, row.values, null, env);
12766
+ changes++;
12767
+ if (abortMode) undoInserted.push(rowid);
12768
+ if (stmt.returning.length)
12769
+ returningRows.push(projectReturning(stmt.returning, scopeFor(table, row, stmt.table, env), env));
12770
+ } catch (error) {
12771
+ if (insertedRowid !== void 0) {
12772
+ const inserted = table.rows.get(insertedRowid);
12773
+ if (inserted) removeOne(table, inserted, env);
12774
+ }
12775
+ if (stmt.mode === "insert_or_ignore" && error instanceof SqliteError && error.category.startsWith("constraint") && error.category !== "constraint_foreign")
12776
+ continue;
12777
+ throw error;
12723
12778
  }
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
12779
  }
12780
+ } catch (error) {
12781
+ undoStatement();
12782
+ throw error;
12737
12783
  }
12738
12784
  const reportedChanges = finalizeDmlChanges(totalBefore, changes, env, last);
12739
12785
  if (stmt.returning.length === 0) return emptyResult(reportedChanges, last);
@@ -12746,8 +12792,6 @@ function evaluateInsertSource(stmt, env) {
12746
12792
  return stmt.values.map(
12747
12793
  (items) => items.map((expr) => {
12748
12794
  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
12795
  return evalExpr(expr, ctx);
12752
12796
  })
12753
12797
  );
@@ -12805,7 +12849,7 @@ function buildFastInsertPlan(stmt, env) {
12805
12849
  const name = expr.name;
12806
12850
  slots.push({ key, affinity: column.affinity, read: (exec) => exec.getBoundParameter(name) });
12807
12851
  } else if (expr.type === "literal") {
12808
- const value = expr.value;
12852
+ const value = expr.forceReal && typeof expr.value === "number" ? asSqlReal(expr.value) : expr.value;
12809
12853
  slots.push({ key, affinity: column.affinity, read: () => value });
12810
12854
  } else if (expr.type === "null") {
12811
12855
  slots.push({ key, affinity: column.affinity, read: () => null });
@@ -12828,13 +12872,13 @@ function executeUpdateCore(stmt, env) {
12828
12872
  const totalBefore = env.state.totalChanges;
12829
12873
  const view = writableView(stmt.table, "UPDATE", env);
12830
12874
  if (view) return executeViewUpdate(stmt, view, env, totalBefore);
12831
- const table = env.state.getWritableTable(stmt.table);
12875
+ const scanTable = env.state.getTable(stmt.table);
12832
12876
  const alias = stmt.alias ?? stmt.table;
12833
12877
  const candidates = [];
12834
12878
  if (stmt.from) {
12835
12879
  const fromRows = scanFrom(stmt.from, env);
12836
- for (const row of table.scan()) {
12837
- const targetScope = scopeFor(table, row, alias, env);
12880
+ for (const row of scanTable.scan()) {
12881
+ const targetScope = scopeFor(scanTable, row, alias, env);
12838
12882
  let matchedScope = null;
12839
12883
  for (const fromRow of fromRows) {
12840
12884
  const joined = {
@@ -12848,47 +12892,69 @@ function executeUpdateCore(stmt, env) {
12848
12892
  }
12849
12893
  } else {
12850
12894
  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();
12895
+ const scanRows = indexed ? indexed.rows : scanTable.scan();
12852
12896
  for (const row of scanRows) {
12853
- const scope = scopeFor(table, row, alias, env);
12897
+ const scope = scopeFor(scanTable, row, alias, env);
12854
12898
  if (stmt.where && isTruthySql(evalExpr(stmt.where, env.createEvalContext(scope))) !== true) continue;
12855
12899
  candidates.push({ row, scope });
12856
12900
  }
12857
12901
  }
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;
12902
+ const applyUpdates = () => {
12903
+ let table = env.state.getWritableTable(stmt.table);
12904
+ const returningRows = [];
12905
+ let changes = 0;
12906
+ for (const { row, scope } of candidates) {
12907
+ const ctx = env.createEvalContext(scope);
12908
+ const updates = evaluateSet(stmt.set, table, ctx);
12909
+ const newValues = mergedValues(table, row, updates);
12910
+ const updatedColumns = new Set(
12911
+ [...updates.keys()].map((key) => {
12912
+ const column = table.columns.find((item) => normalizeColumnName(item.name) === key);
12913
+ return column?.name ?? key;
12914
+ })
12915
+ );
12916
+ if (fireUpdateTriggers("BEFORE", table, row, newValues, updatedColumns, env) === "ignore") continue;
12917
+ try {
12918
+ if (stmt.or === "replace") {
12919
+ for (const conflict of conflictingRows(table, newValues, env)) {
12920
+ if (conflict.rowid === row.rowid) continue;
12921
+ removeOne(table, conflict, env);
12922
+ table = env.state.getWritableTable(stmt.table);
12923
+ changes++;
12924
+ }
12925
+ }
12926
+ const updated = updateOne(table, row, updates, env);
12927
+ table = env.state.getWritableTable(stmt.table);
12928
+ fireUpdateTriggers("AFTER", table, row, updated.row.values, updatedColumns, env);
12929
+ changes += 1 + updated.cascaded;
12930
+ if (stmt.returning.length)
12931
+ returningRows.push(projectReturning(stmt.returning, scopeFor(table, updated.row, alias, env), env));
12932
+ } catch (error) {
12933
+ if (stmt.or === "ignore" && error instanceof SqliteError && error.category.startsWith("constraint") && error.category !== "constraint_foreign")
12934
+ continue;
12935
+ throw error;
12936
+ }
12880
12937
  }
12938
+ const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
12939
+ return valuesToResult(
12940
+ returningNames(stmt.returning, table),
12941
+ returningRows,
12942
+ reportedChanges,
12943
+ env.state.lastInsertRowid
12944
+ );
12945
+ };
12946
+ if (updateAbortResolution(stmt.or) === "abort" && candidates.length > 1) {
12947
+ return withStatementAtomicity(env, "abort", applyUpdates);
12881
12948
  }
12882
- const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
12883
- return valuesToResult(
12884
- returningNames(stmt.returning, table),
12885
- returningRows,
12886
- reportedChanges,
12887
- env.state.lastInsertRowid
12888
- );
12949
+ return applyUpdates();
12889
12950
  }
12890
12951
  function executeDelete(stmt, env) {
12891
- return withDmlCtes(stmt.with, env, () => executeDeleteCore(stmt, env));
12952
+ try {
12953
+ return withDmlCtes(stmt.with, env, () => executeDeleteCore(stmt, env));
12954
+ } catch (error) {
12955
+ handleConflictRollback(stmt.or === "rollback", error, env);
12956
+ throw error;
12957
+ }
12892
12958
  }
12893
12959
  function executeDeleteCore(stmt, env) {
12894
12960
  if (env.state.isVirtualTable(stmt.table)) {
@@ -12897,30 +12963,49 @@ function executeDeleteCore(stmt, env) {
12897
12963
  const totalBefore = env.state.totalChanges;
12898
12964
  const view = writableView(stmt.table, "DELETE", env);
12899
12965
  if (view) return executeViewDelete(stmt, view, env, totalBefore);
12900
- const table = env.state.getWritableTable(stmt.table);
12966
+ const scanTable = env.state.getTable(stmt.table);
12901
12967
  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) => {
12968
+ const selected = [...selectedSource ? selectedSource.rows : scanTable.scan()].filter((row) => {
12903
12969
  if (!stmt.where) return true;
12904
- return isTruthySql(evalExpr(stmt.where, env.createEvalContext(scopeFor(table, row, stmt.alias ?? stmt.table, env)))) === true;
12970
+ return isTruthySql(
12971
+ evalExpr(stmt.where, env.createEvalContext(scopeFor(scanTable, row, stmt.alias ?? stmt.table, env)))
12972
+ ) === true;
12905
12973
  });
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++;
12974
+ const applyDeletes = () => {
12975
+ let table = env.state.getWritableTable(stmt.table);
12976
+ const returningRows = [];
12977
+ let changes = 0;
12978
+ for (const row of selected) {
12979
+ try {
12980
+ if (fireDeleteTriggers("BEFORE", table, row, env) === "ignore") continue;
12981
+ if (stmt.returning.length)
12982
+ returningRows.push(
12983
+ projectReturning(stmt.returning, scopeFor(table, row, stmt.alias ?? stmt.table, env), env)
12984
+ );
12985
+ changes += applyReferentialDelete(table, row, env);
12986
+ table = env.state.getWritableTable(stmt.table);
12987
+ removeOne(table, row, env);
12988
+ table = env.state.getWritableTable(stmt.table);
12989
+ fireDeleteTriggers("AFTER", table, row, env);
12990
+ changes++;
12991
+ } catch (error) {
12992
+ if (stmt.or === "ignore" && error instanceof SqliteError && error.category.startsWith("constraint") && error.category !== "constraint_foreign")
12993
+ continue;
12994
+ throw error;
12995
+ }
12996
+ }
12997
+ const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
12998
+ return valuesToResult(
12999
+ returningNames(stmt.returning, table),
13000
+ returningRows,
13001
+ reportedChanges,
13002
+ env.state.lastInsertRowid
13003
+ );
13004
+ };
13005
+ if (deleteAbortResolution(stmt.or) === "abort" && selected.length > 1) {
13006
+ return withStatementAtomicity(env, "abort", applyDeletes);
12916
13007
  }
12917
- const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
12918
- return valuesToResult(
12919
- returningNames(stmt.returning, table),
12920
- returningRows,
12921
- reportedChanges,
12922
- env.state.lastInsertRowid
12923
- );
13008
+ return applyDeletes();
12924
13009
  }
12925
13010
  function withDmlCtes(withClause, env, execute) {
12926
13011
  if (!withClause) return execute();
@@ -12938,6 +13023,40 @@ function handleConflictRollback(rollback, error, env) {
12938
13023
  env.transactions.rollback();
12939
13024
  }
12940
13025
  }
13026
+ var statementSavepointSeq = 0;
13027
+ var statementAtomicityDepth = 0;
13028
+ function withStatementAtomicity(env, resolution, run) {
13029
+ if (resolution === "fail") return run();
13030
+ if (statementAtomicityDepth > 0) return run();
13031
+ const name = `__mem_stmt_${++statementSavepointSeq}`;
13032
+ statementAtomicityDepth++;
13033
+ env.transactions.savepoint(name);
13034
+ try {
13035
+ const result = run();
13036
+ env.transactions.release(name);
13037
+ return result;
13038
+ } catch (error) {
13039
+ if (env.transactions.inTransaction) {
13040
+ try {
13041
+ env.transactions.rollback(name);
13042
+ env.transactions.release(name);
13043
+ } catch {
13044
+ }
13045
+ }
13046
+ throw error;
13047
+ } finally {
13048
+ statementAtomicityDepth--;
13049
+ }
13050
+ }
13051
+ function insertAbortResolution(mode) {
13052
+ return mode === "insert_or_fail" ? "fail" : "abort";
13053
+ }
13054
+ function updateAbortResolution(or) {
13055
+ return or === "fail" ? "fail" : "abort";
13056
+ }
13057
+ function deleteAbortResolution(or) {
13058
+ return or === "fail" ? "fail" : "abort";
13059
+ }
12941
13060
  function writableView(name, event, env) {
12942
13061
  const { schema, bare } = splitQualifiedName(name);
12943
13062
  const db = env.state.databaseForSchema(schema, name);
@@ -12947,7 +13066,6 @@ function writableView(name, event, env) {
12947
13066
  (trigger) => trigger.tableName.toLowerCase() === bare.toLowerCase() && trigger.event === event && trigger.timing === "INSTEAD"
12948
13067
  );
12949
13068
  if (!hasInsteadOf) {
12950
- env.state.getWritableTable(name);
12951
13069
  throw new SqliteError(`cannot modify ${bare} because it is a view`, "other");
12952
13070
  }
12953
13071
  const names = view.columns ?? executeSelect2(view.select, env).columns;
@@ -13064,6 +13182,7 @@ function evaluateSet(items, table, ctx) {
13064
13182
  return updates;
13065
13183
  }
13066
13184
  function updateOne(table, row, updates, env) {
13185
+ table = env.state.ensureWritableTable(table);
13067
13186
  const before = { rowid: row.rowid, values: new Map(row.values) };
13068
13187
  let after;
13069
13188
  let indexesAdded = false;
@@ -13135,6 +13254,7 @@ function removeIndexes(table, row, env) {
13135
13254
  }
13136
13255
  }
13137
13256
  function removeOne(table, row, env) {
13257
+ table = env.state.ensureWritableTable(table);
13138
13258
  removeIndexes(table, row, env);
13139
13259
  table.delete(row.rowid);
13140
13260
  }
@@ -13325,10 +13445,13 @@ function applyReferentialDelete(parent, row, env) {
13325
13445
  if (!env.state.foreignKeysEnabled) return 0;
13326
13446
  let changes = 0;
13327
13447
  const parentPk = parent.columns.filter((column) => column.primaryKey).map((column) => column.name);
13328
- for (const child of env.state.tables.values()) {
13448
+ for (const childKey of [...env.state.tables.keys()]) {
13449
+ let child = env.state.tables.get(childKey);
13450
+ if (!child) continue;
13329
13451
  for (const constraint of child.constraints) {
13330
13452
  if (constraint.type !== "foreign_key" || constraint.refTable.toLowerCase() !== parent.name.toLowerCase())
13331
13453
  continue;
13454
+ child = env.state.ensureWritableTable(child);
13332
13455
  const referenced = constraint.refColumns ?? parentPk;
13333
13456
  const matches = [...child.scan()].filter(
13334
13457
  (candidate) => !(child === parent && candidate.rowid === row.rowid) && foreignKeyMatches(constraint.columns, candidate, referenced, row)
@@ -13365,10 +13488,13 @@ function applyReferentialUpdate(parent, before, after, env) {
13365
13488
  if (!env.state.foreignKeysEnabled) return 0;
13366
13489
  let changes = 0;
13367
13490
  const parentPk = parent.columns.filter((column) => column.primaryKey).map((column) => column.name);
13368
- for (const child of env.state.tables.values()) {
13491
+ for (const childKey of [...env.state.tables.keys()]) {
13492
+ let child = env.state.tables.get(childKey);
13493
+ if (!child) continue;
13369
13494
  for (const constraint of child.constraints) {
13370
13495
  if (constraint.type !== "foreign_key" || constraint.refTable.toLowerCase() !== parent.name.toLowerCase())
13371
13496
  continue;
13497
+ child = env.state.ensureWritableTable(child);
13372
13498
  const referenced = constraint.refColumns ?? parentPk;
13373
13499
  const oldValues = referenced.map((name) => before.values.get(normalizeColumnName(name)) ?? null);
13374
13500
  const newValues = referenced.map((name) => after.values.get(normalizeColumnName(name)) ?? null);