1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
//! Per-variant transaction-scope handling for the SQLite `RAISE()`
//! conflict-resolution actions (#5417).
//!
//! `RAISE(ABORT|FAIL|ROLLBACK, msg)` fired from a trigger reports `msg` (SQLite
//! error code 19) but the three variants differ in *how much* they undo when
//! they abort the firing statement inside an explicit multi-statement
//! transaction. Verified against sqlite3 3.51.x:
//!
//! - **`RAISE(ABORT, msg)`** — roll back the current **statement** (undo the
//! partial changes it already applied), but keep the enclosing transaction
//! open: statements that ran *before* it in the same transaction survive.
//! This is also SQLite's default conflict behavior.
//! - **`RAISE(FAIL, msg)`** — stop the statement at the failing row but do
//! **not** undo the changes it already applied to earlier rows; keep the
//! transaction open.
//! - **`RAISE(ROLLBACK, msg)`** — roll back the **entire** enclosing
//! transaction.
//! - `RAISE(IGNORE)` is handled earlier as a control-flow signal
//! ([`ExecutorError::RaiseIgnore`]) and never reaches this module.
//!
//! `ABORT` vs `ROLLBACK` is only observable inside an explicit transaction
//! (whether earlier statements survive); in auto-commit both undo the whole
//! statement. `FAIL` differs from `ABORT` in *both* modes — it keeps the rows
//! the statement applied before the abort — so #5464 wires the auto-commit path
//! to commit-on-`FAIL` / rollback-otherwise, not to treat all three variants
//! identically. See [`apply_raise_scope`] and [`run_top_level_dml`].
//!
//! ## Statement atomicity (auto-commit, #5464)
//!
//! SQLite wraps every top-level statement in an implicit transaction. Inside an
//! explicit transaction VibeSQL uses an implicit *statement savepoint*; in
//! auto-commit it wraps the statement in an implicit *transaction* so a
//! mid-statement abort (RAISE(ABORT), FK cascade abort, constraint violation)
//! rolls back the statement's already-applied rows — matching sqlite3 3.51,
//! where e.g. a multi-row DML or cascade that aborts on a later row undoes the
//! earlier ones too. See [`run_top_level_dml`].
//!
//! ## Replication determinism
//!
//! The abort scope is a pure function of the `RaiseAction` carried in the
//! error and the storage transaction state — both of which are identical on
//! every replica that applies the same log entry — so the surviving rows are
//! deterministic across nodes. (The replicated apply path additionally rolls
//! back the *whole* buffered batch on any rejected statement, which is a
//! strictly coarser, equally-deterministic scope; see
//! `vibesql-consensus::state_machine`.)
use RaiseAction;
use Database;
use crateExecutorError;
/// Cheap pre-check: does `table` carry any trigger that could fire a `RAISE`?
///
/// A `RAISE()` can only originate from a trigger body, so a table with no
/// triggers can never need a statement savepoint. This lets the common
/// (trigger-free) DML path skip the snapshot entirely.
///
/// FK cascade caveat (#5440): a DELETE/UPDATE on `table` can cascade into
/// child tables and fire *their* row triggers, which may `RAISE(ABORT)`. So
/// when foreign keys are enabled and any table in the database carries a
/// trigger, this statement could still fire a `RAISE()` even if `table`
/// itself has none. We arm the savepoint conservatively in that case so a
/// cascade-fired `RAISE(ABORT)` rolls back the whole statement (matching
/// sqlite3 3.51). The check is cheap: it only walks the catalog, and only
/// when FKs are on.
pub
/// Apply the transaction-scope rollback for a `RAISE(action, msg)` that
/// aborted a top-level statement, then return the (unchanged) error so the
/// caller can propagate it.
///
/// Assumes the executor armed an implicit statement savepoint via
/// [`Database::arm_statement_savepoint`] before running the statement (when a
/// transaction is active). The savepoint is consumed here regardless of which
/// scope is chosen.
///
/// - `Abort` → roll back to the statement savepoint (undo this statement only).
/// - `Fail` → keep the statement's partial changes; just release the savepoint.
/// - `Rollback` → roll back the whole transaction.
/// - `Ignore` → unreachable (handled as a control-flow signal upstream); kept
/// as a no-op for totality.
pub
/// Run a top-level DML statement with SQLite per-variant `RAISE` scope
/// handling and statement-level atomicity.
///
/// SQLite wraps **every** top-level statement in an implicit transaction that
/// commits on success and rolls back atomically on any error — so a statement
/// that partially applies then aborts (a multi-row DML / FK cascade firing a
/// `RAISE(ABORT)`, an AFTER-trigger abort after earlier rows already landed,
/// or a constraint violation) leaves *no* partial changes. VibeSQL realizes
/// the two modes a statement can run in:
///
/// 1. **Inside an explicit transaction** (`db.in_transaction()`): arm an
/// implicit *statement savepoint* before `f` so a `RAISE(ABORT)` undoes just
/// this statement while earlier statements in the transaction survive, the
/// transaction staying open. This is the #5417/#5431/#5440 path, unchanged.
///
/// 2. **In auto-commit** (no explicit transaction, #5464): wrap the statement
/// in an *implicit transaction* (`begin_transaction`) so the same partial
/// changes can be rolled back. On success the implicit transaction commits;
/// on `RAISE(ABORT)`/`RAISE(ROLLBACK)` or any other error it rolls back the
/// whole statement; on `RAISE(FAIL)` it commits the rows applied so far
/// (matching sqlite3 3.51 FAIL semantics). The implicit transaction emits
/// the usual `TxnBegin`/`TxnCommit`/`TxnRollback` WAL markers, so crash
/// recovery buffers and applies-or-discards the statement atomically too.
///
/// In both modes the work only happens when `may_fire_trigger` is true (the
/// only way a `RAISE()` can fire, and the only way a single statement applies
/// rows incrementally across trigger programs). When no trigger can fire this
/// is a thin pass-through with zero transaction/snapshot cost — preserving the
/// hot path for the common trigger-free statement.
pub
/// Statement-scope handling when an explicit transaction is already open: arm
/// an implicit statement savepoint so a `RAISE(ABORT)` undoes just this
/// statement, leaving earlier statements (and the transaction) intact.
/// Statement-scope handling in auto-commit mode (#5464): wrap the statement in
/// an implicit transaction so its partial changes can be rolled back as a unit,
/// matching SQLite's implicit per-statement transaction.
///
/// If the implicit `begin_transaction` fails (it should not in auto-commit, but
/// be defensive) we fall back to running `f` directly — no worse than the
/// pre-#5464 behavior.
/// Commit the implicit auto-commit transaction, propagating a commit failure as
/// an executor error (so e.g. a deferred-FK COMMIT abort surfaces correctly).
/// Commit the implicit transaction on a path that already carries the error to
/// return (RAISE(FAIL)); a commit failure here cannot change the reported
/// error, so it is best-effort.
/// Roll back the implicit transaction, best-effort: the statement already has
/// an error to propagate, and rollback restores the pre-statement snapshot.