Skip to main content

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}