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},
34    schema::db::{self, Index, Table},
35};
36
37/// Serialize a statement to a SQL string
38#[derive(Debug)]
39pub struct Serializer<'a> {
40    /// Schema against which the statement is to be serialized
41    schema: &'a db::Schema,
42
43    /// The database flavor handles the differences between SQL dialects and
44    /// supported features.
45    flavor: Flavor,
46}
47
48struct Formatter<'a> {
49    /// Handle to the serializer
50    serializer: &'a Serializer<'a>,
51
52    /// Where to write the serialized SQL
53    dst: &'a mut String,
54
55    /// Current query depth. This is used to determine the nesting level when
56    /// generating names
57    depth: usize,
58
59    /// True when table names should be aliased.
60    alias: bool,
61
62    /// True when inside an INSERT statement. Used by MySQL to decide whether
63    /// VALUES rows need the ROW() wrapper (required in subqueries but not in
64    /// INSERT).
65    in_insert: bool,
66
67    /// Collects `Expr::Arg(n)` positions in the order they appear in the SQL.
68    /// Used by MySQL (which uses positional `?` without indices) to reorder
69    /// the params vec to match placeholder occurrence order.
70    arg_positions: Vec<usize>,
71}
72
73/// Expression context bound to a database-level schema.
74pub type ExprContext<'a> = toasty_core::stmt::ExprContext<'a, db::Schema>;
75
76impl<'a> Serializer<'a> {
77    /// Serializes a [`Statement`] to a SQL string with all values inlined as
78    /// literals (no bind parameters). Appends a trailing semicolon.
79    ///
80    /// Use this for DDL statements (`CREATE TABLE`, `CREATE TYPE`, etc.) where
81    /// bind parameters are not supported. DML statements should already have
82    /// their parameters extracted (as `Expr::Arg` placeholders) before reaching
83    /// the serializer.
84    pub fn serialize(&self, stmt: &Statement) -> String {
85        self.serialize_with_arg_order(stmt).0
86    }
87
88    /// Serializes a [`Statement`] and returns both the SQL string and the order
89    /// in which `Expr::Arg(n)` placeholders appear in the SQL.
90    ///
91    /// The arg order is needed by MySQL which uses positional `?` without
92    /// indices — the caller must reorder its params vec to match the occurrence
93    /// order. PostgreSQL and SQLite use indexed placeholders (`$1`, `?1`) so
94    /// they can ignore the arg order.
95    pub fn serialize_with_arg_order(&self, stmt: &Statement) -> (String, Vec<usize>) {
96        let mut ret = String::new();
97
98        let mut fmt = Formatter {
99            serializer: self,
100            dst: &mut ret,
101            depth: 0,
102            alias: false,
103            in_insert: false,
104            arg_positions: Vec::new(),
105        };
106
107        let cx = ExprContext::new(self.schema);
108
109        stmt.to_sql(&cx, &mut fmt);
110
111        let arg_positions = fmt.arg_positions;
112        ret.push(';');
113        (ret, arg_positions)
114    }
115
116    /// Serialize a transaction control operation to a SQL string.
117    ///
118    /// The generated SQL is flavor-specific (e.g., MySQL uses `START TRANSACTION`
119    /// while other databases use `BEGIN`). Savepoints are named `sp_{id}`.
120    pub fn serialize_transaction(&self, op: &Transaction) -> String {
121        let mut ret = String::new();
122
123        let mut f = Formatter {
124            serializer: self,
125            dst: &mut ret,
126            depth: 0,
127            alias: false,
128            in_insert: false,
129            arg_positions: Vec::new(),
130        };
131
132        let cx = ExprContext::new(self.schema);
133
134        match op {
135            Transaction::Start {
136                isolation,
137                read_only,
138            } => fmt!(
139                &cx,
140                &mut f,
141                self.serialize_transaction_start(*isolation, *read_only)
142            ),
143            Transaction::Commit => fmt!(&cx, &mut f, "COMMIT"),
144            Transaction::Rollback => fmt!(&cx, &mut f, "ROLLBACK"),
145            Transaction::Savepoint(name) => {
146                fmt!(&cx, &mut f, "SAVEPOINT " Ident(name))
147            }
148            Transaction::ReleaseSavepoint(name) => {
149                fmt!(&cx, &mut f, "RELEASE SAVEPOINT " Ident(name))
150            }
151            Transaction::RollbackToSavepoint(name) => {
152                fmt!(&cx, &mut f, "ROLLBACK TO SAVEPOINT " Ident(name))
153            }
154        };
155
156        ret.push(';');
157        ret
158    }
159
160    fn serialize_transaction_start(
161        &self,
162        isolation: Option<IsolationLevel>,
163        read_only: bool,
164    ) -> String {
165        fn isolation_level_str(level: IsolationLevel) -> &'static str {
166            match level {
167                IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
168                IsolationLevel::ReadCommitted => "READ COMMITTED",
169                IsolationLevel::RepeatableRead => "REPEATABLE READ",
170                IsolationLevel::Serializable => "SERIALIZABLE",
171            }
172        }
173
174        match self.flavor {
175            Flavor::Mysql => {
176                let mut sql = String::new();
177                if let Some(level) = isolation {
178                    sql.push_str("SET TRANSACTION ISOLATION LEVEL ");
179                    sql.push_str(isolation_level_str(level));
180                    sql.push_str("; ");
181                }
182                sql.push_str("START TRANSACTION");
183                if read_only {
184                    sql.push_str(" READ ONLY");
185                }
186                sql
187            }
188            Flavor::Postgresql => {
189                let mut sql = String::from("BEGIN");
190                if let Some(level) = isolation {
191                    sql.push_str(" ISOLATION LEVEL ");
192                    sql.push_str(isolation_level_str(level));
193                }
194                if read_only {
195                    sql.push_str(" READ ONLY");
196                }
197                sql
198            }
199            Flavor::Sqlite => {
200                // SQLite doesn't support per-transaction isolation levels or read-only mode
201                "BEGIN".to_string()
202            }
203        }
204    }
205
206    fn table(&self, id: impl Into<db::TableId>) -> &'a Table {
207        self.schema.table(id.into())
208    }
209
210    fn index(&self, id: impl Into<db::IndexId>) -> &'a Index {
211        self.schema.index(id.into())
212    }
213
214    fn table_name(&self, id: impl Into<db::TableId>) -> Ident<&str> {
215        let table = self.schema.table(id.into());
216        Ident(&table.name)
217    }
218
219    fn column_name(&self, id: impl Into<db::ColumnId>) -> Ident<&str> {
220        let column = self.schema.column(id.into());
221        Ident(&column.name)
222    }
223}