squonk_ast/ast/tcl.rs
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! Transaction-control statement AST nodes (ADR-0012 operational family).
5
6use super::{Ident, Literal};
7use crate::vocab::Meta;
8use thin_vec::ThinVec;
9
10/// A transaction-control statement (SQL:2016 §17).
11///
12/// The operational statements clients send to bound a transaction: `START
13/// TRANSACTION` (and its near-universal `BEGIN` alias), `COMMIT`, `ROLLBACK`
14/// (optionally rewinding to a savepoint), `SAVEPOINT`, `RELEASE SAVEPOINT`, and
15/// `SET TRANSACTION` characteristics.
16///
17/// One canonical shape per construct. `BEGIN` and `START TRANSACTION`
18/// denote the same operation, so they share the [`Begin`](Self::Begin) shape and a
19/// [`TransactionStart`] tag records which spelling was written. The interchangeable
20/// `WORK` / `TRANSACTION` block noise words on `BEGIN`/`COMMIT`/`ROLLBACK` carry no
21/// meaning; a [`TransactionBlockKeyword`] tag records which (if any) was written so a
22/// source-fidelity render replays it, while a target re-spell and the redacted
23/// fingerprint drop it.
24#[derive(Clone, Debug, PartialEq, Eq, Hash)]
25#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
26#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
27pub enum TransactionStatement {
28 /// A `BEGIN` / `START TRANSACTION` statement.
29 Begin {
30 /// Source spelling used for the syntax.
31 syntax: TransactionStart,
32 /// SQLite's `{DEFERRED | IMMEDIATE | EXCLUSIVE}` transaction-mode modifier,
33 /// written between `BEGIN` and the optional `TRANSACTION` keyword. `None`
34 /// when the dialect does not admit one (or the statement omits it).
35 mode: Option<TransactionModeKind>,
36 /// The optional `TRANSACTION` / `WORK` block noise word after `BEGIN` or
37 /// `START`. `None` for a bare keyword. Standard `START TRANSACTION` records
38 /// `Some(Transaction)`; dialect data controls whether `START` may omit the word
39 /// or use `WORK`.
40 block: Option<TransactionBlockKeyword>,
41 /// SQLite's optional name after an explicit `TRANSACTION` keyword.
42 name: Option<Box<Ident>>,
43 /// modes in source order.
44 modes: ThinVec<TransactionMode>,
45 /// Source location and node identity.
46 meta: Meta,
47 },
48 /// A `COMMIT` statement.
49 Commit {
50 /// Source spelling used for the commit operation.
51 syntax: TransactionCommitKeyword,
52 /// The optional `TRANSACTION` / `WORK` block noise word after `COMMIT`.
53 block: Option<TransactionBlockKeyword>,
54 /// SQLite's optional name after an explicit `TRANSACTION` keyword.
55 name: Option<Box<Ident>>,
56 /// `Some(true)` for `AND CHAIN`, `Some(false)` for `AND NO CHAIN`.
57 chain: Option<bool>,
58 /// `Some(true)` for `RELEASE`, `Some(false)` for `NO RELEASE`.
59 release: Option<bool>,
60 /// Source location and node identity.
61 meta: Meta,
62 },
63 /// `ROLLBACK [WORK | TRANSACTION] [TO [SAVEPOINT] <name>]`.
64 ///
65 /// `to_savepoint` is `Some` for the savepoint-rewind form and `None` for a
66 /// whole-transaction rollback.
67 Rollback {
68 /// Source spelling used for the rollback operation.
69 syntax: TransactionRollbackKeyword,
70 /// The optional `TRANSACTION` / `WORK` block noise word after `ROLLBACK`.
71 block: Option<TransactionBlockKeyword>,
72 /// SQLite's optional name after an explicit `TRANSACTION` keyword.
73 name: Option<Box<Ident>>,
74 /// Whether the optional `SAVEPOINT` keyword was written before the savepoint
75 /// name (`ROLLBACK TO SAVEPOINT x` vs the bare `ROLLBACK TO x`). Meaningful
76 /// only when `to_savepoint` is `Some`; the canonical render emits `SAVEPOINT`.
77 savepoint_keyword: bool,
78 /// Optional to savepoint for this syntax.
79 to_savepoint: Option<Ident>,
80 /// `Some(true)` for `AND CHAIN`, `Some(false)` for `AND NO CHAIN`; only valid for a
81 /// whole-transaction rollback.
82 chain: Option<bool>,
83 /// `Some(true)` for `RELEASE`, `Some(false)` for `NO RELEASE`; only valid for a
84 /// whole-transaction rollback.
85 release: Option<bool>,
86 /// Source location and node identity.
87 meta: Meta,
88 },
89 /// A `SAVEPOINT <name>` statement.
90 Savepoint {
91 /// Name referenced by this syntax.
92 name: Ident,
93 /// Source location and node identity.
94 meta: Meta,
95 },
96 /// A `RELEASE [SAVEPOINT] <name>` statement.
97 Release {
98 /// Whether the optional `SAVEPOINT` keyword was written (`RELEASE SAVEPOINT x`
99 /// vs the bare `RELEASE x`). Exact-synonym fidelity; the canonical render emits
100 /// `SAVEPOINT`.
101 savepoint_keyword: bool,
102 /// The savepoint name being released.
103 savepoint: Ident,
104 /// Source location and node identity.
105 meta: Meta,
106 },
107 /// `SET TRANSACTION <mode> [, ...]`: set the current transaction's
108 /// characteristics (distinct from the session [`SET`](super::SessionStatement)).
109 SetCharacteristics {
110 /// modes in source order.
111 modes: ThinVec<TransactionMode>,
112 /// Source location and node identity.
113 meta: Meta,
114 },
115}
116
117/// Which spelling started a transaction; the two are exact synonyms, so this is a
118/// surface tag, not a separate shape.
119#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
120#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
121#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
122pub enum TransactionStart {
123 /// `BEGIN` — start a transaction.
124 Begin,
125 /// `START TRANSACTION` — start a transaction (the standard spelling).
126 Start,
127}
128
129/// Which exact synonym committed a transaction.
130#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
131#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
132#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
133pub enum TransactionCommitKeyword {
134 /// `COMMIT`.
135 Commit,
136 /// `END`.
137 End,
138}
139
140/// Which exact synonym rolled back a transaction.
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
142#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
143#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
144pub enum TransactionRollbackKeyword {
145 /// `ROLLBACK`.
146 Rollback,
147 /// `ABORT`.
148 Abort,
149}
150
151/// The optional block noise word written after `BEGIN` / `COMMIT` / `ROLLBACK`
152/// (`TRANSACTION` or `WORK`): interchangeable synonyms carrying no meaning. The
153/// canonical AST records which (if any) the source wrote so a source-fidelity render
154/// replays it; a target re-spell and the redacted fingerprint drop it (the enclosing
155/// `Option` is `None` for a bare keyword).
156#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
157#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
158#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
159pub enum TransactionBlockKeyword {
160 /// The `TRANSACTION` noise word.
161 Transaction,
162 /// The `WORK` noise word (a synonym for `TRANSACTION`).
163 Work,
164}
165
166/// SQLite's `BEGIN` transaction-mode modifier (`sqlite-begin-transaction-modifiers`):
167/// selects the locking behaviour SQLite uses to acquire the database lock. Distinct
168/// from [`TransactionMode`] (the ANSI/PostgreSQL `START TRANSACTION`/`SET TRANSACTION`
169/// isolation-level and access-mode list) — SQLite's three keywords are their own
170/// closed, position-fixed vocabulary (immediately after `BEGIN`, before `TRANSACTION`),
171/// not a mode list.
172#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
173#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
174#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
175pub enum TransactionModeKind {
176 /// `BEGIN DEFERRED` — acquire the database lock lazily (SQLite).
177 Deferred,
178 /// `BEGIN IMMEDIATE` — acquire a reserved lock immediately (SQLite).
179 Immediate,
180 /// `BEGIN EXCLUSIVE` — acquire an exclusive lock immediately (SQLite).
181 Exclusive,
182}
183
184/// One transaction mode in a `START TRANSACTION` / `SET TRANSACTION` mode list
185/// (and in the session [`SET SESSION CHARACTERISTICS`](super::SessionStatement::SetSessionCharacteristics)).
186#[derive(Clone, Debug, PartialEq, Eq, Hash)]
187#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
188#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
189pub enum TransactionMode {
190 /// `ISOLATION LEVEL <level>` — set the transaction's isolation level.
191 IsolationLevel {
192 /// Which isolation level; see [`IsolationLevel`].
193 level: IsolationLevel,
194 /// Source location and node identity.
195 meta: Meta,
196 },
197 /// `READ ONLY` / `READ WRITE` — set the transaction's access mode.
198 AccessMode {
199 /// Which access mode; see [`TransactionAccessMode`].
200 access: TransactionAccessMode,
201 /// Source location and node identity.
202 meta: Meta,
203 },
204 /// `DEFERRABLE` / `NOT DEFERRABLE`: the two spellings are one mode toggled by
205 /// a flag; `deferrable` is `false` only for the `NOT` spelling.
206 Deferrable {
207 /// Whether the deferrable form was present in the source.
208 deferrable: bool,
209 /// Source location and node identity.
210 meta: Meta,
211 },
212 /// `WITH CONSISTENT SNAPSHOT`: establish a consistent read view when the
213 /// transaction starts (MySQL).
214 ConsistentSnapshot {
215 /// Source location and node identity.
216 meta: Meta,
217 },
218}
219
220#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
221#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
222#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
223/// The SQL isolation level forms represented by the AST.
224pub enum IsolationLevel {
225 /// `READ UNCOMMITTED` — the weakest isolation; permits dirty reads.
226 ReadUncommitted,
227 /// `READ COMMITTED` — only committed rows are visible.
228 ReadCommitted,
229 /// `REPEATABLE READ` — reads are stable for the transaction's duration.
230 RepeatableRead,
231 /// `SERIALIZABLE` — the strongest isolation; transactions appear fully serial.
232 Serializable,
233}
234
235#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
236#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
237#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
238/// The SQL transaction access mode forms represented by the AST.
239pub enum TransactionAccessMode {
240 /// `READ ONLY` — the transaction may not modify data.
241 ReadOnly,
242 /// `READ WRITE` — the transaction may modify data (the default).
243 ReadWrite,
244}
245
246/// A MySQL `XA` distributed-transaction statement — the X/Open XA two-phase-commit
247/// verbs (`xa_transactions`; `sql_yacc.yy` `xa:`), a family distinct from the ANSI
248/// [`TransactionStatement`] control statements above.
249///
250/// Every verb but [`Recover`](Self::Recover) names an [`Xid`] transaction-branch
251/// identifier. The forms are grammar-recognized but not preparable over the wire
252/// (live mysql:8.4.10 answers `ER_UNSUPPORTED_PS` 1295 to each), so they carry no
253/// planner semantics here beyond their parsed shape.
254#[derive(Clone, Debug, PartialEq, Eq, Hash)]
255#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
256#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
257pub enum XaStatement {
258 /// `XA {START | BEGIN} xid [JOIN | RESUME]` — start (or JOIN/RESUME) a transaction
259 /// branch. `START` and `BEGIN` are exact synonyms (`begin_or_start`), recorded by a
260 /// [`XaStartKeyword`] surface tag.
261 Start {
262 /// Which of the `START` / `BEGIN` synonyms was written.
263 keyword: XaStartKeyword,
264 /// The transaction-branch identifier; see [`Xid`].
265 xid: Xid,
266 /// The optional `JOIN` / `RESUME` branch-association mode (`opt_join_or_resume`);
267 /// `None` for a plain start.
268 association: Option<XaAssociation>,
269 /// Source location and node identity.
270 meta: Meta,
271 },
272 /// `XA END xid [SUSPEND [FOR MIGRATE]]` — end (or suspend) the branch's active work.
273 End {
274 /// The transaction-branch identifier; see [`Xid`].
275 xid: Xid,
276 /// The optional `SUSPEND` / `SUSPEND FOR MIGRATE` mode (`opt_suspend`); `None`
277 /// for a plain end.
278 suspend: Option<XaSuspend>,
279 /// Source location and node identity.
280 meta: Meta,
281 },
282 /// `XA PREPARE xid` — prepare the branch for two-phase commit.
283 Prepare {
284 /// The transaction-branch identifier; see [`Xid`].
285 xid: Xid,
286 /// Source location and node identity.
287 meta: Meta,
288 },
289 /// `XA COMMIT xid [ONE PHASE]` — commit the branch (`ONE PHASE` commits without a
290 /// prior `XA PREPARE`).
291 Commit {
292 /// The transaction-branch identifier; see [`Xid`].
293 xid: Xid,
294 /// Whether the `ONE PHASE` optimisation was written (`opt_one_phase`).
295 one_phase: bool,
296 /// Source location and node identity.
297 meta: Meta,
298 },
299 /// `XA ROLLBACK xid` — roll the branch back.
300 Rollback {
301 /// The transaction-branch identifier; see [`Xid`].
302 xid: Xid,
303 /// Source location and node identity.
304 meta: Meta,
305 },
306 /// `XA RECOVER [CONVERT XID]` — list the branches the resource manager has prepared.
307 Recover {
308 /// Whether the `CONVERT XID` modifier was written (`opt_convert_xid`), which
309 /// reports each `gtrid`/`bqual` as raw hexadecimal bytes.
310 convert_xid: bool,
311 /// Source location and node identity.
312 meta: Meta,
313 },
314}
315
316/// Which of the interchangeable `XA START` / `XA BEGIN` spellings began a transaction
317/// branch. The two are exact synonyms (`begin_or_start`), so this is a surface tag, not
318/// a separate shape.
319#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
320#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
321#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
322pub enum XaStartKeyword {
323 /// `XA START` — the standard spelling.
324 Start,
325 /// `XA BEGIN` — the synonym.
326 Begin,
327}
328
329/// The optional `JOIN` / `RESUME` branch-association mode on `XA START` / `XA BEGIN`
330/// (`opt_join_or_resume`): a closed two-keyword axis, valid only on the branch-start
331/// verb (the engine rejects it on `END`/`COMMIT`).
332#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
333#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
334#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
335pub enum XaAssociation {
336 /// `JOIN` — join the named existing branch.
337 Join,
338 /// `RESUME` — resume the named suspended branch.
339 Resume,
340}
341
342/// The optional `SUSPEND` mode on `XA END` (`opt_suspend`'s two non-empty forms):
343/// `SUSPEND` alone, or `SUSPEND FOR MIGRATE`. `FOR MIGRATE` is valid only after
344/// `SUSPEND`, so the two travel as one closed axis rather than an independent flag.
345#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
346#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
347#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
348pub enum XaSuspend {
349 /// `SUSPEND` — suspend the branch's work.
350 Suspend,
351 /// `SUSPEND FOR MIGRATE` — suspend for migration to another connection.
352 SuspendForMigrate,
353}
354
355/// An XA transaction-branch identifier: `gtrid [, bqual [, formatID]]` (`sql_yacc.yy`
356/// `xid`).
357///
358/// `gtrid` (the global transaction id) and `bqual` (the branch qualifier) are byte-string
359/// constants — a character-string literal (`'…'`) or a hexadecimal / binary literal
360/// (`0x…` / `X'…'` / `0b…` / `B'…'`), the `text_string` production; a bare decimal number
361/// is not accepted. `format_id` is a non-negative numeric literal (`ulong_num`). Each part
362/// rides its own [`Literal`] span, so the exact spelling round-trips and no owned value is
363/// interned.
364///
365/// The engine additionally caps `gtrid`/`bqual` at 64 bytes and rejects a `format_id`
366/// above `LONG_MAX` at parse time; those are value-length/range limits on the decoded
367/// bytes (charset-dependent for a character string), not grammar shape, so they are left
368/// to a binding pass rather than enforced here — the same treatment the parser gives other
369/// literal magnitude limits.
370#[derive(Clone, Debug, PartialEq, Eq, Hash)]
371#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
372#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
373pub struct Xid {
374 /// The global transaction id (`gtrid`) — a string or hex/binary literal.
375 pub gtrid: Literal,
376 /// The optional branch qualifier (`bqual`) — a string or hex/binary literal; `None`
377 /// for a one-part xid.
378 pub bqual: Option<Literal>,
379 /// The optional format identifier (`formatID`) — a non-negative numeric literal;
380 /// `None` unless a `bqual` was also written (the grammar admits it only in the
381 /// three-part form).
382 pub format_id: Option<Literal>,
383 /// Source location and node identity.
384 pub meta: Meta,
385}