From d7e617bdef74dd6e2b63a150c219352d342cd4f3 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Sun, 22 Sep 2024 16:00:41 +0100 Subject: [PATCH] feat: Implement AnyTable trait, add condition and entity support (#8) * feat: Implement AnyTable trait, add condition and entity support Add support for the AnyTable trait for Table with Entity. Implement methods for AnyTable trait: as_any, get_field, add_condition. Define EmptyEntity struct and implement Entity trait. Update Table struct to support the new entity type and add conditions. Update TableDelegate trait to accept Entity type. * doc: Updated README file. --- Cargo.lock | 9 +- TODO.md | 25 ++-- bakery_model/Cargo.toml | 1 + bakery_model/examples/main.rs | 43 +++---- bakery_model/schema-pg.sql | 12 +- bakery_model/src/lib.rs | 6 +- bakery_model/src/product.rs | 113 ++++++++++-------- docs/src/introduction.md | 62 ++++++++-- dorm/Cargo.toml | 3 - dorm/examples/basic-dorm/src/model/client.rs | 8 -- dorm/examples/basic-dorm/src/model/mod.rs | 4 - .../basic-dorm/src/model/order_line.rs | 0 dorm/examples/basic-dorm/src/model/product.rs | 84 ------------- dorm/examples/very-basic-example/src/main.rs | 2 +- dorm/src/join.rs | 20 ++-- dorm/src/lazy_expression.rs | 8 +- dorm/src/prelude.rs | 2 + dorm/src/reference.rs | 37 ------ dorm/src/reference/mod.rs | 4 + .../order.rs => src/reference/related.rs} | 0 dorm/src/reference/unrelated.rs | 80 +++++++++++++ dorm/src/table/mod.rs | 76 +++++++++--- dorm/src/table/with_fields.rs | 3 +- dorm/src/table/with_joins.rs | 13 +- dorm/src/table/with_queries.rs | 3 +- dorm/src/table/with_refs.rs | 33 +++-- dorm/src/traits/any.rs | 43 +++++++ dorm/src/traits/datasource.rs | 2 +- dorm/src/traits/entity.rs | 8 ++ dorm/src/traits/mod.rs | 2 + 30 files changed, 417 insertions(+), 289 deletions(-) delete mode 100644 dorm/examples/basic-dorm/src/model/client.rs delete mode 100644 dorm/examples/basic-dorm/src/model/mod.rs delete mode 100644 dorm/examples/basic-dorm/src/model/order_line.rs delete mode 100644 dorm/examples/basic-dorm/src/model/product.rs delete mode 100644 dorm/src/reference.rs create mode 100644 dorm/src/reference/mod.rs rename dorm/{examples/basic-dorm/src/model/order.rs => src/reference/related.rs} (100%) create mode 100644 dorm/src/reference/unrelated.rs create mode 100644 dorm/src/traits/any.rs create mode 100644 dorm/src/traits/entity.rs diff --git a/Cargo.lock b/Cargo.lock index c17c513..49bab75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,7 @@ dependencies = [ "anyhow", "dorm", "pretty_assertions", + "serde", "serde_json", "testcontainers-modules", "tokio", @@ -1902,18 +1903,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", diff --git a/TODO.md b/TODO.md index 0eed672..5413ff5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ MVP: -0.1.0: Query Building +0.0.1: Query Building - [x] create a basic query type - [x] have query ability to render into a SQL query @@ -15,7 +15,7 @@ MVP: - [x] implement operations: (field.eq(otherfield)) - [x] implement parametric queries - 0.2.0: Nested Query Building + 0.0.2: Nested Query Building - [x] properly handle nested queries - [x] table should own DataSource, which should be cloneable and use Arc for client @@ -26,7 +26,7 @@ MVP: - [x] implemented TableDelegate trait - [x] implemented Query::add_join() - 0.3.0: Table Structure + 0.0.3: Table Structure - [x] add uniq id vendor - [x] implemented Table::join_table() for merging tables @@ -36,9 +36,18 @@ MVP: - [x] When joining table, combine their UniqueIdVendors into one - [x] Implement has_one and has_many in a lazy way - [x] Implement expressions in a lazy way - - 0.4.0: Features for multiple table queries - +- [x] Implemented bakery example + + 0.0.4: Improve Entity tracking and add target documentation + +- [x] Add documentation for target vision of the library +- [x] Add "Entity" concept into Table +- [x] Add example on how to use traits for augmenting Table of specific Entity +- [ ] Check on "Join", they should allow for Entity mutation (joined table associated with a different entity) +- [ ] Implement has_one and has_many in a correct way, moving functionality to Related Reference +- [ ] Implement Unrelated Reference (when ref leads to a table with different Data Source) +- [ ] Implement a better data fetching mechanism, using default entity +- [ ] Restore functionality of bakery example - [ ] Implement ability to include sub-queries based on related tables Create integration test-suite for SQL testing @@ -47,8 +56,8 @@ Create integration test-suite for SQL testing - [x] Implement testcontainers postgres connectivity - [ ] Get rid of testcontainers (they don't work anyway), use regular Postgres - [ ] Create separate test-suite, connect DB etc -- [ ] Populate Bakery tables for tests -- [ ] Seed some data into Bakery tests +- [x] Populate Bakery tables for tests +- [x] Seed some data into Bakery tests - [ ] Make use of Postgres snapshots in the tests Control field queries diff --git a/bakery_model/Cargo.toml b/bakery_model/Cargo.toml index beac960..c1cb5e4 100644 --- a/bakery_model/Cargo.toml +++ b/bakery_model/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" anyhow = "1.0.86" dorm = { path = "../dorm" } pretty_assertions = "1.4.0" +serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.120" testcontainers-modules = { version = "0.8.0", features = [ "postgres", diff --git a/bakery_model/examples/main.rs b/bakery_model/examples/main.rs index f32c65f..f757e8c 100644 --- a/bakery_model/examples/main.rs +++ b/bakery_model/examples/main.rs @@ -1,3 +1,4 @@ +use bakery_model::product::Product; use dorm::prelude::*; use serde_json::Value; @@ -15,37 +16,38 @@ async fn main() { .unwrap(); client.batch_execute(&schema).await.unwrap(); - // Ok, now lets work with the models directly - let bakery_set = bakery_model::bakery::BakerySet::new(); - let query = bakery_set.get_select_query(); - let result = dorm_client.query_raw(&query).await.unwrap(); + // // Ok, now lets work with the models directly + // let bakery_set = bakery_model::bakery::BakerySet::new(); + // let query = bakery_set.get_select_query(); + // let result = dorm_client.query_raw(&query).await.unwrap(); - let Some(Value::String(bakery)) = result[0].get("name") else { - panic!("No bakery found"); - }; - println!("-----------------------------"); - println!("Working for the bakery: {}", bakery); - println!(""); + // let Some(Value::String(bakery)) = result[0].get("name") else { + // panic!("No bakery found"); + // }; + // println!("-----------------------------"); + // println!("Working for the bakery: {}", bakery); + // println!(""); - // Now, lets see how many clients bakery has - let client_set = bakery_set.get_ref("clients").unwrap(); - let client_count = client_set.count(); + // // Now, lets see how many clients bakery has + // let client_set = bakery_set.get_ref("clients").unwrap(); + // let client_count = client_set.count(); - println!( - "There are {} clients in the bakery.", - client_count.get_one().await.unwrap() - ); + // println!( + // "There are {} clients in the bakery.", + // client_count.get_one().await.unwrap() + // ); - // Finally lets see how many products we have in the bakery + // // Finally lets see how many products we have in the bakery - let product_set = bakery_set.get_ref("products").unwrap(); - let product_count = product_set.count(); + // let product_set = bakery_set.get_ref("products").unwrap(); + let product_count = Product::table().count(); println!( "There are {} products in the bakery.", product_count.get_one().await.unwrap() ); + /* // How many products are there with the name // Now for every product, lets calculate how many orders it has @@ -91,4 +93,5 @@ async fn main() { for row in res.into_iter() { println!(" name: {} orders: {}", row["name"], row["orders_count"]); } + */ } diff --git a/bakery_model/schema-pg.sql b/bakery_model/schema-pg.sql index 3ca52c5..1c45541 100644 --- a/bakery_model/schema-pg.sql +++ b/bakery_model/schema-pg.sql @@ -57,12 +57,12 @@ INSERT INTO client (name, contact_details, bakery_id) VALUES ('Doc Brown', '555-1885', 1), ('Biff Tannen', '555-1955', 1); -INSERT INTO product (name, bakery_id) VALUES -('Flux Capacitor Cupcake', 1), -('DeLorean Doughnut', 1), -('Time Traveler Tart', 1), -('Enchantment Under the Sea Pie', 1), -('Hoverboard Cookies', 1); +INSERT INTO product (name, calories, bakery_id) VALUES +('Flux Capacitor Cupcake', 300, 1), +('DeLorean Doughnut', 250, 1), +('Time Traveler Tart', 200, 1), +('Enchantment Under the Sea Pie', 350, 1), +('Hoverboard Cookies', 150, 1); INSERT INTO inventory (product_id, stock) VALUES (1, 50), diff --git a/bakery_model/src/lib.rs b/bakery_model/src/lib.rs index 44d418b..f2ba1e0 100644 --- a/bakery_model/src/lib.rs +++ b/bakery_model/src/lib.rs @@ -10,15 +10,15 @@ use tokio_postgres::NoTls; use dorm::prelude::Postgres; -pub mod bakery; +// pub mod bakery; // pub mod cake; // pub mod cakes_bakers; -pub mod client; +// pub mod client; // pub mod customer; pub mod product; // pub mod lineitem; -pub mod order; +// pub mod order; // pub use bakery::BakerySet; // pub use product::ProductSet; diff --git a/bakery_model/src/product.rs b/bakery_model/src/product.rs index 5f3ec83..c4a9a57 100644 --- a/bakery_model/src/product.rs +++ b/bakery_model/src/product.rs @@ -1,76 +1,83 @@ -use std::{ - ops::Deref, - sync::{Arc, OnceLock}, -}; +use std::sync::{Arc, OnceLock}; -use crate::bakery::BakerySet; +// use crate::bakery::BakerySet; use dorm::prelude::*; +use serde::{Deserialize, Serialize}; use crate::postgres; -#[derive(Debug)] -pub struct Products { - table: Table, +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +pub struct Product { + id: i64, + name: String, } -impl Products { - pub fn new() -> Products { - Products { - table: Products::static_table().clone(), - } - } - pub fn from_table(table: Table) -> Self { - Self { table } - } - pub fn static_table() -> &'static Table { - static TABLE: OnceLock> = OnceLock::new(); +impl Entity for Product {} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +pub struct ProductInventory { + id: i64, + product_id: i64, + stock: i64, +} +impl Entity for ProductInventory {} + +impl Product { + pub fn static_table() -> &'static Table { + static TABLE: OnceLock> = OnceLock::new(); TABLE.get_or_init(|| { - Table::new("product", postgres()) + Table::new_with_entity("product", postgres()) .with_id_field("id") .with_field("name") .with_field("bakery_id") - .has_one("bakery", "bakery_id", || BakerySet::new()) + // .has_one("bakery", "bakery_id", || BakerySet::new()) }) } - pub fn table(&self) -> Table { - self.table.clone() - } - pub fn mod_table(self, func: impl FnOnce(Table) -> Table) -> Self { - let table = self.table.clone(); - let table = func(table); - Self { table } - } - pub fn with_inventory(self) -> Self { - self.mod_table(|t| { - t.with_join( - Table::new("inventory", postgres()) - .with_alias("i") - .with_id_field("product_id") - .with_field("stock"), - "id", - ) - }) + pub fn table() -> Table { + Product::static_table().clone() } +} + +trait ProductTable: AnyTable { + fn with_inventory(self) -> Table; - pub fn id() -> Arc { - Products::static_table().get_field("id").unwrap() + fn id(&self) -> &Arc { + self.get_field("id").unwrap() } - pub fn name() -> Arc { - Products::static_table().get_field("name").unwrap() + fn name(&self) -> &Arc { + self.get_field("name").unwrap() } - pub fn bakery_id() -> Arc { - Products::static_table().get_field("bakery_id").unwrap() + fn bakery_id(&self) -> &Arc { + self.get_field("bakery_id").unwrap() } - pub fn stock(&self) -> Arc { - self.get_join("i").unwrap().get_field("stock").unwrap() - } + // pub fn stock(&self) -> Arc { + // self.get_join("i").unwrap().get_field("stock").unwrap() + // } } -impl Deref for Products { - type Target = Table; - - fn deref(&self) -> &Self::Target { - &self.table +impl ProductTable for Table { + fn with_inventory(self) -> Table { + self.with_join( + Table::new_with_entity("inventory", postgres()) + .with_alias("i") + .with_id_field("product_id") + .with_field("stock"), + "id", + ) } } + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[tokio::test] +// async fn test_product() { +// let table = Product::table(); +// let _field = table.name(); + +// let table = table.with_inventory(); +// // let _field = table.stock(); +// } +// } diff --git a/docs/src/introduction.md b/docs/src/introduction.md index 16c014c..7236425 100644 --- a/docs/src/introduction.md +++ b/docs/src/introduction.md @@ -5,28 +5,72 @@ DORM is a busines entity abstraction framework for Rust. In Enterprise environment, software applications must be easy to maintain and change. Typical Rust applications require significant effort to maintain and change the logic, which makes Rust difficult to compete with languages such as Java, C# and Typescript. +Additionally, existing ORM libraries are rigid and do not allow you to decouple your +business logic from your database implementation detail. -DORM will allow you to separate your entity abstraction from your business logic, making -it easier to maintain and change your code. DORM also will make your code much more -readable and easy to understand. +DORM offers opinionated abstraction over SQL for a separation between your +physical database and business logic. Such decoupling allows you to change +either without affecting the other. -In DORM, you will be able to define your business entities, then work with set of records +DORM also introduces great syntax sugar making your Rust code readable and +easy to understand. To achieve this, DORM comes with the following features: + +1. DataSet abstraction - like a Map, but Rows are stored remotely and only fetched when needed. +2. Expressions - use a power of SQL without writing SQL. +3. Query - a structured query-language aware object for any SQL statement. +4. DataSources - a way to abstract away the database implementation. +5. Table - your in-rust version of SQL table or a view +6. Field - representing columns or arbitrary expressions in a data set. +7. Entity modelin - a pattern for you to create your onw business entities. +8. CRUD operations - serde-compatible insert, update and delete operations. +9. Joins - combining tables into a single table. +10. Reference traversal - convert a set of records into a set of related records. +11. Subqueries - augment a table with expressions based on related tables. + +This book will guide you through the basics of DORM features and how to use them. + +## Example + +In this book, we will be using a fictional database for your typical Bakery business. +Primarily we will be using `product`, `inventory`, `order` and `client` tables. + +With DORM, creating a function to notify clients about low stock, would look like this: ```rust +fn notify_clients_of_low_stock() -> Result<()> { + // Start with all our products and inventory + let products = Products::table_with_inventory() + .add_condition(Products::stock().eq(0)); + + // Find clients who have product orders with low stock + let clients = products + .ref_order() + .ref_client(); + + // Drop clients into notification queue + for client_comm in cilents.get_email_comm().await? { + client_comm.type = ClientCommType::LowStock; + + client_comm.save_into(ClientComm::queue()).await?; + } +} +``` + // Create a set of products with low inventory let out_of_stock_products = Products::new(postgres.clone()) - .with_inventory() - .with_condition(Products::stock().lt(5)); +.with_inventory() +.with_condition(Products::stock().lt(5)); // Traverse into suppliers of a products with low inventory let suppliers = out_of_stock_products.ref_supplier(); // Execute query and iterate over the result for supplier in suppliers.get().await.unwrap() { - order_more_stock(supplier.id()); +order_more_stock(supplier.id()); } -``` + +```` The example above demonstrates multiple DORM features together, specifically: @@ -75,7 +119,7 @@ use dorm::prelude::*; let expr = expr!("concat({}, {})", "hello", "world"); println!("{}", expr.preview()); -``` +```` When expression is created, the template is stored separately from the arguments. This allows you to use arbitrary types as arguments and also use nested queries too: diff --git a/dorm/Cargo.toml b/dorm/Cargo.toml index 80902a1..1bd29d7 100644 --- a/dorm/Cargo.toml +++ b/dorm/Cargo.toml @@ -34,9 +34,6 @@ pretty_assertions = "1.4.0" [dev-dependencies] bakery_model = { path = "../bakery_model" } -[[example]] -name = "basic-dorm" -path = "examples/basic-dorm/src/main.rs" [[example]] name = "query-builder" diff --git a/dorm/examples/basic-dorm/src/model/client.rs b/dorm/examples/basic-dorm/src/model/client.rs deleted file mode 100644 index 9349732..0000000 --- a/dorm/examples/basic-dorm/src/model/client.rs +++ /dev/null @@ -1,8 +0,0 @@ -struct Client { - id: i32, - name: String, - email: String, - phone: String, - is_vip: bool, - address: String, -} diff --git a/dorm/examples/basic-dorm/src/model/mod.rs b/dorm/examples/basic-dorm/src/model/mod.rs deleted file mode 100644 index 49bff89..0000000 --- a/dorm/examples/basic-dorm/src/model/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod client; -pub mod order; -pub mod order_line; -pub mod product; diff --git a/dorm/examples/basic-dorm/src/model/order_line.rs b/dorm/examples/basic-dorm/src/model/order_line.rs deleted file mode 100644 index e69de29..0000000 diff --git a/dorm/examples/basic-dorm/src/model/product.rs b/dorm/examples/basic-dorm/src/model/product.rs deleted file mode 100644 index 574adac..0000000 --- a/dorm/examples/basic-dorm/src/model/product.rs +++ /dev/null @@ -1,84 +0,0 @@ -// CREATE TABLE product ( -// id SERIAL PRIMARY KEY, -// name VARCHAR(255) NOT NULL, -// description TEXT, -// price DECIMAL(10, 2) NOT NULL -// ); - -use std::sync::Arc; - -use rust_decimal::Decimal; - -use anyhow::Result; -use dorm::prelude::*; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_json::Value; - -#[derive(Serialize, Deserialize)] -pub struct Product { - id: i32, - name: String, - description: Option, - price: Decimal, -} - -pub struct ProductSet { - table: Table, -} - -impl TableDelegate for ProductSet { - fn table(&self) -> &Table { - &self.table - } -} - -impl ProductSet { - pub fn new(ds: Postgres) -> Self { - let table = Table::new("product", ds) - .with_field("name") - .with_field("description") - .with_field("default_price"); - // .add_field(Field::new("id", Type::Serial).primary()) - // .add_field(Field::new("name", Type::Varchar(255)).not_null()) - // .add_field(Field::new("description", Type::Text)) - // .add_field(Field::new("price", Type::Decimal(10, 2)).not_null()); - - Self { table } - } - - pub fn name(&self) -> Arc { - self.table.get_field("name").unwrap() - } - - pub fn description(&self) -> Arc { - self.table.get_field("description").unwrap() - } - - pub fn price(&self) -> Arc { - self.table.get_field("default_price").unwrap() - } - - async fn map(self, mut callback: F) -> Result - where - F: FnMut(T) -> T, - T: Serialize + DeserializeOwned, - { - let data = self.table.get_all_data().await?; - let new_data = data.into_iter().map(|row| { - let rec: T = serde_json::from_value(Value::Object(row)).unwrap(); - let modified = callback(rec); - serde_json::to_value(modified) - .unwrap() - .as_object() - .unwrap() - .clone() - }); - - // for row in new_data.into_iter() { - // let insert_query = self.table.update_query(row); - // self.ds.query_execute(&insert_query, row); - // } - - Ok(self) - } -} diff --git a/dorm/examples/very-basic-example/src/main.rs b/dorm/examples/very-basic-example/src/main.rs index 3f38435..f458d3d 100644 --- a/dorm/examples/very-basic-example/src/main.rs +++ b/dorm/examples/very-basic-example/src/main.rs @@ -20,7 +20,7 @@ struct Product { } struct ProductSet { - table: Table, + table: Table, } impl ProductSet { diff --git a/dorm/src/join.rs b/dorm/src/join.rs index 01a0d15..d971dcf 100644 --- a/dorm/src/join.rs +++ b/dorm/src/join.rs @@ -2,7 +2,7 @@ use std::ops::{Deref, DerefMut}; use crate::{ prelude::{JoinQuery, Table}, - traits::datasource::DataSource, + traits::{datasource::DataSource, entity::Entity}, }; #[derive(Clone, Debug)] @@ -14,13 +14,13 @@ enum JoinType { } #[derive(Clone, Debug)] -pub struct Join { - table: Table, +pub struct Join { + table: Table, join_query: JoinQuery, } -impl Join { - pub fn new(table: Table, join_query: JoinQuery) -> Join { +impl Join { + pub fn new(table: Table, join_query: JoinQuery) -> Join { Join { table, join_query } } pub fn alias(&self) -> &str { @@ -29,23 +29,23 @@ impl Join { pub fn join_query(&self) -> &JoinQuery { &self.join_query } - pub fn table(&self) -> &Table { + pub fn table(&self) -> &Table { &self.table } - pub fn table_mut(&mut self) -> &mut Table { + pub fn table_mut(&mut self) -> &mut Table { &mut self.table } } -impl Deref for Join { - type Target = Table; +impl Deref for Join { + type Target = Table; fn deref(&self) -> &Self::Target { &self.table } } -impl DerefMut for Join { +impl DerefMut for Join { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.table } diff --git a/dorm/src/lazy_expression.rs b/dorm/src/lazy_expression.rs index d59610e..f20cb78 100644 --- a/dorm/src/lazy_expression.rs +++ b/dorm/src/lazy_expression.rs @@ -5,16 +5,16 @@ use serde_json::Value; use crate::{ prelude::{Expression, Table}, - traits::datasource::DataSource, + traits::{datasource::DataSource, entity::Entity}, }; #[derive(Clone)] -pub enum LazyExpression { +pub enum LazyExpression { AfterQuery(Arc Value + Send + Sync + 'static>>), - BeforeQuery(Arc) -> Expression + Send + Sync + 'static>>), + BeforeQuery(Arc) -> Expression + Send + Sync + 'static>>), } -impl fmt::Debug for LazyExpression { +impl fmt::Debug for LazyExpression { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { LazyExpression::AfterQuery(_) => f.write_str("AfterQuery()"), diff --git a/dorm/src/prelude.rs b/dorm/src/prelude.rs index cb49f80..ff1f85f 100644 --- a/dorm/src/prelude.rs +++ b/dorm/src/prelude.rs @@ -6,5 +6,7 @@ pub use crate::expression::{Expression, ExpressionArc}; pub use crate::field::Field; pub use crate::operations::Operations; pub use crate::table::TableDelegate; +pub use crate::traits::any::AnyTable; +pub use crate::traits::entity::{EmptyEntity, Entity}; pub use crate::traits::sql_chunk::SqlChunk; pub use crate::{query::JoinQuery, query::Query, table::Table}; diff --git a/dorm/src/reference.rs b/dorm/src/reference.rs deleted file mode 100644 index 7be4c6a..0000000 --- a/dorm/src/reference.rs +++ /dev/null @@ -1,37 +0,0 @@ -use core::fmt; -use std::sync::Arc; - -use crate::{prelude::Table, traits::datasource::DataSource}; - -#[derive(Clone)] -pub struct Reference { - // table: Option>, - get_table: Arc) -> Table + 'static + Sync + Send>>, -} - -impl Reference { - pub fn new(get_table: F) -> Reference - where - F: Fn(&Table) -> Table + 'static + Sync + Send, - { - Reference { - // table: None, - get_table: Arc::new(Box::new(get_table)), - } - } - pub fn table(&self, table: &Table) -> Table { - (self.get_table)(table) - // if self.table.is_none() { - // self.table = Some((self.get_table)(table)); - // } - // self.table.as_ref().unwrap() - } -} - -impl fmt::Debug for Reference { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Reference") - // Cannot print `get_table` since it's a function pointer - .finish() - } -} diff --git a/dorm/src/reference/mod.rs b/dorm/src/reference/mod.rs new file mode 100644 index 0000000..c052a7c --- /dev/null +++ b/dorm/src/reference/mod.rs @@ -0,0 +1,4 @@ +pub mod related; +pub mod unrelated; + +use std::sync::Arc; diff --git a/dorm/examples/basic-dorm/src/model/order.rs b/dorm/src/reference/related.rs similarity index 100% rename from dorm/examples/basic-dorm/src/model/order.rs rename to dorm/src/reference/related.rs diff --git a/dorm/src/reference/unrelated.rs b/dorm/src/reference/unrelated.rs new file mode 100644 index 0000000..d586121 --- /dev/null +++ b/dorm/src/reference/unrelated.rs @@ -0,0 +1,80 @@ +use core::fmt; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use serde_json::json; + +use crate::prelude::Table; +use crate::traits::any::AnyTable; +use crate::traits::datasource::DataSource; +use crate::traits::entity::Entity; + +type RelatedTableFx = dyn Fn(&Table) -> Pin> + Send + Sync>> + + Send + + Sync; + +#[derive(Clone)] +pub struct UnrelatedReference { + get_table: Arc>>, +} + +impl UnrelatedReference { + pub fn new(get_table: F) -> UnrelatedReference + where + F: 'static + + Fn(&Table) -> Pin> + Send + Sync>> + + Send + + Sync, + { + UnrelatedReference { + get_table: Arc::new(Box::new(move |table: &Table| { + Box::pin(get_table(table)) + })), + } + } + + pub async fn as_table(&self, table: &Table) -> Box { + (self.get_table)(table).await + } +} + +impl fmt::Debug for UnrelatedReference { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("UnrelatedReference").finish_non_exhaustive() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mocks::datasource::MockDataSource; + use crate::prelude::{Operations, SqlChunk}; + use crate::traits::entity::EmptyEntity; + + // #[tokio::test] + // async fn test_unrelated_reference() { + // let data = json!([]); + // let data_source = MockDataSource::new(&data); + + // let reference = UnrelatedReference::new(|t: &Table| async { + // let mut t = Table::new("cached_orders", data_source.clone()) + // .with_field("cached_order_id") + // .with_field("cached_data"); + + // let user_id = t.field_query("user_id").get_one().await.unwrap(); + + // t.add_condition(t.get_field("cached_order_id").unwrap().eq(&user_id)); + + // Box::new(t) as Box + // }); + + // let table = reference + // .as_table(&Table::new("users", data_source.clone())) + // .await; + + // let result = table.get_all_data().await; + + // assert_eq!(result.unwrap(), *data_source.data()); + // } +} diff --git a/dorm/src/table/mod.rs b/dorm/src/table/mod.rs index 539919c..ae5af97 100644 --- a/dorm/src/table/mod.rs +++ b/dorm/src/table/mod.rs @@ -1,3 +1,4 @@ +use std::any::Any; use std::ops::Deref; use std::sync::{Arc, Mutex}; @@ -9,9 +10,10 @@ use crate::join::Join; use crate::lazy_expression::LazyExpression; use crate::prelude::{AssociatedQuery, Expression}; use crate::query::Query; -use crate::reference::Reference; +use crate::traits::any::{AnyTable, RelatedTable}; use crate::traits::dataset::{ReadableDataSet, WritableDataSet}; use crate::traits::datasource::DataSource; +use crate::traits::entity::{EmptyEntity, Entity}; use crate::uniqid::UniqueIdVendor; use anyhow::Result; use indexmap::IndexMap; @@ -21,8 +23,9 @@ use serde_json::{Map, Value}; // instead we want to use 3rd party SQL builders, that cary table schema information. #[derive(Debug)] -pub struct Table { +pub struct Table { data_source: T, + _phantom: std::marker::PhantomData, table_name: String, table_alias: Option, @@ -31,22 +34,22 @@ pub struct Table { conditions: Vec, fields: IndexMap>, - joins: IndexMap>>, - lazy_expressions: IndexMap>, - refs: IndexMap>, - + joins: IndexMap>>, + lazy_expressions: IndexMap>, + // refs: IndexMap>, table_aliases: Arc>, } mod with_fields; mod with_joins; mod with_queries; -mod with_refs; +// mod with_refs; -impl Clone for Table { +impl Clone for Table { fn clone(&self) -> Self { Table { data_source: self.data_source.clone(), + _phantom: self._phantom.clone(), table_name: self.table_name.clone(), table_alias: self.table_alias.clone(), @@ -57,7 +60,7 @@ impl Clone for Table { fields: self.fields.clone(), joins: self.joins.clone(), lazy_expressions: self.lazy_expressions.clone(), - refs: self.refs.clone(), + // refs: self.refs.clone(), // Perform a deep clone of the UniqueIdVendor table_aliases: Arc::new(Mutex::new((*self.table_aliases.lock().unwrap()).clone())), @@ -65,10 +68,23 @@ impl Clone for Table { } } -impl Table { - pub fn new(table_name: &str, data_source: T) -> Table { +impl AnyTable for Table { + fn as_any(&self) -> &dyn Any { + self + } + fn get_field(&self, name: &str) -> Option<&Arc> { + self.fields.get(name) + } + fn add_condition(&mut self, condition: Condition) { + self.conditions.push(condition); + } +} + +impl Table { + pub fn new_with_entity(table_name: &str, data_source: T) -> Table { Table { data_source, + _phantom: std::marker::PhantomData, table_name: table_name.to_string(), table_alias: None, @@ -79,12 +95,34 @@ impl Table { fields: IndexMap::new(), joins: IndexMap::new(), lazy_expressions: IndexMap::new(), - refs: IndexMap::new(), + // refs: IndexMap::new(), + table_aliases: Arc::new(Mutex::new(UniqueIdVendor::new())), + } + } +} +impl Table { + pub fn new(table_name: &str, data_source: T) -> Table { + Table { + data_source, + _phantom: std::marker::PhantomData, + + table_name: table_name.to_string(), + table_alias: None, + id_field: None, + title_field: None, + + conditions: Vec::new(), + fields: IndexMap::new(), + joins: IndexMap::new(), + lazy_expressions: IndexMap::new(), + // refs: IndexMap::new(), table_aliases: Arc::new(Mutex::new(UniqueIdVendor::new())), } } +} +impl Table { /// Use a callback with a builder pattern: /// ``` /// let books = BookSet::new().with(|b| { @@ -144,7 +182,7 @@ impl Table { pub fn add_expression( &mut self, name: &str, - expression: impl Fn(&Table) -> Expression + 'static + Sync + Send, + expression: impl Fn(&Table) -> Expression + 'static + Sync + Send, ) { self.lazy_expressions.insert( name.to_string(), @@ -155,7 +193,7 @@ impl Table { pub fn with_expression( mut self, name: &str, - expression: impl Fn(&Table) -> Expression + 'static + Sync + Send, + expression: impl Fn(&Table) -> Expression + 'static + Sync + Send, ) -> Self { self.add_expression(name, expression); self @@ -180,7 +218,7 @@ impl Table { } } -impl ReadableDataSet for Table { +impl ReadableDataSet for Table { fn select_query(&self) -> Query { self.get_select_query() } @@ -192,7 +230,7 @@ impl ReadableDataSet for Table { } } -impl WritableDataSet for Table { +impl WritableDataSet for Table { fn insert_query(&self) -> Query { todo!() } @@ -206,13 +244,13 @@ impl WritableDataSet for Table { } } -pub trait TableDelegate { - fn table(&self) -> &Table; +pub trait TableDelegate { + fn table(&self) -> &Table; fn id(&self) -> Arc { self.table().id() } - fn add_condition(&self, condition: Condition) -> Table { + fn add_condition(&self, condition: Condition) -> Table { self.table().clone().with_condition(condition) } fn sum(&self, field: Arc) -> AssociatedQuery { diff --git a/dorm/src/table/with_fields.rs b/dorm/src/table/with_fields.rs index b11615b..aeeb48a 100644 --- a/dorm/src/table/with_fields.rs +++ b/dorm/src/table/with_fields.rs @@ -10,9 +10,10 @@ use crate::prelude::Operations; use crate::table::Table; use crate::traits::column::Column; use crate::traits::datasource::DataSource; +use crate::traits::entity::Entity; use crate::traits::sql_chunk::SqlChunk; -impl Table { +impl Table { /// Adds a new field to the table. Note, that Field may use an alias pub fn add_field(&mut self, field_name: String, field: Field) { self.fields.insert(field_name, Arc::new(field)); diff --git a/dorm/src/table/with_joins.rs b/dorm/src/table/with_joins.rs index 40f728c..b61ab31 100644 --- a/dorm/src/table/with_joins.rs +++ b/dorm/src/table/with_joins.rs @@ -6,10 +6,11 @@ use crate::prelude::{Operations, SqlChunk}; use crate::query::{JoinQuery, JoinType, QueryConditions}; use crate::table::Table; use crate::traits::datasource::DataSource; +use crate::traits::entity::Entity; use crate::uniqid::UniqueIdVendor; -impl Table { - pub fn with_join(mut self, their_table: Table, our_foreign_id: &str) -> Self { +impl Table { + pub fn with_join(mut self, their_table: Table, our_foreign_id: &str) -> Self { //! Mutate self with a join to another table. //! //! See [Table::add_join] for more details. @@ -70,7 +71,11 @@ impl Table { self } - pub fn add_join(&mut self, mut their_table: Table, our_foreign_id: &str) -> Arc> { + pub fn add_join( + &mut self, + mut their_table: Table, + our_foreign_id: &str, + ) -> Arc> { //! Combine two tables with 1 to 1 relationship into a single table. //! //! Left-Joins their_table table and return self. Assuming their_table has set id field, @@ -151,7 +156,7 @@ impl Table { self.get_join(&their_table_alias).unwrap() } - pub fn get_join(&self, table_alias: &str) -> Option>> { + pub fn get_join(&self, table_alias: &str) -> Option>> { self.joins.get(table_alias).map(|r| r.clone()) } } diff --git a/dorm/src/table/with_queries.rs b/dorm/src/table/with_queries.rs index d4fc465..9aaa0d8 100644 --- a/dorm/src/table/with_queries.rs +++ b/dorm/src/table/with_queries.rs @@ -10,8 +10,9 @@ use crate::query::{Query, QueryType}; use crate::table::Table; use crate::traits::column::Column; use crate::traits::datasource::DataSource; +use crate::traits::entity::Entity; -impl Table { +impl Table { pub fn get_empty_query(&self) -> Query { let mut query = Query::new().with_table(&self.table_name, self.table_alias.clone()); for condition in self.conditions.iter() { diff --git a/dorm/src/table/with_refs.rs b/dorm/src/table/with_refs.rs index f7edcd6..c59ab95 100644 --- a/dorm/src/table/with_refs.rs +++ b/dorm/src/table/with_refs.rs @@ -1,16 +1,23 @@ +use std::any::Any; +use std::sync::Arc; + use anyhow::{anyhow, Result}; +use crate::condition::Condition; +use crate::field::Field; use crate::prelude::Operations; use crate::reference::Reference; use crate::table::Table; +use crate::traits::any::AnyTable; use crate::traits::datasource::DataSource; +use crate::traits::entity::Entity; -impl Table { +impl Table { pub fn has_many( mut self, relation: &str, foreign_key: &str, - cb: impl Fn() -> Table + 'static + Sync + Send, + cb: impl Fn() -> Box + 'static + Sync + Send, ) -> Self { let foreign_key = foreign_key.to_string(); self.add_ref(relation, move |p| { @@ -32,7 +39,7 @@ impl Table { mut self, relation: &str, foreign_key: &str, - cb: impl Fn() -> Table + 'static + Sync + Send, + cb: impl Fn() -> Box + 'static + Sync + Send, ) -> Self { let foreign_key = foreign_key.to_string(); self.add_ref(relation, move |p| { @@ -53,18 +60,26 @@ impl Table { pub fn add_ref( &mut self, relation: &str, - cb: impl Fn(&Table) -> Table + 'static + Sync + Send, + cb: impl Fn(&Table) -> Box + 'static + Sync + Send, ) { let reference = Reference::new(cb); self.refs.insert(relation.to_string(), reference); } - pub fn get_ref(&self, field: &str) -> Result> { - Ok(self - .refs + pub fn get_ref(&self, field: &str) -> Result> { + self.refs .get(field) - .ok_or_else(|| anyhow!("Reference not found"))? - .table(self)) + .map(|reference| reference.as_table(self)) + .ok_or_else(|| anyhow!("Reference not found")) + } + + pub fn get_ref_as(&self, field: &str) -> Result> { + let table = self.get_ref(field)?; + table + .as_any() + .downcast_ref::>() + .map(|t| t.clone()) + .ok_or_else(|| anyhow!("Failed to downcast to specific table type")) } } diff --git a/dorm/src/traits/any.rs b/dorm/src/traits/any.rs new file mode 100644 index 0000000..78f6c43 --- /dev/null +++ b/dorm/src/traits/any.rs @@ -0,0 +1,43 @@ +use std::any::Any; +use std::sync::Arc; + +use crate::condition::Condition; +use crate::field::Field; +use crate::prelude::AssociatedQuery; +use crate::table::Table; + +use super::datasource::DataSource; +use super::entity::Entity; + +/// When defining references between tables, AnyTable represents +/// a target table, that can potentially be associated with a +/// different data source. +/// +/// The implication is that reference fields need to be explicitly +/// fetched and resulting set of "id"s can then be used to define +/// related queries. +/// +/// Table::has_unrelated() can be used to define relation like this. +/// To traverse the relation, use Table::get_unrelated_ref("relation") or +/// Table::get_unrelated_ref_as(ds, "relation"). +/// +/// If tables are defined in the same data source, use has_one(), +/// has_many(), which rely on RelatedTable trait. +/// +pub trait AnyTable: Any + Send + Sync { + fn as_any(&self) -> &dyn Any; + + fn get_field(&self, name: &str) -> Option<&Arc>; + + fn add_condition(&mut self, condition: Condition); +} + +/// When defining references between tables, RelatedTable represents +/// a target table, that resides in the same DataSource and +/// therefore can be referenced inside a query without explicitly +/// fetching the "id"s. +/// +/// +pub trait RelatedTable: AnyTable { + fn field_query(&self, field: Arc) -> AssociatedQuery; +} diff --git a/dorm/src/traits/datasource.rs b/dorm/src/traits/datasource.rs index 1661a84..68e9dab 100644 --- a/dorm/src/traits/datasource.rs +++ b/dorm/src/traits/datasource.rs @@ -6,7 +6,7 @@ use serde_json::Value; use crate::query::Query; -pub trait DataSource: Clone + Send + Sync + std::fmt::Debug { +pub trait DataSource: Clone + Send + Sync + std::fmt::Debug + 'static { // Provided with an arbitrary query, fetch the results and return (Value = arbytrary ) async fn query_fetch(&self, query: &Query) -> Result>>; diff --git a/dorm/src/traits/entity.rs b/dorm/src/traits/entity.rs new file mode 100644 index 0000000..ad3bb3f --- /dev/null +++ b/dorm/src/traits/entity.rs @@ -0,0 +1,8 @@ +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +pub trait Entity: Serialize + DeserializeOwned + Clone + Send + Sync + Sized + 'static {} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EmptyEntity {} + +impl Entity for EmptyEntity {} diff --git a/dorm/src/traits/mod.rs b/dorm/src/traits/mod.rs index 41d898e..d1b2655 100644 --- a/dorm/src/traits/mod.rs +++ b/dorm/src/traits/mod.rs @@ -1,6 +1,8 @@ +pub mod any; pub mod column; pub mod dataset; pub mod datasource; +pub mod entity; // pub mod postgres; pub mod sql_chunk;