diff --git a/Cargo.lock b/Cargo.lock index 6a9226a..0468b24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,10 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "db" version = "0.1.0" dependencies = [ + "assert-json-diff", "serde", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 517342f..bcdf50f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +assert-json-diff = "2.0.2" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" diff --git a/src/database/mod.rs b/src/database/mod.rs index c31025e..a91ab52 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -8,6 +8,8 @@ pub mod index; pub mod relation; pub mod row; pub mod table; +pub mod select_processor; +pub mod query_builder; #[derive(Serialize, Deserialize)] pub struct Database { @@ -37,14 +39,22 @@ impl Database { } } - pub fn get_table(&mut self, name: String) -> Result<&mut Table, String> { - for table in self.tables.iter_mut() { + pub fn get_table(&self, name: String) -> Result<&Table, String> { + for table in self.tables.iter() { if table.name == name { return Ok(table); } } Err("Failed to find table".to_string()) } + pub fn get_table_mut(&mut self, name: String) -> Result<&mut Table, String> { + for table in self.tables.iter_mut() { + if table.name == name { + return Ok(table); + } + } + Err("Failed to find table".to_string()) + } pub fn get_table_relation( &self, diff --git a/src/database/query_builder.rs b/src/database/query_builder.rs new file mode 100644 index 0000000..8ff754f --- /dev/null +++ b/src/database/query_builder.rs @@ -0,0 +1,3 @@ +// select query builder +// where query builder +// Combine them \ No newline at end of file diff --git a/src/database/relation.rs b/src/database/relation.rs index 75c1ea7..cca7509 100644 --- a/src/database/relation.rs +++ b/src/database/relation.rs @@ -5,7 +5,7 @@ use serde_json::Value; #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Relation { pub table_name: String, - variation: String, + pub variation: String, } impl Relation { pub fn new(table_name: String, variation: String) -> Relation { @@ -15,7 +15,7 @@ impl Relation { } } - pub fn get_foreign_row(&self, database: &mut Database, foreign_id: u64) -> Result { + pub fn get_foreign_row(&self, database: &Database, foreign_id: u64) -> Result { // Find foreign table let table = database.get_table(self.table_name.clone()).unwrap(); // Get row from foreign table @@ -43,10 +43,10 @@ impl OneToOne { self.foreign_id } - pub fn get(&self, table_name: String, database: &mut Database) -> Result { + pub fn get(&self, table_name: String, database: &Database) -> Result { let relation_result: Result<&Relation, String> = { // Get table - let table: &mut Table = match database.get_table(table_name) { + let table: &Table = match database.get_table(table_name) { Ok(relation) => relation, Err(error) => return Err(error), }; @@ -86,9 +86,9 @@ impl OneToMany { self.foreign_ids.clone() } - pub fn get(&self, table_name: String, database: &mut Database) -> Result, String> { + pub fn get(&self, table_name: String, database: &Database) -> Result, String> { let relation_result: Result<&Relation, String> = { - let table: &mut Table = match database.get_table(table_name) { + let table: &Table = match database.get_table(table_name) { Ok(relation) => relation, Err(error) => return Err(error), }; @@ -111,4 +111,4 @@ impl OneToMany { pub fn from_value(value: Value) -> OneToMany { serde_json::from_value(value).unwrap() } -} \ No newline at end of file +} diff --git a/src/database/row.rs b/src/database/row.rs index 61d2306..82ff2f4 100644 --- a/src/database/row.rs +++ b/src/database/row.rs @@ -1,4 +1,4 @@ use serde_json::Value; use std::collections::HashMap; -pub type Row = HashMap; +pub type Row = HashMap; \ No newline at end of file diff --git a/src/database/select_processor.rs b/src/database/select_processor.rs new file mode 100644 index 0000000..d6222ad --- /dev/null +++ b/src/database/select_processor.rs @@ -0,0 +1,79 @@ +use std::{borrow::Borrow, collections::HashMap, str::FromStr}; + +use serde_json::{map::Values, Value, json}; + +use crate::database::relation::{OneToOne, OneToMany}; + +use super::{row::Row, table::Table, Database}; + +pub struct SelectProcessor { + +} +/** +{ + select: [ + "id", + "name", + "relation.*" + ] +} +{ + select: [ + "id", + "name", + "relation.id" + ] +} +// +*/ +impl SelectProcessor { + // recursively select relations + + pub fn selector(database: &Database, table_name: &String, row: &Row, select: Vec, mut output: HashMap) -> HashMap { + + for value in select { + let key = value.to_string(); + if(key.eq("*")) { + return row.clone();// recursive call + } + else if(key.contains(".")) { + let split: Vec = key.split(".").map(|part| String::from_str(part).unwrap()).collect(); + + if(split.len() == 2 && split[1].eq("*")) { + let relation_rows = SelectProcessor::resolve_relation(database, table_name, row, &split[0]); + output.insert(split[0].clone(), json!(relation_rows)); + } + // recursive call + } else { + let value = row.get(&key); + match value.is_some() { + true => output.insert(key, value.unwrap().to_owned()), + false => panic!("Failed to read key: {key} on table: {table_name}"), + + }; + + } + } + return output; + } + + pub fn resolve_relation(database: &Database, table_name: &String, row: &Row, key: &String) -> Vec { + let table = database.get_table(table_name.clone()).unwrap(); + + println!("Attempting to find relation for '{key}' in table: '{table_name}'"); + + + let value = row.get(key).unwrap(); + let relation_name = match value.get("relation_name") { + Some(val) => String::from(val.as_str().unwrap()), + None => panic!("Value was not a valid relation") + }; + let relation = table.get_relation(&relation_name).unwrap(); + let relation_variation = relation.variation.as_str(); + return match relation_variation { + "one_to_one" => vec![OneToOne::from_value(value.to_owned()).get(table_name.clone(), database).unwrap()], + "one_to_many" => OneToMany::from_value(value.to_owned()).get(table_name.clone(), database).unwrap(), + other => panic!("Unsupported relationship type: {other}") + } + } +} \ No newline at end of file diff --git a/src/database/table.rs b/src/database/table.rs index 8234b5f..e873f77 100644 --- a/src/database/table.rs +++ b/src/database/table.rs @@ -115,11 +115,12 @@ impl Table { .clone()); } - pub fn get_relation(&self, name: &String) -> Result<&Relation, String> { - let relation = self.relations.get(name); - if relation.is_some() { + pub fn get_relation(&self, column_name: &String) -> Result<&Relation, String> { + let relation = self.relations.get(column_name); + + if !relation.is_none() { return Ok(relation.unwrap()); } - Err("Failed to find relation".to_string()) + Err(format!("Failed to find relation, '{column_name}'").to_string()) } } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 2aa5457..c64a5bc 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -2,3 +2,4 @@ mod database_test; mod index_test; mod relation_test; mod table_test; +mod select_processor_test; \ No newline at end of file diff --git a/src/tests/relation_test.rs b/src/tests/relation_test.rs index f704168..d8314b8 100644 --- a/src/tests/relation_test.rs +++ b/src/tests/relation_test.rs @@ -11,20 +11,20 @@ mod relation_test { #[test] fn should_succed_one_to_one_relationship_found() { let cat_relations = - hashmap!["cat_food" => Relation::new("Food".to_string(), "one_to_one".to_string())]; + hashmap!["cat_food" => Relation::new("Foods".to_string(), "one_to_one".to_string())]; let cat_table: Table = Table::new("Cats", "id", None, Some(cat_relations)); - let food_table: Table = Table::new("Food", "id", None, None); + let food_table: Table = Table::new("Foods", "id", None, None); let mut database: Database = Database::new(vec![cat_table, food_table]); _ = database - .get_table("Food".to_string()) + .get_table_mut("Foods".to_string()) .unwrap() .insert_row(row!["id" => 123, "name" => "Dry Feed"]); - let _ = database.get_table("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "food" => OneToOne::new(123u64, "cat_food".to_string())]); + let _ = database.get_table_mut("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "food" => OneToOne::new(123u64, "cat_food".to_string())]); - let cat_table = database.get_table("Cats".to_string()).unwrap(); + let cat_table = database.get_table_mut("Cats".to_string()).unwrap(); let cat_1 = cat_table.find_by_pk(1u64).unwrap(); let cat_1_food = cat_1.get("food").unwrap(); @@ -52,17 +52,17 @@ mod relation_test { fn should_fail_no_one_to_one_relation_found() { let cat_table: Table = Table::new("Cats", "id", None, None); - let food_table: Table = Table::new("Food", "id", None, None); + let food_table: Table = Table::new("Foods", "id", None, None); let mut database: Database = Database::new(vec![cat_table, food_table]); _ = database - .get_table("Food".to_string()) + .get_table_mut("Foods".to_string()) .unwrap() .insert_row(row!["id" => 123, "name" => "Dry Feed"]); - let _ = database.get_table("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "food" => OneToOne::new(123u64, "cat_food".to_string())]); + let _ = database.get_table_mut("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "food" => OneToOne::new(123u64, "cat_food".to_string())]); - let cat_table = database.get_table("Cats".to_string()).unwrap(); + let cat_table = database.get_table_mut("Cats".to_string()).unwrap(); let cat_1 = cat_table.find_by_pk(1u64).unwrap(); let cat_1_food = cat_1.get("food").unwrap(); @@ -70,20 +70,20 @@ mod relation_test { let relation = OneToOne::from_value(cat_1_food.to_owned()); assert_eq!(relation.get_id(), 123u64); - let foreign_row = relation.get("Food".to_string(), database.borrow_mut()); + let foreign_row = relation.get("Foods".to_string(), database.borrow_mut()); assert_eq!(foreign_row.is_err(), true); } #[test] fn should_fail_no_table_found() { - let cat_table: Table = Table::new("Cats", "id", None, None); + let cat_table: Table = Table::new("Cats", "id", None, None); let mut database: Database = Database::new(vec![cat_table]); - let _ = database.get_table("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "food" => OneToOne::new(123u64, "cat_food".to_string())]); + let _ = database.get_table_mut("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "food" => OneToOne::new(123u64, "cat_food".to_string())]); - let cat_table = database.get_table("Cats".to_string()).unwrap(); + let cat_table = database.get_table_mut("Cats".to_string()).unwrap(); let cat_1 = cat_table.find_by_pk(1u64).unwrap(); let cat_1_food = cat_1.get("food").unwrap(); @@ -91,7 +91,7 @@ mod relation_test { let relation = OneToOne::from_value(cat_1_food.to_owned()); assert_eq!(relation.get_id(), 123u64); - let foreign_row = relation.get("Food".to_string(), database.borrow_mut()); + let foreign_row = relation.get("Foods".to_string(), database.borrow_mut()); print!( "Printing foreign row error {}\n", &foreign_row.unwrap_err().as_str() @@ -101,13 +101,13 @@ mod relation_test { fn should_fail_no_one_to_one_row_found() { let cat_table: Table = Table::new("Cats", "id", None, None); - let food_table: Table = Table::new("Food", "id", None, None); + let food_table: Table = Table::new("Foodss", "id", None, None); let mut database: Database = Database::new(vec![cat_table, food_table]); - let _ = database.get_table("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "food" => OneToOne::new(123u64, "cat_food".to_string())]); + let _ = database.get_table_mut("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "food" => OneToOne::new(123u64, "cat_food".to_string())]); - let cat_table = database.get_table("Cats".to_string()).unwrap(); + let cat_table = database.get_table_mut("Cats".to_string()).unwrap(); let cat_1 = cat_table.find_by_pk(1u64).unwrap(); let cat_1_food = cat_1.get("food").unwrap(); @@ -126,22 +126,22 @@ mod relation_test { #[test] fn should_succed_one_to_many_relationship_found() { let cat_relations = - hashmap!["cat_food" => Relation::new("Food".to_string(), "one_to_many".to_string())]; + hashmap!["cat_food" => Relation::new("Foods".to_string(), "one_to_many".to_string())]; let cat_table: Table = Table::new("Cats", "id", None, Some(cat_relations)); - let food_table: Table = Table::new("Food", "id", None, None); + let food_table: Table = Table::new("Foods", "id", None, None); let mut database: Database = Database::new(vec![cat_table, food_table]); _ = database - .get_table("Food".to_string()) + .get_table_mut("Foods".to_string()) .unwrap() .insert_row(row!["id" => 123, "name" => "Dry Feed"]); _ = database - .get_table("Food".to_string()) + .get_table_mut("Foods".to_string()) .unwrap() .insert_row(row!["id" => 12, "name" => "Dry Feed"]); - let _ = database.get_table("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "foods" => OneToMany::new(vec![123u64,12u64], "cat_food".to_string())]); + let _ = database.get_table_mut("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "foods" => OneToMany::new(vec![123u64,12u64], "cat_food".to_string())]); let cat_table = database.get_table("Cats".to_string()).unwrap(); let cat_1 = cat_table.find_by_pk(1u64).unwrap(); diff --git a/src/tests/select_processor_test.rs b/src/tests/select_processor_test.rs new file mode 100644 index 0000000..a369349 --- /dev/null +++ b/src/tests/select_processor_test.rs @@ -0,0 +1,91 @@ +//hashmap!(["food" => Relation::new("food","one_to_one")]) + +#[cfg(test)] +mod select_processor_test { + use crate::database::{ + relation::{OneToMany, Relation}, + select_processor::SelectProcessor, + table::Table, + Database, + }; + use assert_json_diff::assert_json_include; + use serde_json::json; + use std::collections::HashMap; + + pub fn initialize() -> Database { + let cat_relations = + hashmap!["cat_food" => Relation::new("Food".to_string(), "one_to_many".to_string())]; + let cat_table: Table = Table::new("Cats", "id", None, Some(cat_relations)); + + let food_table: Table = Table::new("Food", "id", None, None); + + let mut database: Database = Database::new(vec![cat_table, food_table]); + + _ = database + .get_table_mut("Food".to_string()) + .unwrap() + .insert_row(row!["id" => 123, "name" => "Wet Feed"]); + _ = database + .get_table_mut("Food".to_string()) + .unwrap() + .insert_row(row!["id" => 12, "name" => "Dry Feed"]); + _ = database.get_table_mut("Cats".to_string()).unwrap().insert_row(row!["id" => 1, "name" => "Ozzy", "breed" => "mixed", "foods" => OneToMany::new(vec![123u64,12u64], "cat_food".to_string())]); + + return database; + } + #[test] + fn should_succed_select_processor_one_level_resolved() { + let database = initialize(); + + let cat_table = database.get_table("Cats".to_string()).unwrap(); + let cat_1 = cat_table.find_by_pk(1u64).unwrap(); + let select = vec!["id".to_string(), "name".to_string(), "foods.*".to_string()]; + let output = + SelectProcessor::selector(&database, &cat_table.name, cat_1, select, HashMap::new()); + assert_json_include!( + actual: &output, + expected: + &row!["id" => 1, "name" =>"Ozzy", "foods" => + vec![ + row!["id" => 123, "name" => "Wet Feed"], + row!["id" => 12, "name" => "Dry Feed"] + + ]] + ); + } + #[test] + fn should_succed_select_processor_zero_level_resolved() { + let database = initialize(); + + let cat_table = database.get_table("Cats".to_string()).unwrap(); + let cat_1 = cat_table.find_by_pk(1u64).unwrap(); + let select = vec!["id".to_string(), "name".to_string(), "breed".to_string()]; + let output = + SelectProcessor::selector(&database, &cat_table.name, cat_1, select, HashMap::new()); + assert_json_include!( + actual: &output, + expected: &row!["id" => 1, "name" =>"Ozzy", "breed" => "mixed"] + ); + } + #[test] + fn should_succed_select_processor_zero_level_asterix_resolved() { + let database = initialize(); + + let cat_table = database.get_table("Cats".to_string()).unwrap(); + let cat_1 = cat_table.find_by_pk(1u64).unwrap(); + let select = vec!["*".to_string()]; + let output = + SelectProcessor::selector(&database, &cat_table.name, cat_1, select, HashMap::new()); + + assert_json_include!( + actual: &output, + expected: + &json!({"id": 1, "name":"Ozzy", "breed": "mixed", "foods": + { + "relation_name": "cat_food", + "foreign_ids": [123,12] + } + }) + ); + } +}