@crvouga/sqlite-mem 1.5.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,12 +1680,24 @@ var Parser = class {
1677
1680
  parseFkActions() {
1678
1681
  let onDelete = null;
1679
1682
  let onUpdate = null;
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();
1680
1693
  while (this.at("ON")) {
1681
1694
  if (this.peek().kind === "DELETE") onDelete = this.parseFkAction("DELETE");
1682
1695
  else if (this.peek().kind === "UPDATE") onUpdate = this.parseFkAction("UPDATE");
1683
1696
  else this.syntaxError("expected ON DELETE or ON UPDATE");
1697
+ readMatch();
1684
1698
  }
1685
- return { onDelete, onUpdate };
1699
+ readMatch();
1700
+ return { onDelete, onUpdate, match };
1686
1701
  }
1687
1702
  parseDeferrable() {
1688
1703
  let deferrable = false;
@@ -2557,32 +2572,46 @@ function applyAffinity(value, affinity) {
2557
2572
  if (value === null) return null;
2558
2573
  switch (affinity) {
2559
2574
  case "TEXT":
2560
- if (value instanceof Uint8Array) return utf8Decode(value);
2575
+ if (value instanceof Uint8Array) return value;
2561
2576
  if (value instanceof SqlJsonText) return value.value;
2562
2577
  if (typeof value === "string") return value;
2563
- 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();
2564
2581
  return String(value);
2565
2582
  case "INTEGER": {
2583
+ if (value instanceof Uint8Array) return value;
2566
2584
  const n = coerceToNumber(value);
2567
2585
  if (n === null) return value;
2568
2586
  if (typeof value === "bigint") return value;
2569
2587
  return Number.isInteger(n) && Number.isSafeInteger(n) ? Math.trunc(n) : canonicalizeNumber(n);
2570
2588
  }
2571
2589
  case "REAL": {
2590
+ if (value instanceof Uint8Array) return value;
2572
2591
  const n = coerceToNumber(value);
2573
2592
  return n === null ? value : asSqlReal(n);
2574
2593
  }
2575
2594
  case "NUMERIC": {
2595
+ if (value instanceof Uint8Array) return value;
2576
2596
  const n = coerceToNumber(value);
2577
2597
  if (n === null) return value;
2578
2598
  if (Number.isInteger(n) && Number.isSafeInteger(n)) return Math.trunc(n);
2579
2599
  return canonicalizeNumber(n);
2580
2600
  }
2581
2601
  case "BLOB":
2582
- if (typeof value === "number") return canonicalizeNumber(value);
2583
2602
  return value;
2584
2603
  }
2585
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
+ }
2586
2615
  function applyComparisonAffinity(left, right, leftAffinity, rightAffinity) {
2587
2616
  const leftNumeric = leftAffinity === "INTEGER" || leftAffinity === "REAL" || leftAffinity === "NUMERIC";
2588
2617
  const rightNumeric = rightAffinity === "INTEGER" || rightAffinity === "REAL" || rightAffinity === "NUMERIC";
@@ -8393,6 +8422,7 @@ var DatabaseState = class _DatabaseState {
8393
8422
  refColumns: constraint.columns,
8394
8423
  onDelete: constraint.onDelete,
8395
8424
  onUpdate: constraint.onUpdate,
8425
+ match: constraint.match,
8396
8426
  name: null,
8397
8427
  deferrable: constraint.deferrable,
8398
8428
  initiallyDeferred: constraint.initiallyDeferred
@@ -9899,11 +9929,13 @@ function pragmaTableInfo(args, env, xinfo) {
9899
9929
  const tableName = requireNameArg(args, "table_info");
9900
9930
  const table = env.state.tables.get(tableName.toLowerCase());
9901
9931
  if (!table) return { columns: xinfoColumns(xinfo), rows: [] };
9902
- const rows = table.columns.map((column, cid) => {
9932
+ const visible = xinfo ? table.columns : table.columns.filter((column) => !column.generated);
9933
+ const rows = visible.map((column, cid) => {
9903
9934
  const pkIndex = table.columns.filter((c) => c.primaryKey).findIndex((c) => c.name === column.name);
9904
9935
  const pk = column.primaryKey ? pkIndex >= 0 ? pkIndex + 1 : 1 : 0;
9905
9936
  const dflt = column.defaultExpr ? defaultLiteral(column.defaultExpr) : null;
9906
- const base = [cid, column.name, column.typeName ?? "", column.notNull ? 1 : 0, dflt, pk];
9937
+ const notNull = column.notNull || table.withoutRowid && column.primaryKey ? 1 : 0;
9938
+ const base = [cid, column.name, column.typeName ?? "", notNull, dflt, pk];
9907
9939
  if (xinfo) {
9908
9940
  let hidden = 0;
9909
9941
  if (column.generated && !column.generated.stored) hidden = 2;
@@ -11362,10 +11394,20 @@ function scanFrom(item, env, parent) {
11362
11394
  if (item.joinType === "RIGHT" || item.joinType === "FULL") {
11363
11395
  right.forEach((rhs, rightIndex) => {
11364
11396
  if (matchedRight.has(rightIndex)) return;
11365
- const rightCells = using ? rhs.cells.map(
11366
- (cell) => using.some((name) => name.toLowerCase() === cell.name.toLowerCase()) ? { ...cell, hiddenByUsing: true } : cell
11367
- ) : rhs.cells;
11368
- result.push({ cells: [...nullLeft, ...rightCells] });
11397
+ if (using) {
11398
+ const leftCells = nullLeft.map((cell) => {
11399
+ const shared = using.some((name) => name.toLowerCase() === cell.name.toLowerCase());
11400
+ if (!shared) return cell;
11401
+ const fromRight = rhs.cells.find((c) => c.name.toLowerCase() === cell.name.toLowerCase());
11402
+ return { ...cell, value: fromRight?.value ?? null, hiddenByUsing: false };
11403
+ });
11404
+ const rightOnly = rhs.cells.filter(
11405
+ (cell) => !using.some((name) => name.toLowerCase() === cell.name.toLowerCase())
11406
+ );
11407
+ result.push({ cells: [...leftCells, ...rightOnly] });
11408
+ } else {
11409
+ result.push({ cells: [...nullLeft, ...rhs.cells] });
11410
+ }
11369
11411
  });
11370
11412
  }
11371
11413
  return result;
@@ -12567,7 +12609,7 @@ function executeInsertCore(stmt, env) {
12567
12609
  if (view) return executeViewInsert(stmt, view, env, totalBefore);
12568
12610
  const fast = tryFastInsert(stmt, env);
12569
12611
  if (fast) return fast;
12570
- const table = env.state.getWritableTable(stmt.table);
12612
+ let table = env.state.getWritableTable(stmt.table);
12571
12613
  const columnNames = stmt.columns ?? table.columns.map((column) => column.name);
12572
12614
  const rowidIndexes = columnNames.map((name, index) => isRowidName3(name) ? index : -1).filter((index) => index >= 0);
12573
12615
  if (rowidIndexes.length > 1) throw new SqliteError("duplicate column name: rowid", "other");
@@ -12603,116 +12645,141 @@ function executeInsertCore(stmt, env) {
12603
12645
  env.state.recordChange(changes, last);
12604
12646
  return emptyResult(changes, last);
12605
12647
  }
12606
- for (const source of sourceRows) {
12607
- if (source.length !== columnNames.length)
12608
- throw new SqliteError(
12609
- stmt.columns ? `${source.length} values for ${columnNames.length} columns` : `table ${table.name} has ${columnNames.length} columns but ${source.length} values were supplied`,
12610
- "other"
12611
- );
12612
- const values = /* @__PURE__ */ new Map();
12613
- for (const column of table.columns) {
12614
- if (column.generated) continue;
12615
- const suppliedIndex = suppliedIndexes[table.columns.indexOf(column)] ?? -1;
12616
- const value = suppliedIndex >= 0 ? source[suppliedIndex] ?? null : column.defaultExpr ? evalExpr(column.defaultExpr, env.createEvalContext()) : null;
12617
- 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);
12618
12662
  }
12619
- for (const column of table.columns) {
12620
- if (!column.generated) continue;
12621
- if (columnNames.some((name) => name.toLowerCase() === column.name.toLowerCase())) {
12622
- throw new SqliteError(`cannot INSERT into generated column "${column.name}"`, "misuse");
12623
- }
12624
- if (column.generated.stored) {
12625
- const genCtx = env.createEvalContext({
12626
- cells: table.columns.filter((c) => !c.generated || c === column).flatMap((c) => {
12627
- if (c.generated && c !== column) return [];
12628
- if (c === column) return [];
12629
- return [
12630
- {
12631
- table: table.name,
12632
- name: c.name,
12633
- value: values.get(normalizeColumnName(c.name)) ?? null,
12634
- affinity: c.affinity,
12635
- collate: c.collate
12636
- }
12637
- ];
12638
- })
12639
- });
12640
- const computed = evalExpr(column.generated.expr, genCtx);
12641
- values.set(normalizeColumnName(column.name), storeColumnValue(table, column, computed));
12642
- } else {
12643
- 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));
12644
12677
  }
12645
- }
12646
- if (fireInsertTriggers("BEFORE", table, values, null, env) === "ignore") continue;
12647
- const conflicts = conflictingRows(table, values, env);
12648
- if (conflicts.length > 0) {
12649
- if (stmt.upsert) {
12650
- const targetConflicts = conflictsForUpsert(table, values, env, stmt.upsert);
12651
- 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 {
12652
12745
  throw new SqliteError(
12653
12746
  `UNIQUE constraint failed: ${table.name}`,
12654
12747
  "constraint_unique",
12655
12748
  "SQLITE_CONSTRAINT_UNIQUE"
12656
12749
  );
12657
12750
  }
12658
- if (stmt.upsert.action === "nothing") continue;
12659
- const row = targetConflicts[0];
12660
- const scope = scopeFor(table, row, stmt.table, env);
12661
- const excluded = {
12662
- cells: table.columns.map((column) => ({
12663
- table: "excluded",
12664
- name: column.name,
12665
- value: values.get(normalizeColumnName(column.name)) ?? null
12666
- }))
12667
- };
12668
- const merged = { cells: [...scope.cells, ...excluded.cells], rowid: row.rowid, sourceTable: table.name };
12669
- const ctx = env.createEvalContext(merged);
12670
- if (stmt.upsert.action.where && isTruthySql(evalExpr(stmt.upsert.action.where, ctx)) !== true) continue;
12671
- const updates = evaluateSet(stmt.upsert.action.set, table, ctx);
12672
- const updated = updateOne(table, row, updates, env);
12673
- changes += 1 + updated.cascaded;
12674
- if (stmt.returning.length)
12675
- returningRows.push(projectReturning(stmt.returning, scopeFor(table, updated.row, stmt.table, env), env));
12676
- continue;
12677
12751
  }
12678
- const mode = stmt.mode;
12679
- if (mode === "insert_or_ignore") continue;
12680
- if (mode === "replace" || mode === "insert_or_replace") {
12681
- for (const row of conflicts) removeOne(table, row, env);
12682
- } else {
12683
- throw new SqliteError(
12684
- `UNIQUE constraint failed: ${table.name}`,
12685
- "constraint_unique",
12686
- "SQLITE_CONSTRAINT_UNIQUE"
12687
- );
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;
12688
12778
  }
12689
12779
  }
12690
- let insertedRowid;
12691
- try {
12692
- const suppliedRowid = rowidIndexes.length ? asExplicitRowid(source[rowidIndexes[0]] ?? null) : void 0;
12693
- const rowid = table.insert({ values, rowid: suppliedRowid }, { prepared: true });
12694
- insertedRowid = rowid;
12695
- const row = table.rows.get(rowid);
12696
- validateRow(table, row, env);
12697
- if (table.indexes.length > 0) addIndexes(table, row, env);
12698
- if (env.state.foreignKeysEnabled) checkForeignKeys(table, row, env);
12699
- if (!table.withoutRowid) {
12700
- last = rowid;
12701
- env.state.lastInsertRowid = rowid;
12702
- }
12703
- fireInsertTriggers("AFTER", table, row.values, null, env);
12704
- changes++;
12705
- if (stmt.returning.length)
12706
- returningRows.push(projectReturning(stmt.returning, scopeFor(table, row, stmt.table, env), env));
12707
- } catch (error) {
12708
- if (insertedRowid !== void 0) {
12709
- const inserted = table.rows.get(insertedRowid);
12710
- if (inserted) removeOne(table, inserted, env);
12711
- }
12712
- if (stmt.mode === "insert_or_ignore" && error instanceof SqliteError && error.category.startsWith("constraint"))
12713
- continue;
12714
- throw error;
12715
- }
12780
+ } catch (error) {
12781
+ undoStatement();
12782
+ throw error;
12716
12783
  }
12717
12784
  const reportedChanges = finalizeDmlChanges(totalBefore, changes, env, last);
12718
12785
  if (stmt.returning.length === 0) return emptyResult(reportedChanges, last);
@@ -12725,8 +12792,6 @@ function evaluateInsertSource(stmt, env) {
12725
12792
  return stmt.values.map(
12726
12793
  (items) => items.map((expr) => {
12727
12794
  if (expr.type === "parameter") return env.getBoundParameter(expr.name);
12728
- if (expr.type === "literal") return expr.value;
12729
- if (expr.type === "null") return null;
12730
12795
  return evalExpr(expr, ctx);
12731
12796
  })
12732
12797
  );
@@ -12784,7 +12849,7 @@ function buildFastInsertPlan(stmt, env) {
12784
12849
  const name = expr.name;
12785
12850
  slots.push({ key, affinity: column.affinity, read: (exec) => exec.getBoundParameter(name) });
12786
12851
  } else if (expr.type === "literal") {
12787
- const value = expr.value;
12852
+ const value = expr.forceReal && typeof expr.value === "number" ? asSqlReal(expr.value) : expr.value;
12788
12853
  slots.push({ key, affinity: column.affinity, read: () => value });
12789
12854
  } else if (expr.type === "null") {
12790
12855
  slots.push({ key, affinity: column.affinity, read: () => null });
@@ -12807,13 +12872,13 @@ function executeUpdateCore(stmt, env) {
12807
12872
  const totalBefore = env.state.totalChanges;
12808
12873
  const view = writableView(stmt.table, "UPDATE", env);
12809
12874
  if (view) return executeViewUpdate(stmt, view, env, totalBefore);
12810
- const table = env.state.getWritableTable(stmt.table);
12875
+ const scanTable = env.state.getTable(stmt.table);
12811
12876
  const alias = stmt.alias ?? stmt.table;
12812
12877
  const candidates = [];
12813
12878
  if (stmt.from) {
12814
12879
  const fromRows = scanFrom(stmt.from, env);
12815
- for (const row of table.scan()) {
12816
- const targetScope = scopeFor(table, row, alias, env);
12880
+ for (const row of scanTable.scan()) {
12881
+ const targetScope = scopeFor(scanTable, row, alias, env);
12817
12882
  let matchedScope = null;
12818
12883
  for (const fromRow of fromRows) {
12819
12884
  const joined = {
@@ -12827,47 +12892,69 @@ function executeUpdateCore(stmt, env) {
12827
12892
  }
12828
12893
  } else {
12829
12894
  const indexed = stmt.where === null ? null : tryIndexedTableRows({ type: "table", schema: null, name: stmt.table, alias: stmt.alias }, stmt.where, env);
12830
- const scanRows = indexed ? indexed.rows : table.scan();
12895
+ const scanRows = indexed ? indexed.rows : scanTable.scan();
12831
12896
  for (const row of scanRows) {
12832
- const scope = scopeFor(table, row, alias, env);
12897
+ const scope = scopeFor(scanTable, row, alias, env);
12833
12898
  if (stmt.where && isTruthySql(evalExpr(stmt.where, env.createEvalContext(scope))) !== true) continue;
12834
12899
  candidates.push({ row, scope });
12835
12900
  }
12836
12901
  }
12837
- const returningRows = [];
12838
- let changes = 0;
12839
- for (const { row, scope } of candidates) {
12840
- const ctx = env.createEvalContext(scope);
12841
- const updates = evaluateSet(stmt.set, table, ctx);
12842
- const newValues = mergedValues(table, row, updates);
12843
- const updatedColumns = new Set(
12844
- [...updates.keys()].map((key) => {
12845
- const column = table.columns.find((item) => normalizeColumnName(item.name) === key);
12846
- return column?.name ?? key;
12847
- })
12848
- );
12849
- if (fireUpdateTriggers("BEFORE", table, row, newValues, updatedColumns, env) === "ignore") continue;
12850
- try {
12851
- const updated = updateOne(table, row, updates, env);
12852
- fireUpdateTriggers("AFTER", table, row, updated.row.values, updatedColumns, env);
12853
- changes += 1 + updated.cascaded;
12854
- if (stmt.returning.length)
12855
- returningRows.push(projectReturning(stmt.returning, scopeFor(table, updated.row, alias, env), env));
12856
- } catch (error) {
12857
- if (stmt.or === "ignore" && error instanceof SqliteError && error.category.startsWith("constraint")) continue;
12858
- 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
+ }
12859
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);
12860
12948
  }
12861
- const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
12862
- return valuesToResult(
12863
- returningNames(stmt.returning, table),
12864
- returningRows,
12865
- reportedChanges,
12866
- env.state.lastInsertRowid
12867
- );
12949
+ return applyUpdates();
12868
12950
  }
12869
12951
  function executeDelete(stmt, env) {
12870
- 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
+ }
12871
12958
  }
12872
12959
  function executeDeleteCore(stmt, env) {
12873
12960
  if (env.state.isVirtualTable(stmt.table)) {
@@ -12876,30 +12963,49 @@ function executeDeleteCore(stmt, env) {
12876
12963
  const totalBefore = env.state.totalChanges;
12877
12964
  const view = writableView(stmt.table, "DELETE", env);
12878
12965
  if (view) return executeViewDelete(stmt, view, env, totalBefore);
12879
- const table = env.state.getWritableTable(stmt.table);
12966
+ const scanTable = env.state.getTable(stmt.table);
12880
12967
  const selectedSource = stmt.where === null ? null : tryIndexedTableRows({ type: "table", schema: null, name: stmt.table, alias: stmt.alias }, stmt.where, env);
12881
- const selected = [...selectedSource ? selectedSource.rows : table.scan()].filter((row) => {
12968
+ const selected = [...selectedSource ? selectedSource.rows : scanTable.scan()].filter((row) => {
12882
12969
  if (!stmt.where) return true;
12883
- 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;
12884
12973
  });
12885
- const returningRows = [];
12886
- let changes = 0;
12887
- for (const row of selected) {
12888
- if (fireDeleteTriggers("BEFORE", table, row, env) === "ignore") continue;
12889
- if (stmt.returning.length)
12890
- returningRows.push(projectReturning(stmt.returning, scopeFor(table, row, stmt.alias ?? stmt.table, env), env));
12891
- changes += applyReferentialDelete(table, row, env);
12892
- removeOne(table, row, env);
12893
- fireDeleteTriggers("AFTER", table, row, env);
12894
- 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);
12895
13007
  }
12896
- const reportedChanges = finalizeDmlChanges(totalBefore, changes, env);
12897
- return valuesToResult(
12898
- returningNames(stmt.returning, table),
12899
- returningRows,
12900
- reportedChanges,
12901
- env.state.lastInsertRowid
12902
- );
13008
+ return applyDeletes();
12903
13009
  }
12904
13010
  function withDmlCtes(withClause, env, execute) {
12905
13011
  if (!withClause) return execute();
@@ -12917,6 +13023,40 @@ function handleConflictRollback(rollback, error, env) {
12917
13023
  env.transactions.rollback();
12918
13024
  }
12919
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
+ }
12920
13060
  function writableView(name, event, env) {
12921
13061
  const { schema, bare } = splitQualifiedName(name);
12922
13062
  const db = env.state.databaseForSchema(schema, name);
@@ -12926,7 +13066,6 @@ function writableView(name, event, env) {
12926
13066
  (trigger) => trigger.tableName.toLowerCase() === bare.toLowerCase() && trigger.event === event && trigger.timing === "INSTEAD"
12927
13067
  );
12928
13068
  if (!hasInsteadOf) {
12929
- env.state.getWritableTable(name);
12930
13069
  throw new SqliteError(`cannot modify ${bare} because it is a view`, "other");
12931
13070
  }
12932
13071
  const names = view.columns ?? executeSelect2(view.select, env).columns;
@@ -13043,6 +13182,7 @@ function evaluateSet(items, table, ctx) {
13043
13182
  return updates;
13044
13183
  }
13045
13184
  function updateOne(table, row, updates, env) {
13185
+ table = env.state.ensureWritableTable(table);
13046
13186
  const before = { rowid: row.rowid, values: new Map(row.values) };
13047
13187
  let after;
13048
13188
  let indexesAdded = false;
@@ -13114,6 +13254,7 @@ function removeIndexes(table, row, env) {
13114
13254
  }
13115
13255
  }
13116
13256
  function removeOne(table, row, env) {
13257
+ table = env.state.ensureWritableTable(table);
13117
13258
  removeIndexes(table, row, env);
13118
13259
  table.delete(row.rowid);
13119
13260
  }
@@ -13266,17 +13407,28 @@ function checkForeignKeys(table, row, env) {
13266
13407
  function fkIsDeferred(constraint, env) {
13267
13408
  return env.transactions.inTransaction && constraint.initiallyDeferred;
13268
13409
  }
13269
- function assertForeignKeySatisfied(row, constraint, env) {
13410
+ function assertForeignKeySatisfied(row, constraint, env, excludeParent = null) {
13270
13411
  const values = constraint.columns.map((name) => row.values.get(normalizeColumnName(name)) ?? null);
13412
+ assertForeignKeyValues(values, constraint, env, excludeParent);
13413
+ }
13414
+ function assertForeignKeySatisfiedWithUpdates(row, constraint, updates, env, excludeParent) {
13415
+ const values = constraint.columns.map((name) => {
13416
+ const key = normalizeColumnName(name);
13417
+ return updates.has(key) ? updates.get(key) ?? null : row.values.get(key) ?? null;
13418
+ });
13419
+ assertForeignKeyValues(values, constraint, env, excludeParent);
13420
+ }
13421
+ function assertForeignKeyValues(values, constraint, env, excludeParent) {
13271
13422
  if (values.some((value) => value === null)) return;
13272
13423
  const parent = env.state.getTable(constraint.refTable);
13273
13424
  const parentColumns = constraint.refColumns ?? parent.columns.filter((column) => column.primaryKey).map((column) => column.name);
13274
- if (![...parent.scan()].some(
13275
- (candidate) => values.every((value, index) => {
13425
+ if (![...parent.scan()].some((candidate) => {
13426
+ if (excludeParent && candidate.rowid === excludeParent.rowid) return false;
13427
+ return values.every((value, index) => {
13276
13428
  const parentColumn = parentColumns[index];
13277
13429
  return parentColumn !== void 0 && compareSql(value, candidate.values.get(normalizeColumnName(parentColumn)) ?? null) === 0;
13278
- })
13279
- )) {
13430
+ });
13431
+ })) {
13280
13432
  throw new SqliteError("FOREIGN KEY constraint failed", "constraint_foreign", "SQLITE_CONSTRAINT_FOREIGNKEY");
13281
13433
  }
13282
13434
  }
@@ -13293,10 +13445,13 @@ function applyReferentialDelete(parent, row, env) {
13293
13445
  if (!env.state.foreignKeysEnabled) return 0;
13294
13446
  let changes = 0;
13295
13447
  const parentPk = parent.columns.filter((column) => column.primaryKey).map((column) => column.name);
13296
- 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;
13297
13451
  for (const constraint of child.constraints) {
13298
13452
  if (constraint.type !== "foreign_key" || constraint.refTable.toLowerCase() !== parent.name.toLowerCase())
13299
13453
  continue;
13454
+ child = env.state.ensureWritableTable(child);
13300
13455
  const referenced = constraint.refColumns ?? parentPk;
13301
13456
  const matches = [...child.scan()].filter(
13302
13457
  (candidate) => !(child === parent && candidate.rowid === row.rowid) && foreignKeyMatches(constraint.columns, candidate, referenced, row)
@@ -13317,7 +13472,9 @@ function applyReferentialDelete(parent, row, env) {
13317
13472
  );
13318
13473
  changes += 1 + updated.cascaded;
13319
13474
  } else if (constraint.onDelete === "SET DEFAULT") {
13320
- const updated = updateOne(child, candidate, defaultUpdates(child, constraint.columns, env), env);
13475
+ const updates = defaultUpdates(child, constraint.columns, env);
13476
+ assertForeignKeySatisfiedWithUpdates(candidate, constraint, updates, env, row);
13477
+ const updated = updateOne(child, candidate, updates, env);
13321
13478
  changes += 1 + updated.cascaded;
13322
13479
  } else if (constraint.onDelete === "RESTRICT" || !fkIsDeferred(constraint, env)) {
13323
13480
  throw new SqliteError("FOREIGN KEY constraint failed", "constraint_foreign", "SQLITE_CONSTRAINT_FOREIGNKEY");
@@ -13331,10 +13488,13 @@ function applyReferentialUpdate(parent, before, after, env) {
13331
13488
  if (!env.state.foreignKeysEnabled) return 0;
13332
13489
  let changes = 0;
13333
13490
  const parentPk = parent.columns.filter((column) => column.primaryKey).map((column) => column.name);
13334
- 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;
13335
13494
  for (const constraint of child.constraints) {
13336
13495
  if (constraint.type !== "foreign_key" || constraint.refTable.toLowerCase() !== parent.name.toLowerCase())
13337
13496
  continue;
13497
+ child = env.state.ensureWritableTable(child);
13338
13498
  const referenced = constraint.refColumns ?? parentPk;
13339
13499
  const oldValues = referenced.map((name) => before.values.get(normalizeColumnName(name)) ?? null);
13340
13500
  const newValues = referenced.map((name) => after.values.get(normalizeColumnName(name)) ?? null);