use crate::ast;
use super::ToSqlString;
mod alter_table;
mod create_table;
mod create_trigger;
mod create_virtual_table;
mod delete;
mod insert;
mod select;
mod update;
impl ToSqlString for ast::Stmt {
fn to_sql_string<C: super::ToSqlContext>(&self, context: &C) -> String {
match self {
Self::AlterTable(alter_table) => {
let (name, body) = alter_table.as_ref();
format!(
"ALTER TABLE {} {};",
name.to_sql_string(context),
body.to_sql_string(context)
)
}
Self::Analyze(name) => {
if let Some(name) = name {
format!("ANALYZE {};", name.to_sql_string(context))
} else {
format!("ANALYZE;")
}
}
Self::Attach {
expr,
db_name,
key: _,
} => {
format!(
"ATTACH {} AS {};",
expr.to_sql_string(context),
db_name.to_sql_string(context)
)
}
Self::Begin(transaction_type, _name) => {
let t_type = transaction_type.map_or("", |t_type| match t_type {
ast::TransactionType::Deferred => " DEFERRED",
ast::TransactionType::Exclusive => " EXCLUSIVE",
ast::TransactionType::Immediate => " IMMEDIATE",
});
format!("BEGIN{};", t_type)
}
Self::Commit(_name) => "COMMIT;".to_string(),
Self::CreateIndex {
unique,
if_not_exists,
idx_name,
tbl_name,
columns,
where_clause,
} => format!(
"CREATE {}INDEX {}{} ON {} ({}){};",
unique.then_some("UNIQUE ").unwrap_or(""),
if_not_exists.then_some("IF NOT EXISTS ").unwrap_or(""),
idx_name.to_sql_string(context),
tbl_name.0,
columns
.iter()
.map(|col| col.to_sql_string(context))
.collect::<Vec<_>>()
.join(", "),
where_clause
.as_ref()
.map_or("".to_string(), |where_clause| format!(
" WHERE {}",
where_clause.to_sql_string(context)
))
),
Self::CreateTable {
temporary,
if_not_exists,
tbl_name,
body,
} => format!(
"CREATE{} TABLE {}{} {};",
temporary.then_some(" TEMP").unwrap_or(""),
if_not_exists.then_some("IF NOT EXISTS ").unwrap_or(""),
tbl_name.to_sql_string(context),
body.to_sql_string(context)
),
Self::CreateTrigger(trigger) => trigger.to_sql_string(context),
Self::CreateView {
temporary,
if_not_exists,
view_name,
columns,
select,
} => {
format!(
"CREATE{} VIEW {}{}{} AS {};",
temporary.then_some(" TEMP").unwrap_or(""),
if_not_exists.then_some("IF NOT EXISTS ").unwrap_or(""),
view_name.to_sql_string(context),
columns.as_ref().map_or("".to_string(), |columns| format!(
" ({})",
columns
.iter()
.map(|col| col.to_string())
.collect::<Vec<_>>()
.join(", ")
)),
select.to_sql_string(context)
)
}
Self::CreateVirtualTable(create_virtual_table) => {
create_virtual_table.to_sql_string(context)
}
Self::Delete(delete) => delete.to_sql_string(context),
Self::Detach(name) => format!("DETACH {};", name.to_sql_string(context)),
Self::DropIndex {
if_exists,
idx_name,
} => format!(
"DROP INDEX{} {};",
if_exists.then_some("IF EXISTS ").unwrap_or(""),
idx_name.to_sql_string(context)
),
Self::DropTable {
if_exists,
tbl_name,
} => format!(
"DROP TABLE{} {};",
if_exists.then_some("IF EXISTS ").unwrap_or(""),
tbl_name.to_sql_string(context)
),
Self::DropTrigger {
if_exists,
trigger_name,
} => format!(
"DROP TRIGGER{} {};",
if_exists.then_some("IF EXISTS ").unwrap_or(""),
trigger_name.to_sql_string(context)
),
Self::DropView {
if_exists,
view_name,
} => format!(
"DROP VIEW{} {};",
if_exists.then_some("IF EXISTS ").unwrap_or(""),
view_name.to_sql_string(context)
),
Self::Insert(insert) => format!("{};", insert.to_sql_string(context)),
Self::Pragma(name, body) => format!(
"PRAGMA {}{};",
name.to_sql_string(context),
body.as_ref()
.map_or("".to_string(), |body| match body.as_ref() {
ast::PragmaBody::Equals(expr) =>
format!(" = {}", expr.to_sql_string(context)),
ast::PragmaBody::Call(expr) => format!("({})", expr.to_sql_string(context)),
})
),
Self::Reindex { obj_name } => format!(
"REINDEX{};",
obj_name.as_ref().map_or("".to_string(), |name| format!(
" {}",
name.to_sql_string(context)
))
),
Self::Release(name) => format!("RELEASE {};", name.0),
Self::Rollback {
tx_name: _,
savepoint_name,
} => format!(
"ROLLBACK{};",
savepoint_name
.as_ref()
.map_or("".to_string(), |name| format!(" TO {}", name.0))
),
Self::Savepoint(name) => format!("SAVEPOINT {};", name.0),
Self::Select(select) => format!("{};", select.to_sql_string(context)),
Self::Update(update) => format!("{};", update.to_sql_string(context)),
Self::Vacuum(name, expr) => {
format!(
"VACUUM{}{};",
name.as_ref()
.map_or("".to_string(), |name| format!(" {}", name.0)),
expr.as_ref().map_or("".to_string(), |expr| format!(
" INTO {}",
expr.to_sql_string(context)
))
)
}
}
}
}
#[cfg(test)]
mod tests {
use crate::to_sql_string::ToSqlContext;
#[macro_export]
macro_rules! to_sql_string_test {
($test_name:ident, $input:expr) => {
#[test]
fn $test_name() {
let context = crate::to_sql_string::stmt::tests::TestContext;
let input = $input.split_whitespace().collect::<Vec<&str>>().join(" ");
let mut parser = crate::lexer::sql::Parser::new(input.as_bytes());
let cmd = fallible_iterator::FallibleIterator::next(&mut parser)
.unwrap()
.unwrap();
assert_eq!(
input,
crate::to_sql_string::ToSqlString::to_sql_string(cmd.stmt(), &context)
);
}
};
($test_name:ident, $input:expr, $($attribute:meta),*) => {
#[test]
$(#[$attribute])*
fn $test_name() {
let context = crate::to_sql_string::stmt::tests::TestContext;
let input = $input.split_whitespace().collect::<Vec<&str>>().join(" ");
let mut parser = crate::lexer::sql::Parser::new(input.as_bytes());
let cmd = fallible_iterator::FallibleIterator::next(&mut parser)
.unwrap()
.unwrap();
assert_eq!(
input,
crate::to_sql_string::ToSqlString::to_sql_string(cmd.stmt(), &context)
);
}
}
}
pub(crate) struct TestContext;
impl ToSqlContext for TestContext {
fn get_column_name(&self, _table_id: crate::ast::TableInternalId, _col_idx: usize) -> &str {
todo!()
}
fn get_table_name(&self, _id: crate::ast::TableInternalId) -> &str {
todo!()
}
}
to_sql_string_test!(test_analyze, "ANALYZE;");
to_sql_string_test!(
test_analyze_table,
"ANALYZE table;",
ignore = "parser can't parse table name"
);
to_sql_string_test!(
test_analyze_schema_table,
"ANALYZE schema.table;",
ignore = "parser can't parse schema.table name"
);
to_sql_string_test!(test_attach, "ATTACH './test.db' AS test_db;");
to_sql_string_test!(test_transaction, "BEGIN;");
to_sql_string_test!(test_transaction_deferred, "BEGIN DEFERRED;");
to_sql_string_test!(test_transaction_immediate, "BEGIN IMMEDIATE;");
to_sql_string_test!(test_transaction_exclusive, "BEGIN EXCLUSIVE;");
to_sql_string_test!(test_commit, "COMMIT;");
to_sql_string_test!(
test_create_index_simple,
"CREATE INDEX idx_name ON employees (last_name);"
);
to_sql_string_test!(
test_create_unique_index,
"CREATE UNIQUE INDEX idx_unique_email ON users (email);"
);
to_sql_string_test!(
test_create_index_multi_column,
"CREATE INDEX idx_name_salary ON employees (last_name, salary);"
);
to_sql_string_test!(
test_create_partial_index,
"CREATE INDEX idx_active_users ON users (username) WHERE active = true;"
);
to_sql_string_test!(
test_create_index_on_expression,
"CREATE INDEX idx_upper_name ON employees (UPPER(last_name));"
);
to_sql_string_test!(
test_create_index_descending,
"CREATE INDEX idx_salary_desc ON employees (salary DESC);"
);
to_sql_string_test!(
test_create_index_mixed_order,
"CREATE INDEX idx_name_asc_salary_desc ON employees (last_name ASC, salary DESC);"
);
to_sql_string_test!(
test_create_view_distinct,
"CREATE VIEW view_distinct AS SELECT DISTINCT name FROM employees;"
);
to_sql_string_test!(
test_create_view_limit,
"CREATE VIEW view_limit AS SELECT id, name FROM employees LIMIT 10;"
);
to_sql_string_test!(
test_create_view_case,
"CREATE VIEW view_case AS SELECT name, CASE WHEN salary > 70000 THEN 'High' ELSE 'Low' END AS salary_level FROM employees;"
);
to_sql_string_test!(
test_create_view_left_join,
"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;"
);
to_sql_string_test!(
test_create_view_having,
"CREATE VIEW view_having AS SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) > 55000;"
);
to_sql_string_test!(
test_create_view_cte,
"CREATE VIEW view_cte AS WITH high_earners AS (SELECT * FROM employees WHERE salary > 80000) SELECT id, name FROM high_earners;"
);
to_sql_string_test!(
test_create_view_multi_where,
"CREATE VIEW view_multi_where AS SELECT id, name FROM employees WHERE salary > 50000 AND department_id = 3;"
);
to_sql_string_test!(
test_create_view_null,
"CREATE VIEW view_null AS SELECT name, COALESCE(salary, 0) AS salary FROM employees;"
);
to_sql_string_test!(
test_create_view_subquery_where,
"CREATE VIEW view_subquery_where AS SELECT name FROM employees WHERE department_id IN (SELECT id FROM departments WHERE name = 'Sales');"
);
to_sql_string_test!(
test_create_view_arithmetic,
"CREATE VIEW view_arithmetic AS SELECT name, salary * 1.1 AS adjusted_salary FROM employees;"
);
to_sql_string_test!(test_detach, "DETACH 'x.db';");
to_sql_string_test!(test_drop_index, "DROP INDEX schema_name.test_index;");
to_sql_string_test!(test_drop_table, "DROP TABLE schema_name.test_table;");
to_sql_string_test!(test_drop_trigger, "DROP TRIGGER schema_name.test_trigger;");
to_sql_string_test!(test_drop_view, "DROP VIEW schema_name.test_view;");
to_sql_string_test!(test_pragma_equals, "PRAGMA schema_name.Pragma_name = 1;");
to_sql_string_test!(test_pragma_call, "PRAGMA schema_name.Pragma_name_2(1);");
to_sql_string_test!(test_reindex, "REINDEX schema_name.test_table;");
to_sql_string_test!(test_reindex_2, "REINDEX;");
to_sql_string_test!(test_release, "RELEASE savepoint_name;");
to_sql_string_test!(test_rollback, "ROLLBACK;");
to_sql_string_test!(test_rollback_2, "ROLLBACK TO savepoint_name;");
to_sql_string_test!(test_savepoint, "SAVEPOINT savepoint_name;");
to_sql_string_test!(test_vacuum, "VACUUM;");
to_sql_string_test!(test_vacuum_2, "VACUUM schema_name;");
to_sql_string_test!(test_vacuum_3, "VACUUM schema_name INTO test.db;");
}