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