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