1use crate::ast;
2
3use super::ToSqlString;
4
5mod alter_table;
6mod create_table;
7mod create_trigger;
8mod create_virtual_table;
9mod delete;
10mod insert;
11mod select;
12mod update;
13
14impl ToSqlString for ast::Stmt {
15 fn to_sql_string<C: super::ToSqlContext>(&self, context: &C) -> String {
16 match self {
17 Self::AlterTable(alter_table) => {
18 let (name, body) = alter_table.as_ref();
19 format!(
20 "ALTER TABLE {} {};",
21 name.to_sql_string(context),
22 body.to_sql_string(context)
23 )
24 }
25 Self::Analyze(name) => {
26 if let Some(name) = name {
27 format!("ANALYZE {};", name.to_sql_string(context))
28 } else {
29 format!("ANALYZE;")
30 }
31 }
32 Self::Attach {
33 expr,
34 db_name,
35 key,
36 database_kw,
37 } => {
38 format!(
39 "ATTACH{} {} AS {}{};",
40 if *database_kw { " DATABASE" } else { "" },
41 expr.to_sql_string(context),
42 db_name.to_sql_string(context),
43 key.as_ref().map_or("".to_string(), |key| format!(
44 " KEY {}",
45 key.to_sql_string(context)
46 ))
47 )
48 }
49 Self::Begin(transaction_type, name) => {
50 let t_type = transaction_type.map_or("", |t_type| match t_type {
51 ast::TransactionType::Deferred => " DEFERRED",
52 ast::TransactionType::Exclusive => " EXCLUSIVE",
53 ast::TransactionType::Immediate => " IMMEDIATE",
54 });
55 format!(
56 "BEGIN{}{};",
57 t_type,
58 name.as_ref()
59 .map_or("".to_string(), |name| format!(" TRANSACTION {}", name.0))
60 )
61 }
62 Self::Commit(name) => format!(
64 "COMMIT{};",
65 name.as_ref()
66 .map_or("".to_string(), |name| format!(" TRANSACTION {}", name.0))
67 ),
68 Self::CreateIndex {
69 unique,
70 if_not_exists,
71 idx_name,
72 tbl_name,
73 columns,
74 where_clause,
75 } => format!(
76 "CREATE {}INDEX {}{} ON {} ({}){};",
77 unique.then_some("UNIQUE ").unwrap_or(""),
78 if_not_exists.then_some("IF NOT EXISTS ").unwrap_or(""),
79 idx_name.to_sql_string(context),
80 tbl_name.0,
81 columns
82 .iter()
83 .map(|col| col.to_sql_string(context))
84 .collect::<Vec<_>>()
85 .join(", "),
86 where_clause
87 .as_ref()
88 .map_or("".to_string(), |where_clause| format!(
89 " WHERE {}",
90 where_clause.to_sql_string(context)
91 ))
92 ),
93 Self::CreateTable {
94 temporary,
95 if_not_exists,
96 tbl_name,
97 body,
98 } => format!(
99 "CREATE{} TABLE {}{} {};",
100 temporary.then_some(" TEMP").unwrap_or(""),
101 if_not_exists.then_some("IF NOT EXISTS ").unwrap_or(""),
102 tbl_name.to_sql_string(context),
103 body.to_sql_string(context)
104 ),
105 Self::CreateTrigger(trigger) => trigger.to_sql_string(context),
106 Self::CreateView {
107 temporary,
108 if_not_exists,
109 view_name,
110 columns,
111 select,
112 } => {
113 format!(
114 "CREATE{} VIEW {}{}{} AS {};",
115 temporary.then_some(" TEMP").unwrap_or(""),
116 if_not_exists.then_some("IF NOT EXISTS ").unwrap_or(""),
117 view_name.to_sql_string(context),
118 columns.as_ref().map_or("".to_string(), |columns| format!(
119 " ({})",
120 columns
121 .iter()
122 .map(|col| col.to_string())
123 .collect::<Vec<_>>()
124 .join(", ")
125 )),
126 select.to_sql_string(context)
127 )
128 }
129 Self::CreateVirtualTable(create_virtual_table) => {
130 create_virtual_table.to_sql_string(context)
131 }
132 Self::Delete(delete) => delete.to_sql_string(context),
133 Self::Detach(name) => format!("DETACH {};", name.to_sql_string(context)),
134 Self::DropIndex {
135 if_exists,
136 idx_name,
137 } => format!(
138 "DROP INDEX{} {};",
139 if_exists.then_some("IF EXISTS ").unwrap_or(""),
140 idx_name.to_sql_string(context)
141 ),
142 Self::DropTable {
143 if_exists,
144 tbl_name,
145 } => format!(
146 "DROP TABLE{} {};",
147 if_exists.then_some("IF EXISTS ").unwrap_or(""),
148 tbl_name.to_sql_string(context)
149 ),
150 Self::DropTrigger {
151 if_exists,
152 trigger_name,
153 } => format!(
154 "DROP TRIGGER{} {};",
155 if_exists.then_some("IF EXISTS ").unwrap_or(""),
156 trigger_name.to_sql_string(context)
157 ),
158 Self::DropView {
159 if_exists,
160 view_name,
161 } => format!(
162 "DROP VIEW{} {};",
163 if_exists.then_some("IF EXISTS ").unwrap_or(""),
164 view_name.to_sql_string(context)
165 ),
166 Self::Insert(insert) => format!("{};", insert.to_sql_string(context)),
167 Self::Pragma(name, body) => format!(
168 "PRAGMA {}{};",
169 name.to_sql_string(context),
170 body.as_ref()
171 .map_or("".to_string(), |body| match body.as_ref() {
172 ast::PragmaBody::Equals(expr) =>
173 format!(" = {}", expr.to_sql_string(context)),
174 ast::PragmaBody::Call(expr) => format!("({})", expr.to_sql_string(context)),
175 })
176 ),
177 Self::Reindex { obj_name } => format!(
179 "REINDEX{};",
180 obj_name.as_ref().map_or("".to_string(), |name| format!(
181 " {}",
182 name.to_sql_string(context)
183 ))
184 ),
185 Self::Release(name) => format!("RELEASE {};", name.0),
186 Self::Rollback {
187 tx_name,
188 savepoint_name,
189 } => format!(
190 "ROLLBACK{}{};",
191 tx_name
192 .as_ref()
193 .map_or("".to_string(), |name| format!(" TRANSACTION {}", name.0)),
194 savepoint_name
195 .as_ref()
196 .map_or("".to_string(), |name| format!(" TO {}", name.0))
197 ),
198 Self::Savepoint(name) => format!("SAVEPOINT {};", name.0),
199 Self::Select(select) => format!("{};", select.to_sql_string(context)),
200 Self::Update(update) => format!("{};", update.to_sql_string(context)),
201 Self::Vacuum(name, expr) => {
202 format!(
203 "VACUUM{}{};",
204 name.as_ref()
205 .map_or("".to_string(), |name| format!(" {}", name.0)),
206 expr.as_ref().map_or("".to_string(), |expr| format!(
207 " INTO {}",
208 expr.to_sql_string(context)
209 ))
210 )
211 }
212 }
213 }
214}
215
216#[cfg(test)]
217pub(crate) mod tests {
218 use crate::to_sql_string::ToSqlContext;
219
220 #[macro_export]
221 macro_rules! to_sql_string_test {
223 ($test_name:ident, $input:expr) => {
224 #[test]
225 fn $test_name() {
226 let context = $crate::to_sql_string::stmt::tests::TestContext;
227 let input = $input.split_whitespace().collect::<Vec<&str>>().join(" ");
228 let mut parser = $crate::lexer::sql::Parser::new(input.as_bytes());
229 let cmd = fallible_iterator::FallibleIterator::next(&mut parser)
230 .unwrap()
231 .unwrap();
232 assert_eq!(
233 input,
234 $crate::to_sql_string::ToSqlString::to_sql_string(cmd.stmt(), &context)
235 );
236 }
237 };
238 ($test_name:ident, $input:expr, $($attribute:meta),*) => {
239 #[test]
240 $(#[$attribute])*
241 fn $test_name() {
242 let context = $crate::to_sql_string::stmt::tests::TestContext;
243 let input = $input.split_whitespace().collect::<Vec<&str>>().join(" ");
244 let mut parser = $crate::lexer::sql::Parser::new(input.as_bytes());
245 let cmd = fallible_iterator::FallibleIterator::next(&mut parser)
246 .unwrap()
247 .unwrap();
248 assert_eq!(
249 input,
250 $crate::to_sql_string::ToSqlString::to_sql_string(cmd.stmt(), &context)
251 );
252 }
253 }
254 }
255
256 pub(crate) struct TestContext;
257
258 impl ToSqlContext for TestContext {
261 fn get_column_name(
262 &self,
263 _table_id: crate::ast::TableReferenceId,
264 _col_idx: usize,
265 ) -> &str {
266 todo!()
267 }
268
269 fn get_table_name(&self, _id: crate::ast::TableReferenceId) -> &str {
270 todo!()
271 }
272 }
273
274 to_sql_string_test!(test_analyze, "ANALYZE;");
275
276 to_sql_string_test!(
277 test_analyze_table,
278 "ANALYZE table;",
279 ignore = "parser can't parse table name"
280 );
281
282 to_sql_string_test!(
283 test_analyze_schema_table,
284 "ANALYZE schema.table;",
285 ignore = "parser can't parse schema.table name"
286 );
287
288 to_sql_string_test!(test_attach, "ATTACH './test.db' AS test_db;");
289
290 to_sql_string_test!(test_attach_no_database_kw, "ATTACH './test.db' AS test_db;");
292
293 to_sql_string_test!(
294 test_attach_database_kw,
295 "ATTACH DATABASE './test.db' AS test_db;"
296 );
297
298 to_sql_string_test!(
299 test_attach_with_key,
300 "ATTACH './test.db' AS test_db KEY 'secret';"
301 );
302
303 to_sql_string_test!(
304 test_attach_database_kw_with_key,
305 "ATTACH DATABASE './test.db' AS test_db KEY 'secret';"
306 );
307
308 to_sql_string_test!(test_transaction, "BEGIN;");
309
310 to_sql_string_test!(test_transaction_with_name, "BEGIN TRANSACTION name;");
311
312 to_sql_string_test!(test_transaction_deferred, "BEGIN DEFERRED;");
313
314 to_sql_string_test!(test_transaction_immediate, "BEGIN IMMEDIATE;");
315
316 to_sql_string_test!(test_transaction_exclusive, "BEGIN EXCLUSIVE;");
317
318 to_sql_string_test!(test_commit, "COMMIT;");
319
320 to_sql_string_test!(test_commit_with_name, "COMMIT TRANSACTION name;");
321
322 to_sql_string_test!(
324 test_create_index_simple,
325 "CREATE INDEX idx_name ON employees (last_name);"
326 );
327
328 to_sql_string_test!(
330 test_create_unique_index,
331 "CREATE UNIQUE INDEX idx_unique_email ON users (email);"
332 );
333
334 to_sql_string_test!(
336 test_create_index_multi_column,
337 "CREATE INDEX idx_name_salary ON employees (last_name, salary);"
338 );
339
340 to_sql_string_test!(
342 test_create_partial_index,
343 "CREATE INDEX idx_active_users ON users (username) WHERE active = true;"
344 );
345
346 to_sql_string_test!(
348 test_create_index_on_expression,
349 "CREATE INDEX idx_upper_name ON employees (UPPER(last_name));"
350 );
351
352 to_sql_string_test!(
354 test_create_index_descending,
355 "CREATE INDEX idx_salary_desc ON employees (salary DESC);"
356 );
357
358 to_sql_string_test!(
360 test_create_index_mixed_order,
361 "CREATE INDEX idx_name_asc_salary_desc ON employees (last_name ASC, salary DESC);"
362 );
363
364 to_sql_string_test!(
366 test_create_view_distinct,
367 "CREATE VIEW view_distinct AS SELECT DISTINCT name FROM employees;"
368 );
369
370 to_sql_string_test!(
372 test_create_view_limit,
373 "CREATE VIEW view_limit AS SELECT id, name FROM employees LIMIT 10;"
374 );
375
376 to_sql_string_test!(
378 test_create_view_case,
379 "CREATE VIEW view_case AS SELECT name, CASE WHEN salary > 70000 THEN 'High' ELSE 'Low' END AS salary_level FROM employees;"
380 );
381
382 to_sql_string_test!(
384 test_create_view_left_join,
385 "CREATE VIEW view_left_join AS SELECT e.name, d.name AS department FROM employees e LEFT JOIN departments d ON e.department_id = d.id;"
386 );
387
388 to_sql_string_test!(
390 test_create_view_having,
391 "CREATE VIEW view_having AS SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) > 55000;"
392 );
393
394 to_sql_string_test!(
396 test_create_view_cte,
397 "CREATE VIEW view_cte AS WITH high_earners AS (SELECT * FROM employees WHERE salary > 80000) SELECT id, name FROM high_earners;"
398 );
399
400 to_sql_string_test!(
402 test_create_view_multi_where,
403 "CREATE VIEW view_multi_where AS SELECT id, name FROM employees WHERE salary > 50000 AND department_id = 3;"
404 );
405
406 to_sql_string_test!(
408 test_create_view_null,
409 "CREATE VIEW view_null AS SELECT name, COALESCE(salary, 0) AS salary FROM employees;"
410 );
411
412 to_sql_string_test!(
414 test_create_view_subquery_where,
415 "CREATE VIEW view_subquery_where AS SELECT name FROM employees WHERE department_id IN (SELECT id FROM departments WHERE name = 'Sales');"
416 );
417
418 to_sql_string_test!(
420 test_create_view_arithmetic,
421 "CREATE VIEW view_arithmetic AS SELECT name, salary * 1.1 AS adjusted_salary FROM employees;"
422 );
423
424 to_sql_string_test!(test_detach, "DETACH 'x.db';");
425
426 to_sql_string_test!(test_drop_index, "DROP INDEX schema_name.test_index;");
427
428 to_sql_string_test!(test_drop_table, "DROP TABLE schema_name.test_table;");
429
430 to_sql_string_test!(test_drop_trigger, "DROP TRIGGER schema_name.test_trigger;");
431
432 to_sql_string_test!(test_drop_view, "DROP VIEW schema_name.test_view;");
433
434 to_sql_string_test!(test_pragma_equals, "PRAGMA schema_name.Pragma_name = 1;");
435
436 to_sql_string_test!(test_pragma_call, "PRAGMA schema_name.Pragma_name_2(1);");
437
438 to_sql_string_test!(test_reindex, "REINDEX schema_name.test_table;");
439
440 to_sql_string_test!(test_reindex_2, "REINDEX;");
441
442 to_sql_string_test!(test_release, "RELEASE savepoint_name;");
443
444 to_sql_string_test!(test_rollback, "ROLLBACK;");
445
446 to_sql_string_test!(test_rollback_2, "ROLLBACK TO savepoint_name;");
447
448 to_sql_string_test!(test_rollback_with_name, "ROLLBACK TRANSACTION name;");
449
450 to_sql_string_test!(
451 test_rollback_with_name_and_savepoint,
452 "ROLLBACK TRANSACTION name TO savepoint_name;"
453 );
454
455 to_sql_string_test!(test_savepoint, "SAVEPOINT savepoint_name;");
456
457 to_sql_string_test!(test_vacuum, "VACUUM;");
458
459 to_sql_string_test!(test_vacuum_2, "VACUUM schema_name;");
460
461 to_sql_string_test!(test_vacuum_3, "VACUUM schema_name INTO test.db;");
462}