Skip to main content

toasty_sql/
serializer.rs

1#[macro_use]
2mod fmt;
3use fmt::ToSql;
4
5mod column;
6use column::ColumnAlias;
7
8mod cte;
9
10mod delim;
11use delim::{Comma, Delimited, Period};
12
13mod flavor;
14use flavor::Flavor;
15
16mod ident;
17use ident::Ident;
18
19mod params;
20pub use params::Placeholder;
21
22// Fragment serializers
23mod column_def;
24mod expr;
25mod name;
26mod statement;
27mod ty;
28mod value;
29
30use crate::stmt::Statement;
31
32use toasty_core::{
33    driver::operation::{IsolationLevel, Transaction, TransactionMode},
34    schema::db::{self, Index, Table},
35    stmt::IntoExprTarget,
36};
37
38/// Serialize a statement to a SQL string
39#[derive(Debug)]
40pub struct Serializer<'a> {
41    /// Schema against which the statement is to be serialized
42    schema: &'a db::Schema,
43
44    /// The database flavor handles the differences between SQL dialects and
45    /// supported features.
46    flavor: Flavor,
47
48    /// SQL emitted for [`TransactionMode::Default`] under the SQLite flavor.
49    /// Constructors that don't override this leave it at `"BEGIN"`, which is
50    /// SQLite's natural default (DEFERRED). A driver that wants `Default` to
51    /// mean something engine-specific — Turso under `concurrent_writes()`
52    /// uses `"BEGIN CONCURRENT"` — sets this through
53    /// [`Self::sqlite_with_default_begin`]. The non-`Default` variants
54    /// (`Deferred`, `Immediate`, `Exclusive`) always emit fixed SQL.
55    sqlite_default_begin: &'static str,
56}
57
58struct Formatter<'a> {
59    /// Handle to the serializer
60    serializer: &'a Serializer<'a>,
61
62    /// Expression-resolution context for the current scope. Re-scoped (via
63    /// [`Formatter::scope`]) each time serialization descends into a new
64    /// query level, so it travels with the formatter rather than as a
65    /// separate argument.
66    cx: ExprContext<'a>,
67
68    /// Where to write the serialized SQL
69    dst: &'a mut String,
70
71    /// Current query depth. This is used to determine the nesting level when
72    /// generating names
73    depth: usize,
74
75    /// True when table names should be aliased.
76    alias: bool,
77
78    /// True when inside an INSERT statement. Used by MySQL to decide whether
79    /// VALUES rows need the ROW() wrapper (required in subqueries but not in
80    /// INSERT).
81    in_insert: bool,
82
83    /// Collects `Expr::Arg(n)` positions in the order they appear in the SQL.
84    /// Used by MySQL (which uses positional `?` without indices) to reorder
85    /// the params vec to match placeholder occurrence order. Borrowed so a
86    /// scoped child formatter writes through to the root's vec.
87    arg_positions: &'a mut Vec<usize>,
88}
89
90impl<'a> Formatter<'a> {
91    /// Descend into a new expression scope, returning a child formatter that
92    /// shares this one's output sink and arg collector (so writes flow back
93    /// to the root) but resolves references against `target`.
94    ///
95    /// The child borrows `self`, so the parent scope stays live on the stack
96    /// for the child's lifetime — that is what keeps the `ExprContext` parent
97    /// chain valid for nested-reference resolution.
98    fn scope<'c>(&'c mut self, target: impl IntoExprTarget<'c, db::Schema>) -> Formatter<'c> {
99        Formatter {
100            serializer: self.serializer,
101            cx: self.cx.scope(target),
102            dst: &mut *self.dst,
103            depth: self.depth,
104            alias: self.alias,
105            in_insert: self.in_insert,
106            arg_positions: &mut *self.arg_positions,
107        }
108    }
109}
110
111/// Expression context bound to a database-level schema.
112pub type ExprContext<'a> = toasty_core::stmt::ExprContext<'a, db::Schema>;
113
114impl<'a> Serializer<'a> {
115    /// Serializes a [`Statement`] to a SQL string with all values inlined as
116    /// literals (no bind parameters). Appends a trailing semicolon.
117    ///
118    /// Use this for DDL statements (`CREATE TABLE`, `CREATE TYPE`, etc.) where
119    /// bind parameters are not supported. DML statements should already have
120    /// their parameters extracted (as `Expr::Arg` placeholders) before reaching
121    /// the serializer.
122    pub fn serialize(&self, stmt: &Statement) -> String {
123        self.serialize_with_arg_order(stmt).0
124    }
125
126    /// Serializes a [`Statement`] and returns both the SQL string and the order
127    /// in which `Expr::Arg(n)` placeholders appear in the SQL.
128    ///
129    /// The arg order is needed by MySQL which uses positional `?` without
130    /// indices — the caller must reorder its params vec to match the occurrence
131    /// order. PostgreSQL and SQLite use indexed placeholders (`$1`, `?1`) so
132    /// they can ignore the arg order.
133    pub fn serialize_with_arg_order(&self, stmt: &Statement) -> (String, Vec<usize>) {
134        let mut ret = String::new();
135        let mut arg_positions = Vec::new();
136
137        {
138            let mut fmt = Formatter {
139                serializer: self,
140                cx: ExprContext::new(self.schema),
141                dst: &mut ret,
142                depth: 0,
143                alias: false,
144                in_insert: false,
145                arg_positions: &mut arg_positions,
146            };
147
148            stmt.to_sql(&mut fmt);
149        }
150
151        ret.push(';');
152        (ret, arg_positions)
153    }
154
155    /// Serialize a transaction control operation to a SQL string.
156    ///
157    /// The generated SQL is flavor-specific (e.g., MySQL uses `START TRANSACTION`
158    /// while other databases use `BEGIN`). Savepoints are named `sp_{id}`.
159    pub fn serialize_transaction(&self, op: &Transaction) -> String {
160        let mut ret = String::new();
161        let mut arg_positions = Vec::new();
162
163        {
164            let mut f = Formatter {
165                serializer: self,
166                cx: ExprContext::new(self.schema),
167                dst: &mut ret,
168                depth: 0,
169                alias: false,
170                in_insert: false,
171                arg_positions: &mut arg_positions,
172            };
173
174            match op {
175                Transaction::Start {
176                    isolation,
177                    read_only,
178                    mode,
179                } => fmt!(
180                    &mut f,
181                    self.serialize_transaction_start(*isolation, *read_only, *mode)
182                ),
183                Transaction::Commit => fmt!(&mut f, "COMMIT"),
184                Transaction::Rollback => fmt!(&mut f, "ROLLBACK"),
185                Transaction::Savepoint(name) => {
186                    fmt!(&mut f, "SAVEPOINT " Ident(name))
187                }
188                Transaction::ReleaseSavepoint(name) => {
189                    fmt!(&mut f, "RELEASE SAVEPOINT " Ident(name))
190                }
191                Transaction::RollbackToSavepoint(name) => {
192                    fmt!(&mut f, "ROLLBACK TO SAVEPOINT " Ident(name))
193                }
194            };
195        }
196
197        ret.push(';');
198        ret
199    }
200
201    fn serialize_transaction_start(
202        &self,
203        isolation: Option<IsolationLevel>,
204        read_only: bool,
205        mode: TransactionMode,
206    ) -> String {
207        fn isolation_level_str(level: IsolationLevel) -> &'static str {
208            match level {
209                IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
210                IsolationLevel::ReadCommitted => "READ COMMITTED",
211                IsolationLevel::RepeatableRead => "REPEATABLE READ",
212                IsolationLevel::Serializable => "SERIALIZABLE",
213            }
214        }
215
216        match self.flavor {
217            // MySQL has no SQLite-style lock-mode keyword; drivers
218            // reject non-Default `mode` before reaching the serializer.
219            Flavor::Mysql => {
220                let mut sql = String::new();
221                if let Some(level) = isolation {
222                    sql.push_str("SET TRANSACTION ISOLATION LEVEL ");
223                    sql.push_str(isolation_level_str(level));
224                    sql.push_str("; ");
225                }
226                sql.push_str("START TRANSACTION");
227                if read_only {
228                    sql.push_str(" READ ONLY");
229                }
230                sql
231            }
232            // PostgreSQL has no SQLite-style lock-mode keyword; drivers
233            // reject non-Default `mode` before reaching the serializer.
234            Flavor::Postgresql => {
235                let mut sql = String::from("BEGIN");
236                if let Some(level) = isolation {
237                    sql.push_str(" ISOLATION LEVEL ");
238                    sql.push_str(isolation_level_str(level));
239                }
240                if read_only {
241                    sql.push_str(" READ ONLY");
242                }
243                sql
244            }
245            // SQLite has no per-transaction isolation level or read-only
246            // keyword; the lock-acquisition mode is the only knob. `Default`
247            // emits whatever the serializer was configured with at
248            // construction (`BEGIN` by default, or e.g. `BEGIN CONCURRENT`
249            // for Turso under MVCC). `Deferred`/`Immediate`/`Exclusive` are
250            // explicit caller requests with fixed SQL.
251            Flavor::Sqlite => match mode {
252                TransactionMode::Default => self.sqlite_default_begin.to_string(),
253                TransactionMode::Deferred => "BEGIN".to_string(),
254                TransactionMode::Immediate => "BEGIN IMMEDIATE".to_string(),
255                TransactionMode::Exclusive => "BEGIN EXCLUSIVE".to_string(),
256            },
257        }
258    }
259
260    fn table(&self, id: impl Into<db::TableId>) -> &'a Table {
261        self.schema.table(id.into())
262    }
263
264    fn index(&self, id: impl Into<db::IndexId>) -> &'a Index {
265        self.schema.index(id.into())
266    }
267
268    fn table_name(&self, id: impl Into<db::TableId>) -> Ident<&str> {
269        let table = self.schema.table(id.into());
270        Ident(&table.name)
271    }
272
273    fn column_name(&self, id: impl Into<db::ColumnId>) -> Ident<&str> {
274        let column = self.schema.column(id.into());
275        Ident(&column.name)
276    }
277}