feat: implements table, columns serialization

This commit is contained in:
Namu
2026-04-01 22:11:20 +02:00
parent a91adfc621
commit ef0767f33a
12 changed files with 634 additions and 0 deletions

107
Cargo.lock generated Normal file
View File

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

8
Cargo.toml Normal file
View File

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

3
src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
mod mappings;
mod schemas;
mod query_builders;

66
src/mappings/mod.rs Normal file
View File

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

View File

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

View File

@@ -0,0 +1,2 @@
mod column_sql_serializer;
mod table_sql_serializer;

View File

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

182
src/schemas/builders.rs Normal file
View File

@@ -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<String>,
auto_increment: bool,
unique: bool,
check_constraint: Option<String>,
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<Column, String> {
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<Column>,
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);
}
}

46
src/schemas/entities.rs Normal file
View File

@@ -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<Column>,
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<String>,
pub auto_increment: bool,
pub unique: bool,
pub check_constraint: Option<String>, // raw check constraint
pub primary_key: bool
}
impl Table {
pub fn new(name: String, columns: Vec<Column>, strict: bool) -> Table {
Table {
name,
columns,
strict
}
}
}
impl Column {
pub fn new(name: String, datatype: SQLiteDatatypes, nullable: bool, default: Option<String>, auto_increment: bool, unique: bool, check_constraint: Option<String>, primary_key: bool) -> Column {
Column {
name,
datatype,
nullable,
default,
auto_increment,
unique,
check_constraint,
primary_key
}
}
}

4
src/schemas/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub(crate) mod reader;
pub(crate) mod entities;
pub(crate) mod builders;
mod writer;

23
src/schemas/reader.rs Normal file
View File

@@ -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<Table> {
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<Table> {
serde_json::from_str(&json_schema).expect("Error parsing JSON schema")
}
}

32
src/schemas/writer.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::fs::File;
use std::io::Write;
use crate::schemas::entities::Table;
pub struct SchemaWriter {
tables: Vec<Table>
}
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(())
}
}