mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-30 06:24:21 +01:00
319 lines
11 KiB
Rust
319 lines
11 KiB
Rust
use crate::ast;
|
|
|
|
use super::ToSqlString;
|
|
|
|
mod alter_table;
|
|
mod create_table;
|
|
mod create_trigger;
|
|
mod create_virtual_table;
|
|
mod delete;
|
|
mod select;
|
|
|
|
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: _,
|
|
} => {
|
|
// TODO: what is `key` in the attach syntax?
|
|
format!(
|
|
"ATTACH {} AS {};",
|
|
expr.to_sql_string(context),
|
|
db_name.to_sql_string(context)
|
|
)
|
|
}
|
|
// TODO: not sure where name is applied here
|
|
// https://www.sqlite.org/lang_transaction.html
|
|
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)
|
|
}
|
|
// END or COMMIT are equivalent here, so just defaulting to COMMIT
|
|
// TODO: again there are no names in the docs
|
|
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_sql_string(context))
|
|
.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::Select(select) => format!("{};", select.to_sql_string(context)),
|
|
_ => todo!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::to_sql_string::ToSqlContext;
|
|
|
|
#[macro_export]
|
|
/// Create a test that first parses then input, the converts the parsed ast back to a string and compares with original input
|
|
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;
|
|
|
|
// Placeholders for compilation
|
|
// Context only necessary parsing inside limbo_core or in the simulator
|
|
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;");
|
|
|
|
// Test a simple index on a single column
|
|
to_sql_string_test!(
|
|
test_create_index_simple,
|
|
"CREATE INDEX idx_name ON employees (last_name);"
|
|
);
|
|
|
|
// Test a unique index to enforce uniqueness on a column
|
|
to_sql_string_test!(
|
|
test_create_unique_index,
|
|
"CREATE UNIQUE INDEX idx_unique_email ON users (email);"
|
|
);
|
|
|
|
// Test a multi-column index
|
|
to_sql_string_test!(
|
|
test_create_index_multi_column,
|
|
"CREATE INDEX idx_name_salary ON employees (last_name, salary);"
|
|
);
|
|
|
|
// Test a partial index with a WHERE clause
|
|
to_sql_string_test!(
|
|
test_create_partial_index,
|
|
"CREATE INDEX idx_active_users ON users (username) WHERE active = true;"
|
|
);
|
|
|
|
// Test an index on an expression
|
|
to_sql_string_test!(
|
|
test_create_index_on_expression,
|
|
"CREATE INDEX idx_upper_name ON employees (UPPER(last_name));"
|
|
);
|
|
|
|
// Test an index with descending order
|
|
to_sql_string_test!(
|
|
test_create_index_descending,
|
|
"CREATE INDEX idx_salary_desc ON employees (salary DESC);"
|
|
);
|
|
|
|
// Test an index with mixed ascending and descending orders on multiple columns
|
|
to_sql_string_test!(
|
|
test_create_index_mixed_order,
|
|
"CREATE INDEX idx_name_asc_salary_desc ON employees (last_name ASC, salary DESC);"
|
|
);
|
|
|
|
// Test 1: View with DISTINCT keyword
|
|
to_sql_string_test!(
|
|
test_create_view_distinct,
|
|
"CREATE VIEW view_distinct AS SELECT DISTINCT name FROM employees;"
|
|
);
|
|
|
|
// Test 2: View with LIMIT clause
|
|
to_sql_string_test!(
|
|
test_create_view_limit,
|
|
"CREATE VIEW view_limit AS SELECT id, name FROM employees LIMIT 10;"
|
|
);
|
|
|
|
// Test 3: View with CASE expression
|
|
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;"
|
|
);
|
|
|
|
// Test 4: View with LEFT JOIN
|
|
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;"
|
|
);
|
|
|
|
// Test 5: View with HAVING clause
|
|
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;"
|
|
);
|
|
|
|
// Test 6: View with CTE (Common Table Expression)
|
|
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;"
|
|
);
|
|
|
|
// Test 7: View with multiple conditions in WHERE
|
|
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;"
|
|
);
|
|
|
|
// Test 8: View with NULL handling
|
|
to_sql_string_test!(
|
|
test_create_view_null,
|
|
"CREATE VIEW view_null AS SELECT name, COALESCE(salary, 0) AS salary FROM employees;"
|
|
);
|
|
|
|
// Test 9: View with subquery in WHERE clause
|
|
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');"
|
|
);
|
|
|
|
// Test 10: View with arithmetic expression
|
|
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';"
|
|
);
|
|
}
|