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::{Params, Placeholder, TypedValue};
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/// Context information when serializing VALUES in an INSERT statement.
38#[derive(Debug, Clone)]
39pub struct InsertContext {
40    /// The table being inserted into.
41    pub table_id: db::TableId,
42    /// Columns receiving values, in order.
43    pub columns: Vec<db::ColumnId>,
44}
45
46/// Serialize a statement to a SQL string
47#[derive(Debug)]
48pub struct Serializer<'a> {
49    /// Schema against which the statement is to be serialized
50    schema: &'a db::Schema,
51
52    /// The database flavor handles the differences between SQL dialects and
53    /// supported features.
54    flavor: Flavor,
55}
56
57struct Formatter<'a, T> {
58    /// Handle to the serializer
59    serializer: &'a Serializer<'a>,
60
61    /// Where to write the serialized SQL
62    dst: &'a mut String,
63
64    /// Where to store parameters
65    params: &'a mut T,
66
67    /// Current query depth. This is used to determine the nesting level when
68    /// generating names
69    depth: usize,
70
71    /// True when table names should be aliased.
72    alias: bool,
73
74    /// Context when serializing VALUES in an INSERT statement
75    insert_context: Option<InsertContext>,
76}
77
78/// Expression context bound to a database-level schema.
79pub type ExprContext<'a> = toasty_core::stmt::ExprContext<'a, db::Schema>;
80
81impl<'a> Serializer<'a> {
82    /// Serializes a [`Statement`] to a SQL string, appending a trailing semicolon.
83    ///
84    /// Parameter placeholders are written in the dialect's native format
85    /// (`$1` for PostgreSQL, `?1` for SQLite, `?` for MySQL) and the
86    /// corresponding values are pushed into `params`.
87    pub fn serialize(&self, stmt: &Statement, params: &mut impl Params) -> String {
88        let mut ret = String::new();
89
90        let mut fmt = Formatter {
91            serializer: self,
92            dst: &mut ret,
93            params,
94            depth: 0,
95            alias: false,
96            insert_context: None,
97        };
98
99        let cx = ExprContext::new(self.schema);
100
101        stmt.to_sql(&cx, &mut fmt);
102
103        ret.push(';');
104        ret
105    }
106
107    /// Serialize a transaction control operation to a SQL string.
108    ///
109    /// The generated SQL is flavor-specific (e.g., MySQL uses `START TRANSACTION`
110    /// while other databases use `BEGIN`). Savepoints are named `sp_{id}`.
111    pub fn serialize_transaction(&self, op: &Transaction) -> String {
112        let mut ret = String::new();
113
114        let mut f = Formatter {
115            serializer: self,
116            dst: &mut ret,
117            params: &mut Vec::<TypedValue>::new(),
118            depth: 0,
119            alias: false,
120            insert_context: None,
121        };
122
123        let cx = ExprContext::new(self.schema);
124
125        match op {
126            Transaction::Start {
127                isolation,
128                read_only,
129            } => fmt!(
130                &cx,
131                &mut f,
132                self.serialize_transaction_start(*isolation, *read_only)
133            ),
134            Transaction::Commit => fmt!(&cx, &mut f, "COMMIT"),
135            Transaction::Rollback => fmt!(&cx, &mut f, "ROLLBACK"),
136            Transaction::Savepoint(name) => {
137                fmt!(&cx, &mut f, "SAVEPOINT " Ident(name))
138            }
139            Transaction::ReleaseSavepoint(name) => {
140                fmt!(&cx, &mut f, "RELEASE SAVEPOINT " Ident(name))
141            }
142            Transaction::RollbackToSavepoint(name) => {
143                fmt!(&cx, &mut f, "ROLLBACK TO SAVEPOINT " Ident(name))
144            }
145        };
146
147        ret.push(';');
148        ret
149    }
150
151    fn serialize_transaction_start(
152        &self,
153        isolation: Option<IsolationLevel>,
154        read_only: bool,
155    ) -> String {
156        fn isolation_level_str(level: IsolationLevel) -> &'static str {
157            match level {
158                IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
159                IsolationLevel::ReadCommitted => "READ COMMITTED",
160                IsolationLevel::RepeatableRead => "REPEATABLE READ",
161                IsolationLevel::Serializable => "SERIALIZABLE",
162            }
163        }
164
165        match self.flavor {
166            Flavor::Mysql => {
167                let mut sql = String::new();
168                if let Some(level) = isolation {
169                    sql.push_str("SET TRANSACTION ISOLATION LEVEL ");
170                    sql.push_str(isolation_level_str(level));
171                    sql.push_str("; ");
172                }
173                sql.push_str("START TRANSACTION");
174                if read_only {
175                    sql.push_str(" READ ONLY");
176                }
177                sql
178            }
179            Flavor::Postgresql => {
180                let mut sql = String::from("BEGIN");
181                if let Some(level) = isolation {
182                    sql.push_str(" ISOLATION LEVEL ");
183                    sql.push_str(isolation_level_str(level));
184                }
185                if read_only {
186                    sql.push_str(" READ ONLY");
187                }
188                sql
189            }
190            Flavor::Sqlite => {
191                // SQLite doesn't support per-transaction isolation levels or read-only mode
192                "BEGIN".to_string()
193            }
194        }
195    }
196
197    fn table(&self, id: impl Into<db::TableId>) -> &'a Table {
198        self.schema.table(id.into())
199    }
200
201    fn index(&self, id: impl Into<db::IndexId>) -> &'a Index {
202        self.schema.index(id.into())
203    }
204
205    fn table_name(&self, id: impl Into<db::TableId>) -> Ident<&str> {
206        let table = self.schema.table(id.into());
207        Ident(&table.name)
208    }
209
210    fn column_name(&self, id: impl Into<db::ColumnId>) -> Ident<&str> {
211        let column = self.schema.column(id.into());
212        Ident(&column.name)
213    }
214}