db_dsl 0.3.3

A simple DSL for creating database objects.
Documentation
mod column;
mod data_type;
mod definition;
mod foreign_key;
mod index;
mod sequence;
mod table;
mod table_definition;
mod table_name;

pub use column::{Column, ColumnOptions};
pub use data_type::DataType;
pub use definition::Definition;
pub use foreign_key::ForeignKey;
pub use index::{Index, IndexOptions};
pub use table::{PrimaryKey, Table, TableOptions, Timestamps};
pub use table_definition::TableDefinition;
pub use table_name::TableName;

#[derive(Copy, Clone, Debug)]
pub enum Dialect {
    Sqlite,
}

pub trait SqlUp {
    fn sqlite_up(&self) -> Option<String> {
        None
    }

    fn sqlite(&self) -> Option<String> {
        self.sql_up(Dialect::Sqlite)
    }

    fn sql_up(&self, dialect: Dialect) -> Option<String> {
        match dialect {
            Dialect::Sqlite => self.sqlite_up(),
        }
    }
}

pub trait SqlDown {
    fn sqlite_down(&self) -> Option<String> {
        None
    }

    fn sql_down(&self, dialect: Dialect) -> Option<String> {
        match dialect {
            Dialect::Sqlite => self.sqlite_down(),
        }
    }
}

#[cfg(test)]
macro_rules! create_tests {
    ($($name:ident: $value:expr,)*) => {
        $(
            #[test]
            fn $name() {
                let (input, expected) = $value;
                assert_eq!(input.sql_up(Dialect::Sqlite).unwrap().as_str(), expected.trim());
            }
        )*
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    create_tests!(
        test_table: (
            Definition::table("users", vec![
                TableDefinition::Column(Column::new("name")),
            ]), r#"
            CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)
        "#),
        test_default_values_for_column: (Definition::column("users", DataType::String.column("name")), r#"
            ALTER TABLE users ADD COLUMN name STRING
        "#),
        test_default_not_null_column: (Definition::column("users", Column::new("name").not_null().to_owned()), r#"
            ALTER TABLE users ADD COLUMN name STRING NOT NULL
        "#),
        test_index: (Definition::index("users", Index::new(vec!["name".to_string()])), r#"
            CREATE INDEX name ON users (name)
        "#),
        test_foreign_key: (Definition::foreign_key("users", ForeignKey::new("other")), r#"
            ALTER TABLE users ADD FOREIGN KEY (other_id) REFERENCES other (id)
        "#),
    );

    #[test]
    fn integration_test() {
        let definition = vec![
            Definition::table_no_timestamps(
                "changeloglock",
                vec![
                    TableDefinition::column(
                        DataType::Bool
                            .column("locked")
                            .not_null()
                            .default(serde_value::Value::Bool(false))
                            .to_owned(),
                    ),
                    TableDefinition::column(
                        DataType::Text.column("locked_by").not_null().to_owned(),
                    ),
                    TableDefinition::column(
                        DataType::Datetime
                            .column("locked_at")
                            .not_null()
                            .default_raw("CURRENT_TIMESTAMP".to_string())
                            .to_owned(),
                    ),
                ],
            ),
            Definition::statement(
                "INSERT INTO changeloglock (id, locked, locked_by) VALUES (1, false, '')",
            ),
            Definition::table_no_timestamps(
                "migrations",
                vec![
                    TableDefinition::column(
                        DataType::Text.column("identifier").not_null().to_owned(),
                    ),
                    TableDefinition::column(
                        DataType::Datetime
                            .column("applied_at")
                            .not_null()
                            .default_raw("CURRENT_TIMESTAMP".to_string())
                            .to_owned(),
                    ),
                ],
            ),
        ];
        assert_eq!(definition.iter().sql_up(Dialect::Sqlite).unwrap().as_str(), vec![
            "CREATE TABLE IF NOT EXISTS changeloglock (id INTEGER PRIMARY KEY AUTOINCREMENT, locked BOOLEAN NOT NULL DEFAULT FALSE, locked_by TEXT NOT NULL, locked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP);",
            "INSERT INTO changeloglock (id, locked, locked_by) VALUES (1, false, '');",
            "CREATE TABLE IF NOT EXISTS migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, identifier TEXT NOT NULL, applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)"
        ].join(" ").trim());

        let connection = rusqlite::Connection::open_in_memory().unwrap();
        connection.execute_batch(definition.iter().sqlite().unwrap().as_str()).unwrap();
        let mut stmt = connection.prepare("SELECT name FROM sqlite_master WHERE type='table'").unwrap();
        let tables = stmt.query_map([], |row| row.get(0)).unwrap().collect::<Result<Vec<String>, _>>().unwrap();
        assert_eq!(tables, vec!["changeloglock", "sqlite_sequence", "migrations"]);

        let mut stmt = connection.prepare("SELECT * FROM changeloglock").unwrap();
        let rows = stmt.query_map([], |row| {
            Ok((row.get(0)?, row.get(1)?, row.get(2)?))
        }).unwrap().collect::<Result<Vec<(i64, bool, String)>, _>>().unwrap();
        assert_eq!(rows, vec![(1, false, "".to_string())]);
    }
}