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