db_dsl/
lib.rs

1mod column;
2mod data_type;
3mod definition;
4mod foreign_key;
5mod index;
6mod sequence;
7mod table;
8mod table_definition;
9mod table_name;
10
11pub use column::{Column, ColumnOptions};
12pub use data_type::DataType;
13pub use definition::Definition;
14pub use foreign_key::ForeignKey;
15pub use index::{Index, IndexOptions};
16pub use table::{PrimaryKey, Table, TableOptions, Timestamps};
17pub use table_definition::TableDefinition;
18pub use table_name::TableName;
19
20#[derive(Copy, Clone, Debug)]
21pub enum Dialect {
22    Sqlite,
23}
24
25pub trait SqlUp {
26    fn sqlite_up(&self) -> Option<String> {
27        None
28    }
29
30    fn sqlite(&self) -> Option<String> {
31        self.sql_up(Dialect::Sqlite)
32    }
33
34    fn sql_up(&self, dialect: Dialect) -> Option<String> {
35        match dialect {
36            Dialect::Sqlite => self.sqlite_up(),
37        }
38    }
39}
40
41pub trait SqlDown {
42    fn sqlite_down(&self) -> Option<String> {
43        None
44    }
45
46    fn sql_down(&self, dialect: Dialect) -> Option<String> {
47        match dialect {
48            Dialect::Sqlite => self.sqlite_down(),
49        }
50    }
51}
52
53#[cfg(test)]
54macro_rules! create_tests {
55    ($($name:ident: $value:expr,)*) => {
56        $(
57            #[test]
58            fn $name() {
59                let (input, expected) = $value;
60                assert_eq!(input.sql_up(Dialect::Sqlite).unwrap().as_str(), expected.trim());
61            }
62        )*
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    create_tests!(
71        test_table: (
72            Definition::table("users", vec![
73                TableDefinition::Column(Column::new("name")),
74            ]), r#"
75            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)
76        "#),
77        test_default_values_for_column: (Definition::column("users", DataType::String.column("name")), r#"
78            ALTER TABLE users ADD COLUMN name STRING
79        "#),
80        test_default_not_null_column: (Definition::column("users", Column::new("name").not_null().to_owned()), r#"
81            ALTER TABLE users ADD COLUMN name STRING NOT NULL
82        "#),
83        test_index: (Definition::index("users", Index::new(vec!["name".to_string()])), r#"
84            CREATE INDEX name ON users (name)
85        "#),
86        test_foreign_key: (Definition::foreign_key("users", ForeignKey::new("other")), r#"
87            ALTER TABLE users ADD FOREIGN KEY (other_id) REFERENCES other (id)
88        "#),
89    );
90
91    #[test]
92    fn integration_test() {
93        let definition = vec![
94            Definition::table_no_timestamps(
95                "changeloglock",
96                vec![
97                    TableDefinition::column(
98                        DataType::Bool
99                            .column("locked")
100                            .not_null()
101                            .default(serde_value::Value::Bool(false))
102                            .to_owned(),
103                    ),
104                    TableDefinition::column(
105                        DataType::Text.column("locked_by").not_null().to_owned(),
106                    ),
107                    TableDefinition::column(
108                        DataType::Datetime
109                            .column("locked_at")
110                            .not_null()
111                            .default_raw("CURRENT_TIMESTAMP".to_string())
112                            .to_owned(),
113                    ),
114                ],
115            ),
116            Definition::statement(
117                "INSERT INTO changeloglock (id, locked, locked_by) VALUES (1, false, '')",
118            ),
119            Definition::table_no_timestamps(
120                "migrations",
121                vec![
122                    TableDefinition::column(
123                        DataType::Text.column("identifier").not_null().to_owned(),
124                    ),
125                    TableDefinition::column(
126                        DataType::Datetime
127                            .column("applied_at")
128                            .not_null()
129                            .default_raw("CURRENT_TIMESTAMP".to_string())
130                            .to_owned(),
131                    ),
132                ],
133            ),
134        ];
135        assert_eq!(definition.iter().sql_up(Dialect::Sqlite).unwrap().as_str(), vec![
136            "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);",
137            "INSERT INTO changeloglock (id, locked, locked_by) VALUES (1, false, '');",
138            "CREATE TABLE IF NOT EXISTS migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, identifier TEXT NOT NULL, applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)"
139        ].join(" ").trim());
140
141        let connection = rusqlite::Connection::open_in_memory().unwrap();
142        connection.execute_batch(definition.iter().sqlite().unwrap().as_str()).unwrap();
143        let mut stmt = connection.prepare("SELECT name FROM sqlite_master WHERE type='table'").unwrap();
144        let tables = stmt.query_map([], |row| row.get(0)).unwrap().collect::<Result<Vec<String>, _>>().unwrap();
145        assert_eq!(tables, vec!["changeloglock", "sqlite_sequence", "migrations"]);
146
147        let mut stmt = connection.prepare("SELECT * FROM changeloglock").unwrap();
148        let rows = stmt.query_map([], |row| {
149            Ok((row.get(0)?, row.get(1)?, row.get(2)?))
150        }).unwrap().collect::<Result<Vec<(i64, bool, String)>, _>>().unwrap();
151        assert_eq!(rows, vec![(1, false, "".to_string())]);
152    }
153}