diff --git a/example.json b/example.json index dc8a8e2..12c7565 100644 --- a/example.json +++ b/example.json @@ -1,16 +1,4 @@ [ - { - "name": "products", - "strict": false, - "columns": [ - { - "name": "id", - "datatype": "Integer", - "primary_key": true, - "auto_increment": true - } - ] - }, { "name": "users", "strict": true, @@ -22,18 +10,68 @@ "auto_increment": true }, { - "name": "name", + "name": "username", "datatype": "Text", - "unique": true - }, - { - "name": "password", - "datatype": "Text" + "unique": true, + "nullable": false }, { "name": "email", "datatype": "Text", - "unique": true + "unique": true, + "nullable": false + }, + { + "name": "password_hash", + "datatype": "Text", + "nullable": false + }, + { + "name": "created_at", + "datatype": "Text", + "default": "CURRENT_TIMESTAMP", + "nullable": false + } + ] + }, + { + "name": "posts", + "strict": true, + "columns": [ + { + "name": "id", + "datatype": "Integer", + "primary_key": true, + "auto_increment": true + }, + { + "name": "user_id", + "datatype": "Integer", + "nullable": false + }, + { + "name": "title", + "datatype": "Text", + "nullable": false + }, + { + "name": "content", + "datatype": "Text", + "nullable": false + }, + { + "name": "created_at", + "datatype": "Text", + "default": "CURRENT_TIMESTAMP", + "nullable": false + } + ], + "foreign_keys": [ + { + "name": "fk_posts_user", + "table_column_name": "user_id", + "other_table_name": "users", + "other_table_column_name": "id" } ] } diff --git a/src/databases/mod.rs b/src/databases/mod.rs index 7943b3b..62244f9 100644 --- a/src/databases/mod.rs +++ b/src/databases/mod.rs @@ -4,8 +4,8 @@ pub fn connect(db_connection: String) -> Result { Connection::open(db_connection) } -pub fn run_command(connection: &Connection, sql: String) -> Result { - connection.execute(sql.as_str(), []) +pub fn run_command(connection: &Connection, sql: String) -> Result<()> { + connection.execute_batch(sql.as_str()) } pub fn disconnect(connection: Connection) { diff --git a/src/lib.rs b/src/lib.rs index 02514c0..9d3acae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ mod tests { fn test_db_builder() { let facade = DbBuilderFacade{}; - match facade.build("./example.json".to_string(), "test.db".to_string()) { + match facade.build("./example.json".to_string(), ":memory:".to_string()) { Ok(_) => {assert!(true)}, Err(message) => {assert!(false, "{}", message)} } diff --git a/src/query_builders/foreign_key_sql_serializer.rs b/src/query_builders/foreign_key_sql_serializer.rs new file mode 100644 index 0000000..8e9ae29 --- /dev/null +++ b/src/query_builders/foreign_key_sql_serializer.rs @@ -0,0 +1,87 @@ +use crate::schemas::entities::ForeignKey; + +#[derive(Debug, Clone, Copy)] +pub struct ForeignKeySqlSerializer {} + +impl ForeignKeySqlSerializer { + pub fn serialize(self, fk: ForeignKey) -> String { + let mut sql = String::new(); + if !fk.name.is_empty() { + sql.push_str(&format!("CONSTRAINT {} ", fk.name)); + } + sql.push_str(&format!("FOREIGN KEY ({}) REFERENCES {}({})", + fk.table_column_name, + fk.other_table_name, + fk.other_table_column_name + )); + sql + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schemas::builders::{ColumnBuilder, ForeignKeyBuilder, TableBuilder}; + use crate::mappings::SQLiteDatatypes; + + #[test] + fn test_foreign_key_sql_serializer() { + let column = ColumnBuilder::new() + .with_name("id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .build() + .unwrap(); + + let other_column = ColumnBuilder::new() + .with_name("other_id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .build() + .unwrap(); + + let other_table = TableBuilder::new() + .with_name("other_table".to_string()) + .with_column(other_column.clone()) + .build(); + + let fk = ForeignKeyBuilder::new() + .with_name("fk_test".to_string()) + .with_table_column(column) + .with_other_table(other_table, other_column) + .build(); + + let serializer = ForeignKeySqlSerializer {}; + let result = serializer.serialize(fk); + + assert_eq!(result, "CONSTRAINT fk_test FOREIGN KEY (id) REFERENCES other_table(other_id)"); + } + + #[test] + fn test_foreign_key_sql_serializer_no_name() { + let column = ColumnBuilder::new() + .with_name("id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .build() + .unwrap(); + + let other_column = ColumnBuilder::new() + .with_name("other_id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .build() + .unwrap(); + + let other_table = TableBuilder::new() + .with_name("other_table".to_string()) + .with_column(other_column.clone()) + .build(); + + let fk = ForeignKeyBuilder::new() + .with_table_column(column) + .with_other_table(other_table, other_column) + .build(); + + let serializer = ForeignKeySqlSerializer {}; + let result = serializer.serialize(fk); + + assert_eq!(result, "FOREIGN KEY (id) REFERENCES other_table(other_id)"); + } +} diff --git a/src/query_builders/mod.rs b/src/query_builders/mod.rs index 35d5180..38bdc9d 100644 --- a/src/query_builders/mod.rs +++ b/src/query_builders/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod column_sql_serializer; pub(crate) mod table_sql_serializer; +mod foreign_key_sql_serializer; diff --git a/src/query_builders/table_sql_serializer.rs b/src/query_builders/table_sql_serializer.rs index a666f89..6056283 100644 --- a/src/query_builders/table_sql_serializer.rs +++ b/src/query_builders/table_sql_serializer.rs @@ -1,4 +1,5 @@ use crate::query_builders::column_sql_serializer::ColumnSqlSerializer; +use crate::query_builders::foreign_key_sql_serializer::ForeignKeySqlSerializer; use crate::schemas::entities::Table; #[derive(Debug, Clone, Copy)] @@ -7,12 +8,20 @@ pub struct TableSqlSerializer {} impl TableSqlSerializer { pub fn serialize(self, table: Table) -> String { let column_serializer = ColumnSqlSerializer{}; - let columns_sql: Vec = table.columns + let mut elements_sql: Vec = table.columns .into_iter() .map(|c| column_serializer.serialize(c).unwrap()) .collect(); - format!("CREATE TABLE {} ({});", table.name, columns_sql.join(", ")) + let fk_serializer = ForeignKeySqlSerializer{}; + let fks_sql: Vec = table.foreign_keys + .into_iter() + .map(|fk| fk_serializer.serialize(fk)) + .collect(); + + elements_sql.extend(fks_sql); + + format!("CREATE TABLE {} ({});", table.name, elements_sql.join(", ")) } } @@ -42,4 +51,41 @@ mod test { assert_eq!(result, "CREATE TABLE test (id INTEGER NOT NULL AUTOINCREMENT);".to_string()); } + + #[test] + pub fn test_table_sql_serializer_with_fk() { + let column = ColumnBuilder::new() + .with_name("id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .build() + .unwrap(); + + let other_column = ColumnBuilder::new() + .with_name("other_id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .build() + .unwrap(); + + let other_table = TableBuilder::new() + .with_name("other_table".to_string()) + .with_column(other_column.clone()) + .build(); + + let fk = crate::schemas::builders::ForeignKeyBuilder::new() + .with_name("fk_test".to_string()) + .with_table_column(column.clone()) + .with_other_table(other_table, other_column) + .build(); + + let table = TableBuilder::new() + .with_name("test".to_string()) + .with_column(column) + .with_foreign_key(fk) + .build(); + + let serializer = TableSqlSerializer{}; + let result = serializer.serialize(table); + + assert_eq!(result, "CREATE TABLE test (id INTEGER NOT NULL, CONSTRAINT fk_test FOREIGN KEY (id) REFERENCES other_table(other_id));".to_string()); + } } diff --git a/src/schemas/builders.rs b/src/schemas/builders.rs index eb404d7..b31010c 100644 --- a/src/schemas/builders.rs +++ b/src/schemas/builders.rs @@ -1,5 +1,5 @@ use crate::mappings::SQLiteDatatypes; -use crate::schemas::entities::{Column, Table}; +use crate::schemas::entities::{Column, Table, ForeignKey}; pub struct ColumnBuilder { name: String, @@ -93,6 +93,7 @@ impl Default for ColumnBuilder { pub struct TableBuilder { name: String, columns: Vec, + foreign_keys: Vec, strict: bool } @@ -101,6 +102,7 @@ impl TableBuilder { Self { name: String::new(), columns: Vec::new(), + foreign_keys: Vec::new(), strict: false, } } @@ -120,8 +122,18 @@ impl TableBuilder { self } + pub fn with_foreign_key(mut self, foreign_key: ForeignKey) -> Self { + self.foreign_keys.push(foreign_key); + self + } + pub fn build(self) -> Table { - Table { name: self.name, columns: self.columns, strict: self.strict } + Table { + name: self.name, + columns: self.columns, + foreign_keys: self.foreign_keys, + strict: self.strict + } } } @@ -131,10 +143,59 @@ impl Default for TableBuilder { } } +pub struct ForeignKeyBuilder { + name: String, + table_column_name: String, + other_table_column_name: String, + other_table_name: String, +} + +impl ForeignKeyBuilder { + pub fn new() -> Self { + Self { + name: String::new(), + table_column_name: String::new(), + other_table_column_name: String::new(), + other_table_name: String::new(), + } + } + + pub fn with_name(mut self, name: String) -> Self { + self.name = name; + self + } + + pub fn with_table_column(mut self, column: Column) -> Self { + self.table_column_name = column.name; + self + } + + pub fn with_other_table(mut self, table: Table, column: Column) -> Self { + self.other_table_name = table.name; + self.other_table_column_name = column.name; + self + } + + pub fn build(self) -> ForeignKey { + ForeignKey { + name: self.name, + table_column_name: self.table_column_name, + other_table_column_name: self.other_table_column_name, + other_table_name: self.other_table_name, + } + } +} + +impl Default for ForeignKeyBuilder { + fn default() -> Self { + Self::new() + } +} + #[cfg(test)] mod test { use crate::mappings::SQLiteDatatypes; - use crate::schemas::builders::{ColumnBuilder, TableBuilder}; + use crate::schemas::builders::{ColumnBuilder, ForeignKeyBuilder, TableBuilder}; #[test] fn test_column_builder() { @@ -191,4 +252,35 @@ mod test { assert_eq!(table.name, String::from("my_table")); assert_eq!(*table.columns.first().unwrap(), column); } + + #[test] + fn test_foreign_key_builder() { + let column = ColumnBuilder::new() + .with_name("id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .build() + .unwrap(); + + let other_column = ColumnBuilder::new() + .with_name("other_id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .build() + .unwrap(); + + let other_table = TableBuilder::new() + .with_name("other_table".to_string()) + .with_column(other_column.clone()) + .build(); + + let fk = ForeignKeyBuilder::new() + .with_name("fk_test".to_string()) + .with_table_column(column) + .with_other_table(other_table, other_column) + .build(); + + assert_eq!(fk.name, "fk_test"); + assert_eq!(fk.table_column_name, "id"); + assert_eq!(fk.other_table_name, "other_table"); + assert_eq!(fk.other_table_column_name, "other_id"); + } } diff --git a/src/schemas/entities.rs b/src/schemas/entities.rs index b3f22af..17b2be0 100644 --- a/src/schemas/entities.rs +++ b/src/schemas/entities.rs @@ -7,6 +7,8 @@ pub struct Table { #[serde(default)] pub columns: Vec, #[serde(default)] + pub foreign_keys: Vec, + #[serde(default)] pub strict: bool } @@ -27,3 +29,11 @@ pub struct Column { #[serde(default)] pub primary_key: bool } + +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)] +pub struct ForeignKey { + pub name: String, + pub table_column_name: String, + pub other_table_column_name: String, + pub other_table_name: String, +}