limbo_sqlite3_parser 0.0.22

SQL parser (as understood by SQLite)
Documentation
use std::fmt::Display;

use crate::{ast, to_sql_string::ToSqlString};

impl ToSqlString for ast::CreateTableBody {
    fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
        match self {
            Self::AsSelect(select) => format!("AS {}", select.to_sql_string(context)),
            Self::ColumnsAndConstraints {
                columns,
                constraints,
                options,
            } => {
                format!(
                    "({}{}){}",
                    columns
                        .iter()
                        .map(|(_, col)| col.to_sql_string(context))
                        .collect::<Vec<_>>()
                        .join(", "),
                    constraints
                        .as_ref()
                        .map_or("".to_string(), |constraints| format!(
                            ", {}",
                            constraints
                                .iter()
                                .map(|constraint| constraint.to_sql_string(context))
                                .collect::<Vec<_>>()
                                .join(", ")
                        )),
                    options
                )
            }
        }
    }
}

impl ToSqlString for ast::NamedTableConstraint {
    fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
        if let Some(name) = &self.name {
            format!(
                "CONSTRAINT {} {}",
                name.0,
                self.constraint.to_sql_string(context)
            )
        } else {
            format!("{}", self.constraint.to_sql_string(context))
        }
    }
}

impl ToSqlString for ast::TableConstraint {
    fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
        match self {
            Self::Check(expr) => format!("CHECK ({})", expr.to_sql_string(context)),
            Self::ForeignKey {
                columns,
                clause,
                deref_clause,
            } => format!(
                "FOREIGN KEY ({}) {}{}",
                columns
                    .iter()
                    .map(|col| col.to_string())
                    .collect::<Vec<_>>()
                    .join(", "),
                clause,
                if let Some(deref) = deref_clause {
                    deref.to_string()
                } else {
                    "".to_string()
                }
            ),
            Self::PrimaryKey {
                columns,
                auto_increment,
                conflict_clause,
            } => format!(
                "PRIMARY KEY ({}){}{}",
                columns
                    .iter()
                    .map(|col| col.to_sql_string(context))
                    .collect::<Vec<_>>()
                    .join(", "),
                conflict_clause.map_or("".to_string(), |conflict| format!(" {}", conflict)),
                auto_increment.then_some(" AUTOINCREMENT").unwrap_or("")
            ),
            Self::Unique {
                columns,
                conflict_clause,
            } => format!(
                "UNIQUE ({}){}",
                columns
                    .iter()
                    .map(|col| col.to_sql_string(context))
                    .collect::<Vec<_>>()
                    .join(", "),
                conflict_clause.map_or("".to_string(), |conflict| format!(" {}", conflict))
            ),
        }
    }
}

impl Display for ast::TableOptions {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            if *self == Self::NONE {
                ""
            } else if *self == Self::STRICT {
                " STRICT"
            } else {
                " WITHOUT ROWID"
            }
        )
    }
}

#[cfg(test)]
mod tests {
    use crate::to_sql_string_test;

    to_sql_string_test!(
        test_create_table_simple,
        "CREATE TABLE t (a INTEGER, b TEXT);"
    );

    to_sql_string_test!(
        test_create_table_primary_key,
        "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);"
    );

    to_sql_string_test!(
        test_create_table_multi_primary_key,
        "CREATE TABLE t (a INTEGER, b TEXT, PRIMARY KEY (a, b));"
    );

    to_sql_string_test!(
        test_create_table_data_types,
        "CREATE TABLE t (a INTEGER, b TEXT, c REAL, d BLOB, e NUMERIC);"
    );

    to_sql_string_test!(
        test_create_table_foreign_key,
        "CREATE TABLE t2 (id INTEGER PRIMARY KEY, t_id INTEGER, FOREIGN KEY (t_id) REFERENCES t(id));"
    );

    to_sql_string_test!(
        test_create_table_foreign_key_cascade,
        "CREATE TABLE t2 (id INTEGER PRIMARY KEY, t_id INTEGER, FOREIGN KEY (t_id) REFERENCES t(id) ON DELETE CASCADE);"
    );

    to_sql_string_test!(
        test_create_table_unique,
        "CREATE TABLE t (a INTEGER UNIQUE, b TEXT);"
    );

    to_sql_string_test!(
        test_create_table_not_null,
        "CREATE TABLE t (a INTEGER NOT NULL, b TEXT);"
    );

    to_sql_string_test!(
        test_create_table_check,
        "CREATE TABLE t (a INTEGER CHECK (a > 0), b TEXT);"
    );

    to_sql_string_test!(
        test_create_table_default,
        "CREATE TABLE t (a INTEGER DEFAULT 0, b TEXT);"
    );

    to_sql_string_test!(
        test_create_table_multiple_constraints,
        "CREATE TABLE t (a INTEGER NOT NULL UNIQUE, b TEXT DEFAULT 'default');"
    );

    to_sql_string_test!(
        test_create_table_generated_column,
        "CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER AS (a + b));"
    );

    to_sql_string_test!(
        test_create_table_generated_stored,
        "CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER AS (a + b) STORED);"
    );

    to_sql_string_test!(
        test_create_table_generated_virtual,
        "CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER AS (a + b) VIRTUAL);"
    );

    to_sql_string_test!(
        test_create_table_quoted_columns,
        "CREATE TABLE t (\"select\" INTEGER, \"from\" TEXT);"
    );

    to_sql_string_test!(
        test_create_table_quoted_table,
        "CREATE TABLE \"my table\" (a INTEGER);"
    );

    to_sql_string_test!(
        test_create_table_if_not_exists,
        "CREATE TABLE IF NOT EXISTS t (a INTEGER);"
    );

    to_sql_string_test!(test_create_temp_table, "CREATE TEMP TABLE t (a INTEGER);");

    to_sql_string_test!(
        test_create_table_without_rowid,
        "CREATE TABLE t (a INTEGER PRIMARY KEY, b TEXT) WITHOUT ROWID;"
    );

    to_sql_string_test!(
        test_create_table_named_primary_key,
        "CREATE TABLE t (a INTEGER CONSTRAINT pk_a PRIMARY KEY);"
    );

    to_sql_string_test!(
        test_create_table_named_unique,
        "CREATE TABLE t (a INTEGER, CONSTRAINT unique_a UNIQUE (a));"
    );

    to_sql_string_test!(
        test_create_table_named_foreign_key,
        "CREATE TABLE t2 (id INTEGER, t_id INTEGER, CONSTRAINT fk_t FOREIGN KEY (t_id) REFERENCES t(id));"
    );

    to_sql_string_test!(
        test_create_table_complex,
        "CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER NOT NULL, b TEXT DEFAULT 'default', c INTEGER AS (a * 2), CONSTRAINT unique_a UNIQUE (a));"
    );

    to_sql_string_test!(
        test_create_table_multiple_foreign_keys,
        "CREATE TABLE t3 (id INTEGER PRIMARY KEY, t1_id INTEGER, t2_id INTEGER, FOREIGN KEY (t1_id) REFERENCES t1(id), FOREIGN KEY (t2_id) REFERENCES t2(id));"
    );
}