feat: add foreign key support
All checks were successful
Build project / quality-and-build (push) Successful in 10m38s
Build project / SonarQube Trigger (push) Successful in 22s

This commit is contained in:
Namu
2026-06-16 22:47:22 +02:00
parent 06a23fc6d3
commit f314f3daca
8 changed files with 301 additions and 27 deletions

View File

@@ -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"
} }
] ]
} }

View File

@@ -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) {

View File

@@ -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)}
} }

View 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)");
}
}

View File

@@ -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;

View File

@@ -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());
}
} }

View File

@@ -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");
}
} }

View File

@@ -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,
}