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}