From ef0767f33a8b67015ac763a8b7c47987d988ccc7 Mon Sep 17 00:00:00 2001 From: Namu Date: Wed, 1 Apr 2026 22:11:20 +0200 Subject: [PATCH] feat: implements table, columns serialization --- Cargo.lock | 107 ++++++++++++ Cargo.toml | 8 + src/lib.rs | 3 + src/mappings/mod.rs | 66 +++++++ src/query_builders/column_sql_serializer.rs | 109 ++++++++++++ src/query_builders/mod.rs | 2 + src/query_builders/table_sql_serializer.rs | 52 ++++++ src/schemas/builders.rs | 182 ++++++++++++++++++++ src/schemas/entities.rs | 46 +++++ src/schemas/mod.rs | 4 + src/schemas/reader.rs | 23 +++ src/schemas/writer.rs | 32 ++++ 12 files changed, 634 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/mappings/mod.rs create mode 100644 src/query_builders/column_sql_serializer.rs create mode 100644 src/query_builders/mod.rs create mode 100644 src/query_builders/table_sql_serializer.rs create mode 100644 src/schemas/builders.rs create mode 100644 src/schemas/entities.rs create mode 100644 src/schemas/mod.rs create mode 100644 src/schemas/reader.rs create mode 100644 src/schemas/writer.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..64da7f5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "db_builder" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..20582d4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "db_builder" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde_json = "1.0.149" +serde = { version = "1.0.228", features = ["derive"] } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ebd829f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +mod mappings; +mod schemas; +mod query_builders; diff --git a/src/mappings/mod.rs b/src/mappings/mod.rs new file mode 100644 index 0000000..f5957fd --- /dev/null +++ b/src/mappings/mod.rs @@ -0,0 +1,66 @@ +/* +This module is used to make mapping between Rust and SQLite + */ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize, Default)] +pub enum SQLiteDatatypes { + Integer, + Real, + #[default] + Text, + Blob, + Null, + Numeric, + Any, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Default)] +pub enum SupportedTypes { + I32, + I64, + F64, + #[default] + String, + VecU8, + Bool, + Option, +} + +impl From for SQLiteDatatypes { + fn from(t: SupportedTypes) -> Self { + match t { + SupportedTypes::I32 | SupportedTypes::I64 | SupportedTypes::Bool => SQLiteDatatypes::Integer, + SupportedTypes::F64 => SQLiteDatatypes::Real, + SupportedTypes::String => SQLiteDatatypes::Text, + SupportedTypes::VecU8 => SQLiteDatatypes::Blob, + SupportedTypes::Option => SQLiteDatatypes::Any, + } + } +} + +impl From for SupportedTypes { + fn from(t: SQLiteDatatypes) -> Self { + match t { + SQLiteDatatypes::Integer => SupportedTypes::I64, + SQLiteDatatypes::Real | SQLiteDatatypes::Numeric => SupportedTypes::F64, + SQLiteDatatypes::Text => SupportedTypes::String, + SQLiteDatatypes::Blob => SupportedTypes::VecU8, + SQLiteDatatypes::Null | SQLiteDatatypes::Any => SupportedTypes::Option, + } + } +} + +impl From for String { + fn from(t: SQLiteDatatypes) -> Self { + match t { + SQLiteDatatypes::Integer => String::from("INTEGER"), + SQLiteDatatypes::Real => String::from("REAL"), + SQLiteDatatypes::Text => String::from("TEXT"), + SQLiteDatatypes::Blob => String::from("BLOB"), + SQLiteDatatypes::Any => String::from("ANY"), + SQLiteDatatypes::Null => String::from("NULL"), + SQLiteDatatypes::Numeric => String::from("NUMERIC") + } + } +} \ No newline at end of file diff --git a/src/query_builders/column_sql_serializer.rs b/src/query_builders/column_sql_serializer.rs new file mode 100644 index 0000000..65f4fde --- /dev/null +++ b/src/query_builders/column_sql_serializer.rs @@ -0,0 +1,109 @@ +use crate::mappings::SQLiteDatatypes; +use crate::schemas::entities::Column; + +#[derive(Debug, Clone, Copy)] +pub struct ColumnSqlSerializer {} + +impl ColumnSqlSerializer { + pub fn new() -> Self { + ColumnSqlSerializer {} + } + + pub fn serialize(self, column: Column) -> Result { + let mut query = String::from(column.name); + query.push(' '); + + query += &*String::from(column.datatype); + query.push(' '); + + if !column.nullable { + query += "NOT NULL " // Notice the space at the end + } + + if column.unique { + query += "UNIQUE " // Notice the space at the end + } + + if column.auto_increment { + query += "AUTO_INCREMENT " // Notice the space at the end + } + + if let Some(default) = column.default { + match column.datatype { + SQLiteDatatypes::Text | SQLiteDatatypes::Blob => { + query += &format!("DEFAULT '{}' ", default); + }, + SQLiteDatatypes::Numeric | SQLiteDatatypes::Integer | SQLiteDatatypes::Real => { + query += &format!("DEFAULT {} ", default); + }, + SQLiteDatatypes::Any | SQLiteDatatypes::Null => { + return Err("No default value can be set with Any and Null".to_string()) + } + } + } + + if let Some(check_constraint) = column.check_constraint { + query += &format!("CHECK {} ", check_constraint); + } + + if column.primary_key { + query += "PRIMARY KEY "; + } + + query.push(','); + Ok(query) + } +} + +#[cfg(test)] +mod tests { + use super::super::super::schemas::builders::ColumnBuilder; + use crate::mappings::SQLiteDatatypes; + use crate::query_builders::column_sql_serializer::ColumnSqlSerializer; + + #[test] + pub fn test_column_sql_serializer() { + let column = ColumnBuilder::new() + .with_name("id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .with_auto_increment() + .with_primary_key() + .build() + .unwrap(); + + let serializer = ColumnSqlSerializer::new(); + let result = serializer.serialize(column).unwrap(); + + assert_eq!(result, "id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY ,".to_string()); + } + + #[test] + pub fn test_column_sql_serializer_with_text_default() { + let column = ColumnBuilder::new() + .with_name("name".to_string()) + .with_datatype(SQLiteDatatypes::Text) + .with_default("None".to_string()) + .build() + .unwrap(); + + let serializer = ColumnSqlSerializer::new(); + let result = serializer.serialize(column).unwrap(); + + assert_eq!(result, "name TEXT NOT NULL DEFAULT 'None' ,".to_string()); + } + + #[test] + pub fn test_column_sql_serializer_with_integer_default() { + let column = ColumnBuilder::new() + .with_name("age".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .with_default("18".to_string()) + .build() + .unwrap(); + + let serializer = ColumnSqlSerializer::new(); + let result = serializer.serialize(column).unwrap(); + + assert_eq!(result, "age INTEGER NOT NULL DEFAULT 18 ,".to_string()); + } +} diff --git a/src/query_builders/mod.rs b/src/query_builders/mod.rs new file mode 100644 index 0000000..2a91f30 --- /dev/null +++ b/src/query_builders/mod.rs @@ -0,0 +1,2 @@ +mod column_sql_serializer; +mod table_sql_serializer; \ No newline at end of file diff --git a/src/query_builders/table_sql_serializer.rs b/src/query_builders/table_sql_serializer.rs new file mode 100644 index 0000000..f226819 --- /dev/null +++ b/src/query_builders/table_sql_serializer.rs @@ -0,0 +1,52 @@ +use crate::query_builders::column_sql_serializer::ColumnSqlSerializer; +use crate::schemas::entities::Table; + +#[derive(Debug, Clone, Copy)] +pub struct TableSqlSerializer {} + +impl TableSqlSerializer { + pub fn new() -> Self { + TableSqlSerializer{} + } + + pub fn serialize(self, table: Table) -> String { + let mut query = String::from("CREATE TABLE "); + query += &format!("{} (\n", table.name); + + let column_serializer = ColumnSqlSerializer::new(); + for column in table.columns { + query += &column_serializer.serialize(column).unwrap(); + } + + query += ");"; + query + } +} + +#[cfg(test)] +mod test { + use crate::mappings::SQLiteDatatypes; + use crate::query_builders::table_sql_serializer::TableSqlSerializer; + use crate::schemas::builders::{ColumnBuilder, TableBuilder}; + + #[test] + pub fn test_table_sql_serializer() { + let column = ColumnBuilder::new() + .with_name("id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .with_auto_increment() + .build() + .unwrap(); + + let table = TableBuilder::new() + .with_name("test".to_string()) + .with_column(column) + .build(); + + let serializer = TableSqlSerializer::new(); + + let result = serializer.serialize(table); + + assert_eq!(result, "CREATE TABLE test (\nid INTEGER NOT NULL AUTO_INCREMENT ,);".to_string()); + } +} diff --git a/src/schemas/builders.rs b/src/schemas/builders.rs new file mode 100644 index 0000000..afbbbc2 --- /dev/null +++ b/src/schemas/builders.rs @@ -0,0 +1,182 @@ +use crate::mappings::SQLiteDatatypes; +use crate::schemas::entities::{Column, Table}; + +pub struct ColumnBuilder { + name: String, + datatype: SQLiteDatatypes, + nullable: bool, + default: Option, + auto_increment: bool, + unique: bool, + check_constraint: Option, + primary_key: bool, +} + +impl ColumnBuilder { + pub fn new() -> ColumnBuilder { + ColumnBuilder { + name: String::new(), + datatype: SQLiteDatatypes::Text, + nullable: false, + default: None, + auto_increment: false, + unique: false, + check_constraint: None, + primary_key: false, + } + } + + pub fn with_name(mut self, name: String) -> Self { + self.name = name; + self + } + + pub fn with_datatype(mut self, datatype: SQLiteDatatypes) -> Self { + self.datatype = datatype; + self + } + + pub fn with_nullable(mut self) -> Self { + self.nullable = true; + self + } + + pub fn with_default(mut self, default: String) -> Self { + self.default = Some(default); + self + } + + pub fn with_auto_increment(mut self) -> Self { + self.auto_increment = true; + self + } + + pub fn with_unique(mut self) -> Self { + self.unique = true; + self + } + + pub fn with_check_constraint(mut self, constraint: String) -> Self { + self.check_constraint = Some(constraint); + self + } + + pub fn with_primary_key(mut self) -> Self { + self.primary_key = true; + self + } + + pub fn build(self) -> Result { + if self.auto_increment && self.datatype != SQLiteDatatypes::Integer { + return Err("Cannot set AUTO_INCREMENT on non-INTEGER column".to_string()); + } + + Ok(Column::new( + self.name, + self.datatype, + self.nullable, + self.default, + self.auto_increment, + self.unique, + self.check_constraint, + self.primary_key, + )) + } +} + +pub struct TableBuilder { + name: String, + columns: Vec, + strict: bool +} + +impl TableBuilder { + pub fn new() -> TableBuilder { + TableBuilder { + name: String::new(), + columns: Vec::new(), + strict: false, + } + } + + pub fn with_name(mut self, name: String) -> Self { + self.name = name; + self + } + + pub fn with_strict(mut self) -> Self { + self.strict = true; + self + } + + pub fn with_column(mut self, column: Column) -> Self { + self.columns.push(column); + self + } + + pub fn build(self) -> Table { + Table::new(self.name, self.columns, self.strict) + } +} + +#[cfg(test)] +mod test { + use crate::mappings::SQLiteDatatypes; + use crate::schemas::builders::{ColumnBuilder, TableBuilder}; + + #[test] + fn test_column_builder() { + let result = ColumnBuilder::new() + .with_name("name".to_string()) + .with_datatype(SQLiteDatatypes::Text) + .with_nullable() + .build(); + + match result { + Ok(column) => { + assert_eq!(column.name, String::from("name")); + assert_eq!(column.datatype, SQLiteDatatypes::Text); + assert_eq!(column.nullable, true); + }, + Err(_) => { + assert!(false, "should not fail"); + } + } + } + + #[test] + fn test_column_builder_should_fail() { + let result = ColumnBuilder::new() + .with_name("name".to_string()) + .with_datatype(SQLiteDatatypes::Blob) + .with_auto_increment() + .build(); + + match result { + Ok(_) => { + assert!(false, "Should not make auto increment with another type than integer") + }, + Err(_) => { + assert!(true); + } + } + } + + #[test] + fn test_table_builder() { + let column = ColumnBuilder::new() + .with_name("id".to_string()) + .with_datatype(SQLiteDatatypes::Integer) + .with_auto_increment() + .build() + .expect("Failed to create id column"); + + let table = TableBuilder::new() + .with_name("my_table".to_string()) + .with_column(column.clone()) + .build(); + + assert_eq!(table.name, String::from("my_table")); + assert_eq!(*table.columns.first().unwrap(), column); + } +} diff --git a/src/schemas/entities.rs b/src/schemas/entities.rs new file mode 100644 index 0000000..2628bc4 --- /dev/null +++ b/src/schemas/entities.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; +use crate::mappings::SQLiteDatatypes; + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct Table { + pub name: String, + pub columns: Vec, + pub strict: bool +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)] +pub struct Column { + pub name: String, + pub datatype: SQLiteDatatypes, + pub nullable: bool, + pub default: Option, + pub auto_increment: bool, + pub unique: bool, + pub check_constraint: Option, // raw check constraint + pub primary_key: bool +} + +impl Table { + pub fn new(name: String, columns: Vec, strict: bool) -> Table { + Table { + name, + columns, + strict + } + } +} + +impl Column { + pub fn new(name: String, datatype: SQLiteDatatypes, nullable: bool, default: Option, auto_increment: bool, unique: bool, check_constraint: Option, primary_key: bool) -> Column { + Column { + name, + datatype, + nullable, + default, + auto_increment, + unique, + check_constraint, + primary_key + } + } +} diff --git a/src/schemas/mod.rs b/src/schemas/mod.rs new file mode 100644 index 0000000..5e5b323 --- /dev/null +++ b/src/schemas/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod reader; +pub(crate) mod entities; +pub(crate) mod builders; +mod writer; diff --git a/src/schemas/reader.rs b/src/schemas/reader.rs new file mode 100644 index 0000000..b819d24 --- /dev/null +++ b/src/schemas/reader.rs @@ -0,0 +1,23 @@ +use std::fs; +use crate::schemas::entities::Table; + +pub struct SchemaParsingFacade {} + +impl SchemaParsingFacade { + pub fn new() -> SchemaParsingFacade { + SchemaParsingFacade{} + } + + pub fn parse(&self, path: String) -> Vec { + let json_schema = self.read_schema_file(path); + self.parse_schema_file(json_schema) + } + + fn read_schema_file(&self, path: String) -> String { + fs::read_to_string(path).expect("Error reading schema files") + } + + fn parse_schema_file(&self, json_schema: String) -> Vec
{ + serde_json::from_str(&json_schema).expect("Error parsing JSON schema") + } +} diff --git a/src/schemas/writer.rs b/src/schemas/writer.rs new file mode 100644 index 0000000..0a7d3de --- /dev/null +++ b/src/schemas/writer.rs @@ -0,0 +1,32 @@ +use std::fs::File; +use std::io::Write; +use crate::schemas::entities::Table; + +pub struct SchemaWriter { + tables: Vec
+} + +impl SchemaWriter { + pub fn new() -> Self { + SchemaWriter{ + tables: Vec::new() + } + } + + pub fn add_table(mut self, table: Table) -> Self { + self.tables.push(table); + self + } + + // create_schemas create the schema as a JSON file. the name doesn't need to have the ".json" + pub fn create_schemas(self, schema_name: String) -> Result<(), String> { + let schema_file_path = format!("{}.json", schema_name); + + let content_result = serde_json::to_string(self.tables.as_slice()).expect("Error serializing tables"); + + let mut schema_file = File::create(schema_file_path).expect("Failed to create schema file"); + schema_file.write_all(content_result.as_bytes()).expect("Error writing in schema file"); + + Ok(()) + } +}