feat: add foreign key support
This commit is contained in:
76
example.json
76
example.json
@@ -1,16 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"name": "products",
|
|
||||||
"strict": false,
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"datatype": "Integer",
|
|
||||||
"primary_key": true,
|
|
||||||
"auto_increment": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -22,18 +10,68 @@
|
|||||||
"auto_increment": true
|
"auto_increment": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "username",
|
||||||
"datatype": "Text",
|
"datatype": "Text",
|
||||||
"unique": true
|
"unique": true,
|
||||||
},
|
"nullable": false
|
||||||
{
|
|
||||||
"name": "password",
|
|
||||||
"datatype": "Text"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"datatype": "Text",
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ pub fn connect(db_connection: String) -> Result<Connection> {
|
|||||||
Connection::open(db_connection)
|
Connection::open(db_connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_command(connection: &Connection, sql: String) -> Result<usize> {
|
pub fn run_command(connection: &Connection, sql: String) -> Result<()> {
|
||||||
connection.execute(sql.as_str(), [])
|
connection.execute_batch(sql.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(connection: Connection) {
|
pub fn disconnect(connection: Connection) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ mod tests {
|
|||||||
fn test_db_builder() {
|
fn test_db_builder() {
|
||||||
let facade = DbBuilderFacade{};
|
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)},
|
Ok(_) => {assert!(true)},
|
||||||
Err(message) => {assert!(false, "{}", message)}
|
Err(message) => {assert!(false, "{}", message)}
|
||||||
}
|
}
|
||||||
|
|||||||
87
src/query_builders/foreign_key_sql_serializer.rs
Normal file
87
src/query_builders/foreign_key_sql_serializer.rs
Normal file
@@ -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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub(crate) mod column_sql_serializer;
|
pub(crate) mod column_sql_serializer;
|
||||||
pub(crate) mod table_sql_serializer;
|
pub(crate) mod table_sql_serializer;
|
||||||
|
mod foreign_key_sql_serializer;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::query_builders::column_sql_serializer::ColumnSqlSerializer;
|
use crate::query_builders::column_sql_serializer::ColumnSqlSerializer;
|
||||||
|
use crate::query_builders::foreign_key_sql_serializer::ForeignKeySqlSerializer;
|
||||||
use crate::schemas::entities::Table;
|
use crate::schemas::entities::Table;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -7,12 +8,20 @@ pub struct TableSqlSerializer {}
|
|||||||
impl TableSqlSerializer {
|
impl TableSqlSerializer {
|
||||||
pub fn serialize(self, table: Table) -> String {
|
pub fn serialize(self, table: Table) -> String {
|
||||||
let column_serializer = ColumnSqlSerializer{};
|
let column_serializer = ColumnSqlSerializer{};
|
||||||
let columns_sql: Vec<String> = table.columns
|
let mut elements_sql: Vec<String> = table.columns
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| column_serializer.serialize(c).unwrap())
|
.map(|c| column_serializer.serialize(c).unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
format!("CREATE TABLE {} ({});", table.name, columns_sql.join(", "))
|
let fk_serializer = ForeignKeySqlSerializer{};
|
||||||
|
let fks_sql: Vec<String> = 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());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::mappings::SQLiteDatatypes;
|
use crate::mappings::SQLiteDatatypes;
|
||||||
use crate::schemas::entities::{Column, Table};
|
use crate::schemas::entities::{Column, Table, ForeignKey};
|
||||||
|
|
||||||
pub struct ColumnBuilder {
|
pub struct ColumnBuilder {
|
||||||
name: String,
|
name: String,
|
||||||
@@ -93,6 +93,7 @@ impl Default for ColumnBuilder {
|
|||||||
pub struct TableBuilder {
|
pub struct TableBuilder {
|
||||||
name: String,
|
name: String,
|
||||||
columns: Vec<Column>,
|
columns: Vec<Column>,
|
||||||
|
foreign_keys: Vec<ForeignKey>,
|
||||||
strict: bool
|
strict: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +102,7 @@ impl TableBuilder {
|
|||||||
Self {
|
Self {
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
columns: Vec::new(),
|
columns: Vec::new(),
|
||||||
|
foreign_keys: Vec::new(),
|
||||||
strict: false,
|
strict: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,8 +122,18 @@ impl TableBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_foreign_key(mut self, foreign_key: ForeignKey) -> Self {
|
||||||
|
self.foreign_keys.push(foreign_key);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Table {
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::mappings::SQLiteDatatypes;
|
use crate::mappings::SQLiteDatatypes;
|
||||||
use crate::schemas::builders::{ColumnBuilder, TableBuilder};
|
use crate::schemas::builders::{ColumnBuilder, ForeignKeyBuilder, TableBuilder};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_column_builder() {
|
fn test_column_builder() {
|
||||||
@@ -191,4 +252,35 @@ mod test {
|
|||||||
assert_eq!(table.name, String::from("my_table"));
|
assert_eq!(table.name, String::from("my_table"));
|
||||||
assert_eq!(*table.columns.first().unwrap(), column);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ pub struct Table {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub columns: Vec<Column>,
|
pub columns: Vec<Column>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub foreign_keys: Vec<ForeignKey>,
|
||||||
|
#[serde(default)]
|
||||||
pub strict: bool
|
pub strict: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,3 +29,11 @@ pub struct Column {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub primary_key: bool
|
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,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user