turso_sqlite3_parser 0.1.5

SQL parser (as understood by SQLite)
Documentation
mod alter_table;
mod create_table;
mod create_trigger;
mod create_virtual_table;
mod delete;
mod insert;
mod select;
mod update;

#[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() {
                use $crate::parser::ast::fmt::ToTokens;
                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,
                    cmd.stmt().format_with_context(&context).unwrap().replace('\n', " "),
                );
            }
        };
        ($test_name:ident, $input:expr, $($attribute:meta),*) => {
            #[test]
            $(#[$attribute])*
            fn $test_name() {
                use $crate::parser::ast::fmt::ToTokens;
                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,
                    cmd.stmt().format_with_context(&context).unwrap().replace('\n', " "),
                );
            }
        }
    }

    pub(crate) struct TestContext;

    // Placeholders for compilation
    // Context only necessary parsing inside turso_core or in the simulator
    impl ToSqlContext for TestContext {
        fn get_column_name(
            &self,
            _table_id: crate::ast::TableInternalId,
            _col_idx: usize,
        ) -> String {
            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 OUTER 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'");

    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");
}