From e5391196a325bcfdbeb41ffacfc1643781444e7c Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Sun, 5 Jan 2025 21:34:08 +0000 Subject: [PATCH] feat: table columns now use RwLock to read table alias (#49) * feat: table columns now use RwLock to read table alias * rewrote README slightly * minor cleanups * refactor joins entirely * cleanup readme --- README.md | 24 +- bakery_model/src/bakery.rs | 6 +- bakery_model/src/client.rs | 10 +- bakery_model/src/lineitem.rs | 6 +- bakery_model/src/order.rs | 4 +- bakery_model/src/product.rs | 8 +- vantage/src/datasource/sqlx/postgres.rs | 8 +- .../datasource/sqlx/postgres/uuid_column.rs | 173 ++++++ .../datasource/sqlx/postgres/value_column.rs | 223 +++++++ vantage/src/prelude.rs | 1 - vantage/src/sql/condition.rs | 80 ++- vantage/src/sql/expression/expression_arc.rs | 6 + vantage/src/sql/mod.rs | 1 - vantage/src/sql/operations.rs | 2 +- vantage/src/sql/query.rs | 9 +- vantage/src/sql/query/parts.rs | 56 +- vantage/src/sql/table.rs | 97 ++- vantage/src/sql/table/alias.rs | 584 ++++++++++++++++++ vantage/src/sql/table/column.rs | 153 +---- vantage/src/sql/table/column/sqlcolumn.rs | 9 +- .../src/sql/table/extensions/soft_delete.rs | 8 +- vantage/src/sql/table/join.rs | 6 +- vantage/src/sql/table/reference/many.rs | 13 +- vantage/src/sql/table/reference/one.rs | 9 +- vantage/src/sql/table/with_columns.rs | 67 +- vantage/src/sql/table/with_joins.rs | 112 ++-- vantage/src/sql/table/with_queries.rs | 11 +- vantage/src/uniqid.rs | 110 ++-- vantage/tests/types_test.rs | 1 + 29 files changed, 1374 insertions(+), 423 deletions(-) create mode 100644 vantage/src/datasource/sqlx/postgres/uuid_column.rs create mode 100644 vantage/src/datasource/sqlx/postgres/value_column.rs create mode 100644 vantage/src/sql/table/alias.rs diff --git a/README.md b/README.md index 844192b..881e71a 100644 --- a/README.md +++ b/README.md @@ -6,21 +6,25 @@ Vantage is a type-safe, easy to use database toolkit for Rust that focuses on de without compromising performance. It allows you to work with your database using Rust's strong type system while abstracting away the complexity of SQL queries. -Vantage enables use of Model Driven Architecture (DSL/DDD) patterns in your Rust applications. This -approach separates business and application logic from underlying platform technology. Vantage uses -native Rust syntax to define Entities, Attributes, Validations, Relations, Actions and mapping them -to one or several persistence layers - such as SQL, NoSQL or APIs. +Vantage enables use of Model Driven Architecture, implementing Object Relationship Manager, Query +Builder, and Entity Framework with enterprise-grade enhancements such as soft-delete, aggregation, +event hooks, disjoint subtypes, and high-performance data mocking for your application. -The long-term goal for Vantage is to be a building block for configurable ERP/CRM/HR/Supply business -management system rivaling Odoo or Salesforce written entirely in Rust. +As a part of a broader ecosystem, Vantage enables creation of reusable HR, CRM, Payment and Supply +Chain management solutions rivaling the likes of [Odoo](https://odoo.com/), +[Salesforce](https://www.salesforce.com/) and SAP, while retaining Rust values - open source, performance, +extensibility and safety. ## Quick Start -Your application would typically require a model definition. Here is example: -[bakery_model](bakery_model/src/). You would also need a Postgres database populated with sample data -from [schema-pg.sql](bakery_model/schema-pg.sql) and create role `postgres`. +To get started with Vantage, you first need to define your business model. For example, take a look +at the provided [bakery_model](bakery_model/src/). This model represents a real-world domain that +you can interact with using Vantage. You'll also need a Postgres database with sample data preloaded +from [schema-pg.sql](bakery_model/schema-pg.sql) and a `postgres` role configured. -Once this is in place, you can use Vantage to interract with your data like this: +Once your environment is ready, Vantage enables you to interact with your data through its intuitive +interface, allowing you to focus on business logic without worrying about SQL intricacies. Here’s a +quick example: ```rust use vantage::prelude::*; diff --git a/bakery_model/src/bakery.rs b/bakery_model/src/bakery.rs index f242eee..44c741f 100644 --- a/bakery_model/src/bakery.rs +++ b/bakery_model/src/bakery.rs @@ -30,13 +30,13 @@ impl Bakery { pub trait BakeryTable: AnyTable { // fields - fn id(&self) -> Arc { + fn id(&self) -> Arc { self.get_column("id").unwrap() } - fn name(&self) -> Arc { + fn name(&self) -> Arc { self.get_column("name").unwrap() } - fn profit_margin(&self) -> Arc { + fn profit_margin(&self) -> Arc { self.get_column("profit_margin").unwrap() } diff --git a/bakery_model/src/client.rs b/bakery_model/src/client.rs index 27f8fb4..e36750b 100644 --- a/bakery_model/src/client.rs +++ b/bakery_model/src/client.rs @@ -34,19 +34,19 @@ impl Client { } pub trait ClientTable: SqlTable { - fn name(&self) -> Arc { + fn name(&self) -> Arc { self.get_column("name").unwrap() } - fn email(&self) -> Arc { + fn email(&self) -> Arc { self.get_column("email").unwrap() } - fn contact_details(&self) -> Arc { + fn contact_details(&self) -> Arc { self.get_column("contact_details").unwrap() } - fn bakery_id(&self) -> Arc { + fn bakery_id(&self) -> Arc { self.get_column("bakery_id").unwrap() } - fn is_paying_client(&self) -> Arc { + fn is_paying_client(&self) -> Arc { self.get_column("is_paying_client").unwrap() } diff --git a/bakery_model/src/lineitem.rs b/bakery_model/src/lineitem.rs index a2d2a3f..b1a949a 100644 --- a/bakery_model/src/lineitem.rs +++ b/bakery_model/src/lineitem.rs @@ -44,13 +44,13 @@ pub trait LineItemTable: AnyTable { fn as_table(&self) -> &Table { self.as_any_ref().downcast_ref().unwrap() } - fn quantity(&self) -> Arc { + fn quantity(&self) -> Arc { self.get_column("quantity").unwrap() } - fn order_id(&self) -> Arc { + fn order_id(&self) -> Arc { self.get_column("order_id").unwrap() } - fn product_id(&self) -> Arc { + fn product_id(&self) -> Arc { self.get_column("product_id").unwrap() } fn total(&self) -> Box { diff --git a/bakery_model/src/order.rs b/bakery_model/src/order.rs index e99f796..76ffa6c 100644 --- a/bakery_model/src/order.rs +++ b/bakery_model/src/order.rs @@ -55,10 +55,10 @@ impl Order { } pub trait OrderTable: SqlTable { - fn client_id(&self) -> Arc { + fn client_id(&self) -> Arc { Order::table().get_column("client_id").unwrap() } - fn product_id(&self) -> Arc { + fn product_id(&self) -> Arc { Order::table().get_column("product_id").unwrap() } diff --git a/bakery_model/src/product.rs b/bakery_model/src/product.rs index 530272a..61c213b 100644 --- a/bakery_model/src/product.rs +++ b/bakery_model/src/product.rs @@ -43,13 +43,13 @@ impl Product { pub trait ProductTable: AnyTable { fn with_inventory(self) -> Table; - fn name(&self) -> Arc { + fn name(&self) -> Arc { self.get_column("name").unwrap() } - fn price(&self) -> Arc { + fn price(&self) -> Arc { self.get_column("price").unwrap() } - fn bakery_id(&self) -> Arc { + fn bakery_id(&self) -> Arc { self.get_column("bakery_id").unwrap() } @@ -77,7 +77,7 @@ pub trait ProductInventoryTable: RelatedTable { j.table().clone() } - fn stock(&self) -> Arc { + fn stock(&self) -> Arc { let j = self.j_stock(); j.get_column("stock").unwrap() } diff --git a/vantage/src/datasource/sqlx/postgres.rs b/vantage/src/datasource/sqlx/postgres.rs index 4593c62..d7774f6 100644 --- a/vantage/src/datasource/sqlx/postgres.rs +++ b/vantage/src/datasource/sqlx/postgres.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::{anyhow, Context, Result}; use serde_json::{Map, Value}; -use sqlx::{postgres::PgArguments, Execute}; +use sqlx::postgres::PgArguments; use crate::{ prelude::Query, @@ -12,6 +12,12 @@ use crate::{ use super::sql_to_json::row_to_json; +mod value_column; +pub use value_column::PgValueColumn; + +// mod uuid_column; +// pub use uuid_column::PgUuidColumn; + #[derive(Debug, Clone)] pub struct Postgres { pub pool: Arc, diff --git a/vantage/src/datasource/sqlx/postgres/uuid_column.rs b/vantage/src/datasource/sqlx/postgres/uuid_column.rs new file mode 100644 index 0000000..afc0930 --- /dev/null +++ b/vantage/src/datasource/sqlx/postgres/uuid_column.rs @@ -0,0 +1,173 @@ +use std::sync::Arc; +use std::sync::RwLock; +use std::sync::Weak; + +use crate::expr; +use crate::expr_arc; +use crate::prelude::column::SqlColumn; +use crate::prelude::Column; +use crate::prelude::SqlTable; +use crate::sql::chunk::Chunk; +use crate::sql::Condition; +use crate::sql::Operations; +use crate::sql::WrapArc; +use crate::sql::{Expression, ExpressionArc}; +use crate::traits::column::SqlField; + +#[derive(Debug, Clone)] +pub struct PgUuidColumn { + name: String, + table_alias: Option>>>, + column_alias: Option, +} + +impl PgUuidColumn { + pub fn new(name: &str) -> PgUuidColumn { + PgUuidColumn { + name: name.to_string(), + table_alias: None, + column_alias: None, + } + } + pub fn with_alias(mut self, alias: &str) -> Self { + self.set_alias(alias.to_string()); + self + } +} + +impl SqlColumn for PgUuidColumn { + fn name(&self) -> String { + self.name.clone() + } + fn name_with_table(&self) -> String { + match self.get_table_alias() { + Some(table_alias) => format!("{}.{}", table_alias, self.name), + None => format!("{}", self.name), + } + } + fn set_table_alias(&mut self, table_alias: Weak>>) { + self.table_alias = Some(table_alias); + } + fn get_table_alias(&self) -> Option { + let weak_ref = self.table_alias.as_ref()?; + let arc_ref = weak_ref.upgrade()?; + let guard = arc_ref.read().ok()?; + guard.clone() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_alias(&mut self, alias: String) { + self.column_alias = Some(alias); + } + + fn get_alias(&self) -> Option { + self.column_alias.clone() + } +} + +impl Chunk for PgUuidColumn { + fn render_chunk(&self) -> Expression { + Arc::new(self.clone()).render_chunk() + } +} +impl Operations for PgUuidColumn {} + +impl Operations for Arc { + fn eq(&self, other: &impl Chunk) -> Condition { + let column: Arc = Arc::new(Box::new((**self).clone()) as Box); + + Condition::from_field(column, "=", WrapArc::wrap_arc(other.render_chunk())) + } + + // fn add(&self, other: impl SqlChunk) -> Expression { + // let chunk = other.render_chunk(); + // expr_arc!(format!("{} + {{}}", &self.name), chunk).render_chunk() + // } +} + +impl Chunk for Arc { + fn render_chunk(&self) -> Expression { + expr!(self.name_with_table()) + } +} + +impl SqlField for Arc { + fn render_column(&self, mut alias: Option<&str>) -> Expression { + // If the alias is the same as the field name, we don't need to render it + if alias.is_some() && alias.unwrap() == self.name { + alias = None; + } + + let alias = alias.or(self.column_alias.as_deref()); + + if let Some(alias) = alias { + expr!(format!( + "{} AS {}", + self.name_with_table(), + alias.to_string() + )) + } else { + expr!(self.name_with_table()) + } + } + fn calculated(&self) -> bool { + false + } +} + +impl From for PgUuidColumn { + fn from(name: String) -> Self { + PgUuidColumn { + name, + table_alias: None, + column_alias: None, + } + } +} + +impl From<&str> for PgUuidColumn { + fn from(name: &str) -> Self { + name.to_string().into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_field() { + let field = Arc::new(PgUuidColumn::new("id")); + let (sql, params) = field.render_chunk().split(); + + assert_eq!(sql, "id"); + assert_eq!(params.len(), 0); + + let (sql, params) = field.render_column(Some("id")).render_chunk().split(); + assert_eq!(sql, "id"); + assert_eq!(params.len(), 0); + + let (sql, params) = &field.render_column(Some("id_alias")).render_chunk().split(); + assert_eq!(sql, "id AS id_alias"); + assert_eq!(params.len(), 0); + } + + #[test] + fn test_eq() { + let field = Arc::new(PgUuidColumn::new("id")); + let (sql, params) = field.eq(&1).render_chunk().split(); + + assert_eq!(sql, "(id = {})"); + assert_eq!(params.len(), 1); + assert_eq!(params[0], 1); + + let f_age = Arc::new(PgUuidColumn::new("age").with_alias("u")); + let (sql, params) = f_age.add(5).eq(&18).render_chunk().split(); + + assert_eq!(sql, "((u.age) + ({}) = {})"); + assert_eq!(params.len(), 2); + assert_eq!(params[0], 5); + assert_eq!(params[1], 18); + } +} diff --git a/vantage/src/datasource/sqlx/postgres/value_column.rs b/vantage/src/datasource/sqlx/postgres/value_column.rs new file mode 100644 index 0000000..0928587 --- /dev/null +++ b/vantage/src/datasource/sqlx/postgres/value_column.rs @@ -0,0 +1,223 @@ +use std::sync::Arc; +use std::sync::RwLock; +use std::sync::Weak; + +use tokio_postgres::types::Format; + +use crate::expr; +use crate::expr_arc; +use crate::prelude::column::SqlColumn; +use crate::prelude::Column; +use crate::prelude::TableAlias; +use crate::sql::chunk::Chunk; +use crate::sql::expression::{Expression, ExpressionArc}; +use crate::sql::Condition; +use crate::sql::Operations; +use crate::sql::WrapArc; +use crate::traits::column::SqlField; + +#[derive(Debug, Clone)] +pub struct PgValueColumn { + name: String, + table_alias: Option, + use_table_alias: bool, + use_quotes: bool, + column_alias: Option, +} + +impl PgValueColumn { + pub fn new(name: &str) -> PgValueColumn { + PgValueColumn { + name: name.to_string(), + table_alias: None, + use_table_alias: false, + use_quotes: false, + column_alias: None, + } + } + pub fn with_alias(mut self, alias: &str) -> Self { + self.set_alias(alias.to_string()); + self + } + pub fn with_quotes(&self) -> Self { + let mut c = self.clone(); + c.use_quotes = true; + c + } + pub fn with_table_alias(&self) -> Self { + let mut c = self.clone(); + c.use_table_alias = true; + c + } +} + +impl SqlColumn for PgValueColumn { + fn name(&self) -> String { + self.name.clone() + } + fn name_with_table(&self) -> String { + match &self.table_alias { + Some(alias) => { + if self.use_table_alias { + if self.use_quotes { + format!("\"{}\".\"{}\"", alias.get(), self.name) + } else { + format!("{}.{}", alias.get(), self.name) + } + } else { + match alias.try_get() { + Some(table_alias) => { + if self.use_quotes { + format!("\"{}\".\"{}\"", table_alias, self.name) + } else { + format!("{}.{}", table_alias, self.name) + } + } + None => { + if self.use_quotes { + format!("\"{}\"", self.name) + } else { + format!("{}", self.name) + } + } + } + } + } + None => { + if self.use_quotes { + format!("\"{}\"", self.name) + } else { + format!("{}", self.name) + } + } + } + } + fn set_table_alias(&mut self, table_alias: &TableAlias) { + self.table_alias = Some(table_alias.clone()); + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn get_table_alias(&self) -> &Option { + &self.table_alias + } + // fn set_table_alias(&mut self, alias: String) { + // self.table_alias = Some(alias); + // } + fn set_alias(&mut self, alias: String) { + self.column_alias = Some(alias); + } + + fn get_alias(&self) -> Option { + self.column_alias.clone() + } +} + +impl Chunk for PgValueColumn { + fn render_chunk(&self) -> Expression { + expr!(self.name_with_table()) + } +} +impl Operations for PgValueColumn {} + +impl Operations for Arc { + fn eq(&self, other: &impl Chunk) -> Condition { + let column: Arc = Arc::new(Box::new((**self).clone()) as Box); + + Condition::from_field(column, "=", WrapArc::wrap_arc(other.render_chunk())) + } + + // fn add(&self, other: impl SqlChunk) -> Expression { + // let chunk = other.render_chunk(); + // expr_arc!(format!("{} + {{}}", &self.name), chunk).render_chunk() + // } +} + +impl Chunk for Arc { + fn render_chunk(&self) -> Expression { + expr!(self.name_with_table()) + } +} + +impl SqlField for Arc { + fn render_column(&self, mut alias: Option<&str>) -> Expression { + // If the alias is the same as the field name, we don't need to render it + if alias.is_some() && alias.unwrap() == self.name { + alias = None; + } + + let alias = alias.or(self.column_alias.as_deref()); + + if let Some(alias) = alias { + expr!(format!( + "{} AS {}", + self.name_with_table(), + alias.to_string() + )) + } else { + expr!(self.name_with_table()) + } + } + fn calculated(&self) -> bool { + false + } +} + +impl From for PgValueColumn { + fn from(name: String) -> Self { + PgValueColumn { + name, + use_table_alias: false, + use_quotes: false, + table_alias: None, + column_alias: None, + } + } +} + +impl From<&str> for PgValueColumn { + fn from(name: &str) -> Self { + name.to_string().into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_field() { + let field = Arc::new(PgValueColumn::new("id")); + let (sql, params) = field.render_chunk().split(); + + assert_eq!(sql, "id"); + assert_eq!(params.len(), 0); + + let (sql, params) = field.render_column(Some("id")).render_chunk().split(); + assert_eq!(sql, "id"); + assert_eq!(params.len(), 0); + + let (sql, params) = &field.render_column(Some("id_alias")).render_chunk().split(); + assert_eq!(sql, "id AS id_alias"); + assert_eq!(params.len(), 0); + } + + #[test] + fn test_eq() { + let field = Arc::new(PgValueColumn::new("id")); + let (sql, params) = field.eq(&1).render_chunk().split(); + + assert_eq!(sql, "(id = {})"); + assert_eq!(params.len(), 1); + assert_eq!(params[0], 1); + + let f_age = Arc::new(PgValueColumn::new("age").with_alias("u")); + let (sql, params) = f_age.add(5).eq(&18).render_chunk().split(); + + // dispite the "alias" of "u" the column name is used here, alias is ignored + assert_eq!(sql, "((age) + ({}) = {})"); + assert_eq!(params.len(), 2); + assert_eq!(params[0], 5); + assert_eq!(params[1], 18); + } +} diff --git a/vantage/src/prelude.rs b/vantage/src/prelude.rs index 956fcf5..3b759cf 100644 --- a/vantage/src/prelude.rs +++ b/vantage/src/prelude.rs @@ -6,7 +6,6 @@ pub use crate::datasource::sqlx::postgres::*; pub use crate::expr; pub use crate::expr_arc; pub use crate::mocks::MockDataSource; -pub use crate::sql::table::Column; pub use crate::traits::column::SqlField; pub use crate::traits::DataSource; pub use crate::{ diff --git a/vantage/src/sql/condition.rs b/vantage/src/sql/condition.rs index c9a78ab..1d26a8c 100644 --- a/vantage/src/sql/condition.rs +++ b/vantage/src/sql/condition.rs @@ -1,15 +1,14 @@ -use std::sync::Arc; - use serde_json::Value; +use std::sync::Arc; +use std::sync::RwLock; use crate::expr; -use crate::prelude::Column; use crate::sql::expression::{Expression, ExpressionArc}; use crate::sql::Chunk; -use super::table::column::SqlColumn; +use super::table::Column; -#[derive(Debug, Clone)] +#[derive(Clone)] enum ConditionOperand { Column(Arc), Expression(Box), @@ -17,6 +16,12 @@ enum ConditionOperand { Value(Value), } +impl std::fmt::Debug for ConditionOperand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + #[derive(Debug, Clone)] pub struct Condition { field: ConditionOperand, @@ -27,12 +32,12 @@ pub struct Condition { #[allow(dead_code)] impl Condition { pub fn from_field( - field: Arc, + column: Arc, operation: &str, value: Arc>, ) -> Condition { Condition { - field: ConditionOperand::Column(field), + field: ConditionOperand::Column(column), operation: operation.to_string(), value, } @@ -60,17 +65,15 @@ impl Condition { } } - pub fn set_table_alias(&mut self, alias: &str) { - match &mut self.field { - ConditionOperand::Column(field) => { - let mut f = field.as_ref().clone(); - f.set_table_alias(alias.to_string()); - *field = Arc::new(f); - } - ConditionOperand::Condition(condition) => condition.set_table_alias(alias), - _ => {} - } - } + // pub fn set_table_alias(&mut self, alias: &str) { + // match &mut self.field { + // ConditionOperand::Column(field) => { + // field.set_table_alias(alias.to_string()); + // } + // ConditionOperand::Condition(condition) => condition.set_table_alias(alias), + // _ => {} + // } + // } pub fn from_value(operand: Value, operation: &str, value: Arc>) -> Condition { Condition { @@ -82,7 +85,7 @@ impl Condition { fn render_operand(&self) -> Expression { match self.field.clone() { - ConditionOperand::Column(field) => field.render_chunk(), + ConditionOperand::Column(field) => expr!(field.name_with_table()), ConditionOperand::Expression(expression) => expression.render_chunk(), ConditionOperand::Condition(condition) => condition.render_chunk(), ConditionOperand::Value(value) => expr!("{}", value.clone()).render_chunk(), @@ -113,13 +116,27 @@ impl Chunk for Condition { #[cfg(test)] mod tests { + use serde_json::json; + + use crate::{ + mocks::MockDataSource, + prelude::{AnyTable, SqlTable}, + sql::Table, + }; + use super::*; #[test] fn test_condition() { - let field = Arc::new(Column::new("id".to_string(), None)); + let ds = MockDataSource::new(&json!([])); + + let table = Table::new("test", ds).with_column("id"); - let condition = Condition::from_field(field, "=", Arc::new(Box::new("1".to_string()))); + let condition = Condition::from_field( + table.get_column_box("id").unwrap(), + "=", + Arc::new(Box::new("1".to_string())), + ); let (sql, params) = condition.render_chunk().split(); assert_eq!(sql, "(id = {})"); @@ -142,13 +159,22 @@ mod tests { #[test] fn test_and() { - let f_married = Arc::new(Column::new("married".to_string(), None)); - let f_divorced = Arc::new(Column::new("divorced".to_string(), None)); + let ds = MockDataSource::new(&json!([])); - let condition = - Condition::from_field(f_married, "=", Arc::new(Box::new("yes".to_string()))).and( - Condition::from_field(f_divorced, "=", Arc::new(Box::new("yes".to_string()))), - ); + let table = Table::new("test", ds) + .with_column("married") + .with_column("divorced"); + + let condition = Condition::from_field( + table.get_column_box("married").unwrap(), + "=", + Arc::new(Box::new("yes".to_string())), + ) + .and(Condition::from_field( + table.get_column_box("divorced").unwrap(), + "=", + Arc::new(Box::new("yes".to_string())), + )); let (sql, params) = condition.render_chunk().split(); diff --git a/vantage/src/sql/expression/expression_arc.rs b/vantage/src/sql/expression/expression_arc.rs index bac4de9..78dfe42 100644 --- a/vantage/src/sql/expression/expression_arc.rs +++ b/vantage/src/sql/expression/expression_arc.rs @@ -64,6 +64,12 @@ impl ExpressionArc { let parameters = Expression::from_vec(parameters, ", "); expr_arc!(format!("{}({{}})", function_name), parameters) } + + /// Places values into the template and returns a String. + /// Useful for debugging, but not for SQL execution. + pub fn preview(&self) -> String { + self.render_chunk().preview() + } } impl Chunk for ExpressionArc { diff --git a/vantage/src/sql/mod.rs b/vantage/src/sql/mod.rs index e38a70f..d247982 100644 --- a/vantage/src/sql/mod.rs +++ b/vantage/src/sql/mod.rs @@ -25,6 +25,5 @@ pub use operations::Operations; pub use condition::Condition; -pub use table::Column; pub use table::Join; pub use table::Table; diff --git a/vantage/src/sql/operations.rs b/vantage/src/sql/operations.rs index a3f4786..ffd6675 100644 --- a/vantage/src/sql/operations.rs +++ b/vantage/src/sql/operations.rs @@ -125,7 +125,7 @@ mod tests { #[test] fn test_upper() { - let a = Arc::new(Column::new("name".to_string(), None)); + let a = Arc::new(PgValueColumn::new("name")); let b = a.upper(); assert_eq!(b.render_chunk().sql(), "UPPER(name)"); diff --git a/vantage/src/sql/query.rs b/vantage/src/sql/query.rs index 6d83316..ba0a12d 100644 --- a/vantage/src/sql/query.rs +++ b/vantage/src/sql/query.rs @@ -7,10 +7,10 @@ pub use with_traits::SqlQuery; use crate::{ expr, expr_arc, + prelude::PgValueColumn, sql::{ chunk::Chunk, expression::{Expression, ExpressionArc}, - table::Column, }, traits::column::SqlField, }; @@ -19,6 +19,8 @@ mod parts; pub use parts::*; +use super::table::TableAlias; + #[derive(Debug, Clone)] pub struct Query { table: QuerySource, @@ -123,10 +125,7 @@ impl Query { } // Simplified ways to define a field with a string pub fn with_column_field(self, name: &str) -> Self { - self.with_field( - name.to_string(), - Arc::new(Column::new(name.to_string(), None)), - ) + self.with_field(name.to_string(), Arc::new(PgValueColumn::new(name))) } pub fn with_field_arc(mut self, name: String, field: Arc>) -> Self { diff --git a/vantage/src/sql/query/parts.rs b/vantage/src/sql/query/parts.rs index 5c214c8..147a7e7 100644 --- a/vantage/src/sql/query/parts.rs +++ b/vantage/src/sql/query/parts.rs @@ -1,6 +1,10 @@ use std::sync::Arc; -use crate::{expr, expr_arc, prelude::Expression, prelude::ExpressionArc, sql::chunk::Chunk}; +use crate::{ + expr, expr_arc, + prelude::{Expression, ExpressionArc, TableAlias}, + sql::chunk::Chunk, +}; use super::Query; @@ -18,6 +22,7 @@ pub enum QueryType { pub enum QuerySource { None, Table(String, Option), + TableWithAlias(String, Arc), Query(Arc>, Option), Expression(Expression, Option), } @@ -33,9 +38,18 @@ impl QuerySource { query.render_chunk() ) .render_chunk(), - QuerySource::Table(table, None) => expr!(format!("{}{}", prefix, table)), - QuerySource::Table(table, Some(alias)) => { - expr!(format!("{}{} AS {}", prefix, table, alias)) + QuerySource::TableWithAlias(table, a) => { + if a.alias_is_some() { + expr!(format!("{}{} AS {}", prefix, table, a.get())) + } else { + expr!(format!("{}{}", prefix, table)) + } + } + QuerySource::Table(table, None) => { + expr!(format!("{}{}", prefix, table)) + } + QuerySource::Table(table, Some(a)) => { + expr!(format!("{}{} AS {}", prefix, table, a)) } QuerySource::Expression(expression, None) => { expr_arc!(format!("{}{{}}", prefix), expression.render_chunk()).render_chunk() @@ -154,15 +168,16 @@ mod tests { use serde_json::Value; use crate::{ - prelude::{Column, Operations}, - sql::Condition, + prelude::PgValueColumn, + sql::{Condition, Operations}, }; use super::*; #[test] fn test_query_source_render() { - let query = QuerySource::Table("user".to_string(), None); + let query = + QuerySource::TableWithAlias("user".to_string(), Arc::new(TableAlias::new("user"))); let result = query.render_chunk().split(); assert_eq!(result.0, "FROM user"); @@ -198,18 +213,15 @@ mod tests { #[test] fn test_conditions_expressions() { - let name = Arc::new(Column::new("name".to_string(), None)); - let surname = Arc::new(Column::new("surname".to_string(), Some("sur".to_string()))); + let name = Arc::new(PgValueColumn::new("name")); + let surname = Arc::new(PgValueColumn::new("surname")); let conditions = QueryConditions::having().with_condition( Condition::or(name.eq(&surname), surname.eq(&Value::Null)).render_chunk(), ); let result = conditions.render_chunk().split(); - assert_eq!( - result.0, - " HAVING ((name = sur.surname) OR (sur.surname = {}))" - ); + assert_eq!(result.0, " HAVING ((name = surname) OR (surname = {}))"); assert_eq!(result.1.len(), 1); assert_eq!(result.1[0], Value::Null); } @@ -245,4 +257,22 @@ mod tests { assert_eq!(result.0, " JOIN user AS u ON u.id = address.user_id"); assert_eq!(result.1.len(), 0); } + + #[test] + fn test_join_with_alias_render2() { + let a = Arc::new(TableAlias::new("user")); + a.set("u"); + let join_query = JoinQuery { + join_type: JoinType::Inner, + source: QuerySource::TableWithAlias("user".to_string(), a), + on_conditions: QueryConditions { + condition_type: ConditionType::On, + conditions: vec![expr!("u.id = address.user_id")], + }, + }; + let result = join_query.render_chunk().split(); + + assert_eq!(result.0, " JOIN user AS u ON u.id = address.user_id"); + assert_eq!(result.1.len(), 0); + } } diff --git a/vantage/src/sql/table.rs b/vantage/src/sql/table.rs index aa2b43c..3b896f4 100644 --- a/vantage/src/sql/table.rs +++ b/vantage/src/sql/table.rs @@ -25,11 +25,13 @@ use std::any::{type_name, Any}; use std::fmt::{Debug, Display}; use std::ops::Deref; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; +mod alias; pub mod column; mod join; +pub use alias::TableAlias; pub use column::Column; use column::SqlColumn; pub use extensions::{Hooks, SoftDelete, TableExtension}; @@ -37,7 +39,7 @@ pub use join::Join; use crate::expr_arc; use crate::lazy_expression::LazyExpression; -use crate::prelude::{AssociatedQuery, Expression}; +use crate::prelude::{AssociatedQuery, Expression, PgValueColumn, SqlField}; use crate::sql::Condition; use crate::sql::ExpressionArc; use crate::sql::Query; @@ -69,7 +71,8 @@ pub trait AnyTable: Any + Send + Sync { fn as_any_ref(&self) -> &dyn Any; - fn get_column(&self, name: &str) -> Option>; + fn get_column(&self, name: &str) -> Option>; + fn get_column_box(&self, name: &str) -> Option>>; fn add_condition(&mut self, condition: Condition); fn hooks(&self) -> &Hooks; @@ -82,16 +85,13 @@ pub trait AnyTable: Any + Send + Sync { /// /// pub trait RelatedTable: SqlTable { - fn column_query(&self, column: Arc) -> AssociatedQuery; + fn column_query(&self, column: Arc) -> AssociatedQuery; fn add_columns_into_query(&self, query: Query, alias_prefix: Option<&str>) -> Query; fn get_join(&self, table_alias: &str) -> Option>>; - fn get_alias(&self) -> Option<&String>; fn get_table_name(&self) -> Option<&String>; - fn set_alias(&mut self, alias: &str); - - fn get_columns(&self) -> &IndexMap>; + fn get_columns(&self) -> &IndexMap>; fn get_title_column(&self) -> Option; } @@ -121,16 +121,15 @@ pub struct Table { _phantom: std::marker::PhantomData, table_name: String, - table_alias: Option, + alias: TableAlias, id_column: Option, title_column: Option, conditions: Vec, - columns: IndexMap>, + columns: IndexMap>, joins: IndexMap>>, lazy_expressions: IndexMap>, refs: IndexMap>>, - table_aliases: Arc>, hooks: Hooks, } @@ -153,9 +152,23 @@ mod with_fetching; mod extensions; -pub trait SqlTable: TableWithColumns + TableWithQueries {} +pub trait SqlTable: TableWithColumns + TableWithQueries { + fn get_alias(&self) -> &TableAlias; + fn set_alias(&mut self, alias: &str); + fn mut_alias(&mut self) -> &mut TableAlias; +} -impl SqlTable for Table {} +impl SqlTable for Table { + fn get_alias(&self) -> &TableAlias { + &self.alias + } + fn set_alias(&mut self, alias: &str) { + self.alias.set(alias); + } + fn mut_alias(&mut self) -> &mut TableAlias { + &mut self.alias + } +} impl Clone for Table { fn clone(&self) -> Self { @@ -164,7 +177,7 @@ impl Clone for Table { _phantom: self._phantom.clone(), table_name: self.table_name.clone(), - table_alias: self.table_alias.clone(), + alias: self.alias.deep_clone(), id_column: self.id_column.clone(), title_column: self.title_column.clone(), @@ -175,8 +188,7 @@ impl Clone for Table { refs: self.refs.clone(), // Perform a deep clone of the UniqueIdVendor - table_aliases: Arc::new(Mutex::new((*self.table_aliases.lock().unwrap()).clone())), - + // table_aliases: Arc::new(Mutex::new((*self.table_aliases.lock().unwrap()).clone())), hooks: self.hooks.clone(), } } @@ -199,9 +211,14 @@ impl AnyTable for Table { /// Handy way to reference column by name, for example to use with [`Operations`]. /// /// [`Operations`]: super::super::operations::Operations - fn get_column(&self, name: &str) -> Option> { + fn get_column(&self, name: &str) -> Option> { self.columns.get(name).cloned() } + fn get_column_box(&self, name: &str) -> Option>> { + let c = (**self.columns.get(name)?).clone(); + let c = Box::new(c) as Box; + Some(Arc::new(c)) + } fn add_condition(&mut self, condition: Condition) { self.conditions.push(condition); } @@ -211,7 +228,7 @@ impl AnyTable for Table { } impl RelatedTable for Table { - fn column_query(&self, column: Arc) -> AssociatedQuery { + fn column_query(&self, column: Arc) -> AssociatedQuery { let query = self.get_empty_query().with_field(column.name(), column); AssociatedQuery::new(query, self.data_source.clone()) } @@ -222,7 +239,7 @@ impl RelatedTable for Table { let column_val = if let Some(alias_prefix) = &alias_prefix { let alias = format!("{}_{}", alias_prefix, column_key); let mut column_val = column_val.deref().clone(); - column_val.set_column_alias(alias); + column_val.set_alias(alias); Arc::new(column_val) } else { column_val.clone() @@ -230,7 +247,7 @@ impl RelatedTable for Table { query = query.with_field( column_val .deref() - .get_column_alias() + .get_alias() .unwrap_or_else(|| column_key.clone()), column_val, ); @@ -242,29 +259,10 @@ impl RelatedTable for Table { query } - - fn get_alias(&self) -> Option<&String> { - self.table_alias.as_ref() - } - fn set_alias(&mut self, alias: &str) { - if let Some(alias) = &self.table_alias { - self.table_aliases.lock().unwrap().dont_avoid(alias); - } - self.table_alias = Some(alias.to_string()); - self.table_aliases.lock().unwrap().avoid(alias); - for column in self.columns.values_mut() { - let mut new_column = column.deref().deref().clone(); - new_column.set_table_alias(alias.to_string()); - *column = Arc::new(new_column); - } - for condition in &mut self.conditions { - condition.set_table_alias(alias); - } - } fn get_table_name(&self) -> Option<&String> { Some(&self.table_name) } - fn get_columns(&self) -> &IndexMap> { + fn get_columns(&self) -> &IndexMap> { &self.columns } fn get_join(&self, table_alias: &str) -> Option>> { @@ -282,7 +280,7 @@ impl Table { _phantom: std::marker::PhantomData, table_name: table_name.to_string(), - table_alias: None, + alias: TableAlias::new(table_name), id_column: None, title_column: None, @@ -291,7 +289,6 @@ impl Table { joins: IndexMap::new(), lazy_expressions: IndexMap::new(), refs: IndexMap::new(), - table_aliases: Arc::new(Mutex::new(UniqueIdVendor::new())), hooks: Hooks::new(), } @@ -305,7 +302,7 @@ impl Table { _phantom: std::marker::PhantomData, table_name: table_name.to_string(), - table_alias: None, + alias: TableAlias::new(table_name), id_column: None, title_column: None, @@ -314,7 +311,6 @@ impl Table { joins: IndexMap::new(), lazy_expressions: IndexMap::new(), refs: IndexMap::new(), - table_aliases: Arc::new(Mutex::new(UniqueIdVendor::new())), hooks: Hooks::new(), } @@ -345,7 +341,7 @@ impl Table { _phantom: std::marker::PhantomData, table_name: self.table_name, - table_alias: self.table_alias, + alias: self.alias, id_column: self.id_column, title_column: self.title_column, @@ -355,9 +351,6 @@ impl Table { lazy_expressions: IndexMap::new(), // TODO: cast proprely refs: IndexMap::new(), // TODO: cast proprely - // Perform a deep clone of the UniqueIdVendor - table_aliases: Arc::new(Mutex::new((*self.table_aliases.lock().unwrap()).clone())), - hooks: self.hooks, } } @@ -449,13 +442,13 @@ impl Table { pub trait TableDelegate: TableWithColumns { fn table(&self) -> &Table; - fn id(&self) -> Arc { + fn id(&self) -> Arc { self.table().id() } fn add_condition(&self, condition: Condition) -> Table { self.table().clone().with_condition(condition) } - fn sum(&self, column: Arc) -> AssociatedQuery { + fn sum(&self, column: Arc) -> AssociatedQuery { self.table().sum(column) } } @@ -494,8 +487,8 @@ mod tests { let data_source = MockDataSource::new(&data); let books = Table::new("book", data_source) .with(|b| { - b.add_column("title".to_string(), Column::new("title".to_string(), None)); - b.add_column("price".to_string(), Column::new("price".to_string(), None)); + b.add_column("title".to_string(), PgValueColumn::new("title")); + b.add_column("price".to_string(), PgValueColumn::new("price")); }) .with(|b| { b.add_condition(b.get_column("title").unwrap().gt(100)); diff --git a/vantage/src/sql/table/alias.rs b/vantage/src/sql/table/alias.rs new file mode 100644 index 0000000..0a79205 --- /dev/null +++ b/vantage/src/sql/table/alias.rs @@ -0,0 +1,584 @@ +use crate::{traits::DataSource, uniqid::UniqueIdVendor}; +use anyhow::{anyhow, Context, Result}; +use indexmap::IndexMap; +use std::sync::{Arc, Mutex, RwLock}; +use tokio_postgres::types::ToSql; + +use super::{Join, SqlTable}; + +/// For a table (in a wider join) describes how the table should be aliased. +/// AutoAssigned alias can be automatically changed to resolve conflicts. Explicitly +/// requesting alias will not be changed automatically, but can be changed manually. +/// None would never attempt to alias a table (uses table name) and Any will be +/// automatically changed to AutoAssigned() when a conflict arises. +#[derive(Debug, Clone, PartialEq, Eq)] +enum DesiredAlias { + /// Never use any alias + None, + /// Use any alias + Any, + /// Use alias that was auto-assigned + AutoAssigned(T), + /// Use explicitly requested alias + ExplicitlyRequested(T), +} +impl DesiredAlias { + pub fn unwrap(&self) -> &T { + match self { + DesiredAlias::None => panic!("Alias is disabled"), + DesiredAlias::Any => panic!("Alias was not set yet"), + DesiredAlias::AutoAssigned(t) => t, + DesiredAlias::ExplicitlyRequested(t) => t, + } + } + pub fn is_some(&self) -> bool { + match self { + DesiredAlias::None => false, + DesiredAlias::Any => false, + _ => true, + } + } +} + +impl Into> for DesiredAlias { + fn into(self) -> Option { + match self { + DesiredAlias::None => None, + DesiredAlias::Any => None, + DesiredAlias::AutoAssigned(t) => Some(t), + DesiredAlias::ExplicitlyRequested(t) => Some(t), + } + } +} + +/// TableAlias is a shareable configuration for table alias configuration, +/// accessible by columns of that table. +/// +/// Columns can request table alias configuration at any time through +/// table_alias.try_get() (equal to read().unwrap().try_get()) which returns +/// optional table alias. This will inform column if it should prefix +/// itself with a table. +/// +/// Column may want to explicitly use a table alias through table_alias.get(), +/// which will return either table alias or table name. +/// +/// If Table makes use of joins, the table aliases will be automatically re-assigned +/// and enforced for all columns, even if column was cloned earlier. +/// +/// Cloning the table will deep-clone alias config and therefore will have +/// independent alias settings +/// +/// Joining tables does merges unique id vendor across all tables, and all +/// alias configuration will be set to enforce alias. + +#[derive(Debug, Clone)] +struct TableAliasConfig { + // Copy of a table name + table_name: String, + + // User requested for table to have a custom alias + desired_alias: DesiredAlias, + + // Should we include table(or alias) when rendering field queries (e.g. select user.name from user) + specify_table_for_field_queries: bool, + + // ID generated shared by all joined tables to re-generate DesiredAlias::AutoAssigned + alias_vendor: Arc>, +} + +impl TableAliasConfig { + pub fn new(table_name: &str) -> Self { + let mut id_vendor = UniqueIdVendor::new(); + let alias = id_vendor.avoid(table_name); + + TableAliasConfig { + table_name: table_name.to_string(), + desired_alias: DesiredAlias::Any, + specify_table_for_field_queries: false, + alias_vendor: Arc::new(Mutex::new(id_vendor)), + } + } + + /// When our table joins other tables, we enforce table prefix for all columns. + /// This is important because columns can be ambiguous. In addition to setting + /// the flag, we will also auto-generate alias if DesiredAlias is set to Any + pub fn enforce_table_in_field_queries(&mut self) { + if self.specify_table_for_field_queries { + return; + } + if self.desired_alias == DesiredAlias::Any { + // we will treat table name as alias now, so we must reserve it + // let t = self.table_name.clone(); + // self.set(&t); + } + self.specify_table_for_field_queries = true; + } + + /// Use custom alias for this table. If alias was used previously, it won't be reserved + /// anymore. + pub fn set(&mut self, alias: &str) { + // If alias is ExplicitlyRequested or AutoAssigned, we must release it + if self.desired_alias.is_some() { + self.alias_vendor + .lock() + .unwrap() + .dont_avoid(self.desired_alias.unwrap()) + .unwrap(); + } + let alias = self.alias_vendor.lock().unwrap().get_uniq_id(alias); + self.desired_alias = DesiredAlias::ExplicitlyRequested(alias.to_string()); + } + + pub fn set_short_alias(&mut self) { + self.desired_alias = DesiredAlias::AutoAssigned( + self.alias_vendor + .lock() + .unwrap() + .get_short_uniq_id(&self.table_name), + ) + } + + /// Used by a column if it wants to be explicitly prefixed (e.g. used in subquery) + pub fn get(&self) -> String { + if self.desired_alias.is_some() { + self.desired_alias.unwrap().clone() + } else { + self.table_name.clone() + } + } + + pub fn alias_is_some(&self) -> bool { + self.desired_alias.is_some() + } + + /// Used by a column natively, to guard against situations when we join more tables + /// and therefore all fields should be prefixed to avoid ambiguity + pub fn try_get(&self) -> Option { + if self.specify_table_for_field_queries { + Some(self.get()) + } else { + None + } + } + + /// Used for FROM field to append "AS" + pub fn try_get_for_from(&self) -> Option { + if self.desired_alias.is_some() { + Some(self.get()) + } else { + None + } + } + + pub fn deep_clone(&self) -> Self { + TableAliasConfig { + table_name: self.table_name.clone(), + desired_alias: self.desired_alias.clone(), + specify_table_for_field_queries: self.specify_table_for_field_queries, + alias_vendor: Arc::new(Mutex::new(UniqueIdVendor::new())), + } + } + + /// Get rid of existing ID vendor, and replace with a clone of the one + /// we are providing. Subsequently you will need to lock alias with + /// _lock_explicit_alias and _lock_implicit_alias + pub fn _reset_id_vendor(&mut self, id_vendor: Arc>) { + self.alias_vendor = id_vendor; + } + + /// Assuming that uniq id vendor was set but not initialized yet with + /// our table - reserve explicit our explicit alias (if we have it) + pub fn _lock_explicit_alias(&mut self) -> Result<()> { + match &self.desired_alias { + DesiredAlias::ExplicitlyRequested(a) => self.alias_vendor.lock().unwrap().avoid(a)?, + DesiredAlias::None => self.alias_vendor.lock().unwrap().avoid(&self.table_name)?, + _ => {} + } + Ok(()) + } + + /// After all tables have their explicit aliases locked in, we will do + /// another pass calculating auto-assigned aliases. The logic here would + /// be to use shortened table name (e.g. p for person) but append _1, _2 + /// if it clashes with similar tables. + pub fn _lock_implicit_alias(&mut self) { + match &self.desired_alias { + DesiredAlias::ExplicitlyRequested(_) => return, + DesiredAlias::None => return, + _ => { + self.desired_alias = DesiredAlias::AutoAssigned( + self.alias_vendor + .lock() + .unwrap() + .get_short_uniq_id(&self.table_name), + ) + } + } + } + + pub fn _reassign_alias( + &mut self, + our_old_joins: IndexMap>>, + their_old_joins: IndexMap>>, + ) -> Result>>> { + let mut result = IndexMap::new(); + + let tmp: Vec<_> = our_old_joins + .into_values() + .chain(their_old_joins.into_values()) + .map(|j| j.split()) + .collect(); + + self.alias_vendor = Arc::new(Mutex::new(UniqueIdVendor::new())); + + for (table, _) in &tmp { + table + .alias + .config + .write() + .unwrap() + ._reset_id_vendor(self.alias_vendor.clone()); + } + + self._lock_explicit_alias() + .context(anyhow!("for primary table"))?; + + for (table, _) in &tmp { + table.alias.config.write().unwrap()._lock_explicit_alias()?; + } + + self._lock_implicit_alias(); + + for (table, join_query) in tmp { + table.alias.config.write().unwrap()._lock_implicit_alias(); + + let alias = table.alias.get(); + result.insert(alias, Arc::new(Join::new(table, join_query))); + } + + Ok(result) + } +} + +#[derive(Clone, Debug)] +pub struct TableAlias { + config: Arc>, +} + +impl TableAlias { + pub fn new(table_name: &str) -> Self { + TableAlias { + config: Arc::new(RwLock::new(TableAliasConfig::new(table_name))), + } + } + pub fn enforce_table_in_field_queries(&self) { + self.config + .write() + .unwrap() + .enforce_table_in_field_queries(); + } + pub fn try_get(&self) -> Option { + self.config.read().unwrap().try_get() + } + pub fn try_get_for_from(&self) -> Option { + self.config.read().unwrap().try_get_for_from() + } + pub fn get(&self) -> String { + self.config.read().unwrap().get() + } + pub fn alias_is_some(&self) -> bool { + self.config.read().unwrap().alias_is_some() + } + pub fn set(&self, alias: &str) { + self.config.write().unwrap().set(alias); + } + pub fn set_short_alias(&self) { + self.config.write().unwrap().set_short_alias(); + } + pub fn disable_alias(&self) { + self.config.write().unwrap().desired_alias = DesiredAlias::None; + } + pub fn deep_clone(&self) -> Self { + Self { + config: Arc::new(RwLock::new(self.config.read().unwrap().deep_clone())), + } + } + /// Returns true if both table alias records have same vendor ID + /// which effectively mean the tables are joined + pub fn is_same_id_vendor(&self, other: &Self) -> bool { + Arc::ptr_eq( + &self.config.read().unwrap().alias_vendor, + &other.config.read().unwrap().alias_vendor, + ) + } + + pub fn _reassign_alias( + &self, + our_old_joins: IndexMap>>, + their_old_joins: IndexMap>>, + ) -> Result>>> { + self.config + .write() + .unwrap() + ._reassign_alias(our_old_joins, their_old_joins) + } +} + +#[cfg(test)] +mod tests { + use std::{os, sync::Arc}; + + use crate::{ + expr_arc, + prelude::{ExpressionArc, JoinQuery, PgValueColumn, SqlTable, TableWithQueries}, + sql::{ + query::{ConditionType, JoinType, QueryConditions, QuerySource}, + Chunk, Join, + }, + }; + use indexmap::IndexMap; + use serde_json::json; + + use crate::{mocks::MockDataSource, prelude::AnyTable, sql::Table}; + + #[test] + fn test_table_cloning() { + let data = json!([]); + let data_source = MockDataSource::new(&data); + let table = Table::new("users", data_source.clone()).with_column("name"); + + let table2 = table.clone(); + + assert_eq!( + table + .alias + .config + .read() + .unwrap() + .specify_table_for_field_queries, + false + ); + assert_eq!( + table2 + .alias + .config + .read() + .unwrap() + .specify_table_for_field_queries, + false + ); + + table.alias.enforce_table_in_field_queries(); + + assert_eq!( + table + .alias + .config + .read() + .unwrap() + .specify_table_for_field_queries, + true + ); + assert_eq!( + table2 + .alias + .config + .read() + .unwrap() + .specify_table_for_field_queries, + false + ); + } + + #[test] + fn test_reassign_alias() { + let data = json!([]); + let data_source = MockDataSource::new(&data); + let table = Table::new("users", data_source.clone()).with_column("name"); + + let table1 = table.clone(); + let mut table2 = table.clone(); + let table3 = table.clone(); + let table4 = table.clone(); + + // leave table1 as-is + table2.set_alias("uzzah"); + table3.alias.disable_alias(); + // leave table4 as-is + + let some_join_query = JoinQuery::new( + JoinType::Inner, + QuerySource::Table("user".to_string(), None), + QueryConditions::on(), + ); + + let mut i1 = IndexMap::new(); + i1.insert( + "a".to_string(), + Arc::new(Join::new(table1, some_join_query.clone())), + ); + i1.insert( + "b".to_string(), + Arc::new(Join::new(table2, some_join_query.clone())), + ); + + let mut i2 = IndexMap::new(); + i2.insert( + "c".to_string(), + Arc::new(Join::new(table3, some_join_query.clone())), + ); + i2.insert( + "d".to_string(), + Arc::new(Join::new(table4, some_join_query.clone())), + ); + + // after merging all tables, we should have u_1, uzzah, users, u_2 + let result = table.alias._reassign_alias(i1, i2).unwrap(); + + // Resulting join uses `user` table 5 times. + // + // table is as-is and is assetred above, aliased as `u` + // + // table1 was as-is, but `u` is taken, so aliased into `us` + // table2.set_alias("uzzah"); + // table3.alias.disable_alias(), so `users` is used + // leave table4 as-is, so `use` is the alias + assert_eq!(&table.alias.config.read().unwrap().get(), "u"); + assert_eq!( + result + .iter() + .map(|(k, t)| (k, t.table_name.clone())) + .collect::>(), + vec![ + (&"us".to_string(), "users".to_string()), + (&"uzzah".to_string(), "users".to_string()), + (&"users".to_string(), "users".to_string()), + (&"use".to_string(), "users".to_string()) + ] + ); + } + + #[test] + fn test_try_and_get_alias() { + let data = json!([]); + let data_source = MockDataSource::new(&data); + let table = Table::new("users", data_source.clone()) + .with_column(PgValueColumn::new("name").with_quotes()); + + // render field regularly + let f1 = table.get_column("name").unwrap().with_quotes(); + assert_eq!(f1.render_chunk().preview(), "\"name\""); + + // get field with table alias + let f2 = table.get_column("name").unwrap().with_table_alias(); + assert_eq!(f2.render_chunk().preview(), "\"users\".\"name\""); + + // next change table alias and make sure existing fields are affected + table.alias.set("u"); + assert_eq!(expr_arc!("{}", f1.render_chunk()).preview(), "\"name\""); + assert_eq!( + expr_arc!("{}", f2.render_chunk()).preview(), + "\"u\".\"name\"" + ); + + // setting enforce will prefix all fields with table name or table alias + table.alias.enforce_table_in_field_queries(); + assert_eq!( + expr_arc!("{}", f1.render_chunk()).preview(), + "\"u\".\"name\"" + ); + assert_eq!( + expr_arc!("{}", f2.render_chunk()).preview(), + "\"u\".\"name\"" + ); + } + + #[test] + fn test_table_joining() { + let data = json!([]); + let data_source = MockDataSource::new(&data); + let mut person = Table::new("person", data_source.clone()).with_column("name"); + let mut father = person.clone(); + + let person_name = person.get_column("name").unwrap(); + let father_name = father.get_column("name").unwrap(); + + // Tables are unrelated, so both names render without alias + assert_eq!(person_name.render_chunk().preview(), "name"); + assert_eq!(father_name.render_chunk().preview(), "name"); + + // Linking father to a person, will enforce table prefixing for both but since + // the table name is identical, a unique alias will be generated + person.link(&mut father); + dbg!(&person.alias); + return; + assert_eq!(person_name.render_chunk().preview(), "person.name"); + assert_eq!( + father_name.render_chunk().preview(), + "\"person_2\".\"name\"" + ); + + father.alias.set("par"); + assert_eq!(person_name.render_chunk().preview(), "person.name"); + assert_eq!(father_name.render_chunk().preview(), "par.name"); + + let mut mother = Table::new("person", data_source.clone()) + .with_column("name") + .with_alias("par"); + let mother_name = mother.get_column("name").unwrap(); + person.link(&mut mother); + + assert_eq!(person_name.render_chunk().preview(), "person.name"); + assert_eq!(father_name.render_chunk().preview(), "par.name"); + assert_eq!(mother_name.render_chunk().preview(), "par_2.name"); + + // now lets add grandparents + let mut gr1 = Table::new("person", data_source.clone()).with_column("name"); + let gr1_name = gr1.get_column("name").unwrap(); + + let mut gr2 = Table::new("person", data_source.clone()).with_column("name"); + let gr2_name = gr2.get_column("name").unwrap(); + + assert_eq!(gr1_name.render_chunk().preview(), "person.name"); + + father.link(&mut gr1); + mother.link(&mut gr2); + + assert_eq!(person_name.render_chunk().preview(), "person.name"); + assert_eq!(father_name.render_chunk().preview(), "par.name"); + assert_eq!(mother_name.render_chunk().preview(), "par_2.name"); + assert_eq!(gr1_name.render_chunk().preview(), "person_2.name"); + assert_eq!(gr2_name.render_chunk().preview(), "person_3.name"); + } + + #[test] + fn test_table_alias() { + let data = json!([]); + let data_source = MockDataSource::new(&data); + let mut users = Table::new("users", data_source.clone()).with_column("name"); + let mut roles = Table::new("users", data_source.clone()).with_column("name"); + + assert_eq!( + &users + .field_query(users.get_column("name").unwrap()) + .preview(), + "SELECT name FROM users" + ); + + users.alias.enforce_table_in_field_queries(); + assert_eq!( + &users + .field_query(users.get_column("name").unwrap()) + .preview(), + "SELECT users.name FROM users" + ); + + users.link(&mut roles); + assert_eq!( + &users + .field_query(users.get_column("name").unwrap()) + .preview(), + "SELECT u.name FROM users AS u" + ); + } +} diff --git a/vantage/src/sql/table/column.rs b/vantage/src/sql/table/column.rs index b34c4d5..5461fc7 100644 --- a/vantage/src/sql/table/column.rs +++ b/vantage/src/sql/table/column.rs @@ -1,157 +1,6 @@ -use std::sync::Arc; - -use crate::expr; -use crate::sql::chunk::Chunk; -use crate::sql::Condition; -use crate::sql::Expression; -use crate::sql::Operations; -use crate::sql::WrapArc; -use crate::traits::column::SqlField; - mod chrono; mod sqlcolumn; pub use sqlcolumn::SqlColumn; -#[derive(Debug, Clone)] -pub struct Column { - name: String, - table_alias: Option, - column_alias: Option, -} - -impl Column { - pub fn new(name: String, table_alias: Option) -> Column { - Column { - name, - table_alias, - column_alias: None, - } - } -} - -impl SqlColumn for Column { - fn name(&self) -> String { - self.name.clone() - } - fn name_with_table(&self) -> String { - match &self.table_alias { - Some(table_alias) => format!("{}.{}", table_alias, self.name), - None => self.name.clone(), - } - } - fn set_name(&mut self, name: String) { - self.name = name; - } - fn set_table_alias(&mut self, alias: String) { - self.table_alias = Some(alias); - } - fn set_column_alias(&mut self, alias: String) { - self.column_alias = Some(alias); - } - - fn get_column_alias(&self) -> Option { - self.column_alias.clone() - } -} - -impl Chunk for Column { - fn render_chunk(&self) -> Expression { - Arc::new(self.clone()).render_chunk() - } -} -impl Operations for Column {} - -impl Operations for Arc { - fn eq(&self, other: &impl Chunk) -> Condition { - Condition::from_field(self.clone(), "=", WrapArc::wrap_arc(other.render_chunk())) - } - - // fn add(&self, other: impl SqlChunk) -> Expression { - // let chunk = other.render_chunk(); - // expr_arc!(format!("{} + {{}}", &self.name), chunk).render_chunk() - // } -} - -impl Chunk for Arc { - fn render_chunk(&self) -> Expression { - Expression::new(format!("{}", self.name_with_table()), vec![]) - } -} - -impl SqlField for Arc { - fn render_column(&self, mut alias: Option<&str>) -> Expression { - // If the alias is the same as the field name, we don't need to render it - if alias.is_some() && alias.unwrap() == self.name { - alias = None; - } - - let alias = alias.or(self.column_alias.as_deref()); - - if let Some(alias) = alias { - expr!(format!("{} AS {}", self.name_with_table(), alias)) - } else { - expr!(format!("{}", self.name_with_table())) - } - .render_chunk() - } - fn calculated(&self) -> bool { - false - } -} - -impl From for Column { - fn from(name: String) -> Self { - Column { - name, - table_alias: None, - column_alias: None, - } - } -} - -impl From<&str> for Column { - fn from(name: &str) -> Self { - name.to_string().into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_field() { - let field = Arc::new(Column::new("id".to_string(), None)); - let (sql, params) = field.render_chunk().split(); - - assert_eq!(sql, "id"); - assert_eq!(params.len(), 0); - - let (sql, params) = field.render_column(Some("id")).render_chunk().split(); - assert_eq!(sql, "id"); - assert_eq!(params.len(), 0); - - let (sql, params) = &field.render_column(Some("id_alias")).render_chunk().split(); - assert_eq!(sql, "id AS id_alias"); - assert_eq!(params.len(), 0); - } - - #[test] - fn test_eq() { - let field = Arc::new(Column::new("id".to_string(), None)); - let (sql, params) = field.eq(&1).render_chunk().split(); - - assert_eq!(sql, "(id = {})"); - assert_eq!(params.len(), 1); - assert_eq!(params[0], 1); - - let f_age = Arc::new(Column::new("age".to_string(), Some("u".to_string()))); - let (sql, params) = f_age.add(5).eq(&18).render_chunk().split(); - - assert_eq!(sql, "((u.age) + ({}) = {})"); - assert_eq!(params.len(), 2); - assert_eq!(params[0], 5); - assert_eq!(params[1], 18); - } -} +pub type Column = Box; diff --git a/vantage/src/sql/table/column/sqlcolumn.rs b/vantage/src/sql/table/column/sqlcolumn.rs index c4648fa..2917965 100644 --- a/vantage/src/sql/table/column/sqlcolumn.rs +++ b/vantage/src/sql/table/column/sqlcolumn.rs @@ -1,10 +1,11 @@ -use crate::sql::Chunk; +use crate::{prelude::TableAlias, sql::Chunk}; pub trait SqlColumn: Chunk { fn name(&self) -> String; fn name_with_table(&self) -> String; + fn get_table_alias(&self) -> &Option; fn set_name(&mut self, name: String); - fn set_table_alias(&mut self, alias: String); - fn set_column_alias(&mut self, alias: String); - fn get_column_alias(&self) -> Option; + fn set_table_alias(&mut self, alias: &TableAlias); + fn set_alias(&mut self, alias: String); + fn get_alias(&self) -> Option; } diff --git a/vantage/src/sql/table/extensions/soft_delete.rs b/vantage/src/sql/table/extensions/soft_delete.rs index 7c57746..00d3d2e 100644 --- a/vantage/src/sql/table/extensions/soft_delete.rs +++ b/vantage/src/sql/table/extensions/soft_delete.rs @@ -4,8 +4,8 @@ use anyhow::Result; use serde_json::json; use crate::{ - prelude::SqlTable, - sql::{query::SqlQuery, Chunk, Column, Operations, Query}, + prelude::{PgValueColumn, SqlTable}, + sql::{query::SqlQuery, Chunk, Operations, Query}, }; use super::TableExtension; @@ -21,7 +21,7 @@ impl SoftDelete { soft_delete_field: soft_delete_field.to_string(), } } - fn is_deleted(&self, table: &dyn SqlTable) -> Arc { + fn is_deleted(&self, table: &dyn SqlTable) -> Arc { table.get_column(&self.soft_delete_field).unwrap() } } @@ -30,7 +30,7 @@ impl TableExtension for SoftDelete { fn init(&self, table: &mut dyn SqlTable) { table.add_column( self.soft_delete_field.clone(), - Column::new(self.soft_delete_field.clone(), None), + PgValueColumn::new(&self.soft_delete_field), ); } diff --git a/vantage/src/sql/table/join.rs b/vantage/src/sql/table/join.rs index 2a87af2..54e4ff9 100644 --- a/vantage/src/sql/table/join.rs +++ b/vantage/src/sql/table/join.rs @@ -44,9 +44,6 @@ impl Join { Join { table, join_query } } - pub fn alias(&self) -> &str { - self.table.get_alias().unwrap() - } pub fn join_query(&self) -> &JoinQuery { &self.join_query } @@ -56,6 +53,9 @@ impl Join { pub fn table_mut(&mut self) -> &mut Table { &mut self.table } + pub fn split(&self) -> (Table, JoinQuery) { + (self.table.clone(), self.join_query.clone()) + } } impl Deref for Join { diff --git a/vantage/src/sql/table/reference/many.rs b/vantage/src/sql/table/reference/many.rs index f8ac7f4..f84d636 100644 --- a/vantage/src/sql/table/reference/many.rs +++ b/vantage/src/sql/table/reference/many.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use super::{RelatedSqlTable, RelatedTableFx}; -use crate::{prelude::SqlTable, sql::Operations}; +use crate::{ + prelude::SqlTable, + sql::{Chunk, Operations}, +}; #[derive(Clone)] pub struct ReferenceMany { @@ -41,9 +44,11 @@ impl RelatedSqlTable for ReferenceMany { fn get_linked_set(&self, table: &dyn SqlTable) -> Box { let mut target = (self.get_table)(); let target_field = target - .get_column_with_table_alias(&self.target_foreign_key) - .unwrap(); - target.add_condition(target_field.eq(&table.id_with_table_alias())); + .get_column(&self.target_foreign_key) + .unwrap() + .with_table_alias(); + dbg!(&table.id().with_table_alias().render_chunk().preview()); + target.add_condition(target_field.eq(&table.id().with_table_alias())); target } } diff --git a/vantage/src/sql/table/reference/one.rs b/vantage/src/sql/table/reference/one.rs index 3300173..5d67d45 100644 --- a/vantage/src/sql/table/reference/one.rs +++ b/vantage/src/sql/table/reference/one.rs @@ -42,11 +42,12 @@ impl RelatedSqlTable for ReferenceOne { fn get_linked_set(&self, table: &dyn SqlTable) -> Box { let mut target = (self.get_table)(); - let target_field = target.id_with_table_alias(); + let target_field = target.id().with_table_alias(); target.add_condition( target_field.eq(&table - .get_column_with_table_alias(self.our_foreign_key.as_str()) - .unwrap()), + .get_column(self.our_foreign_key.as_str()) + .unwrap() + .with_table_alias()), ); target } @@ -61,7 +62,7 @@ mod tests { use crate::sql::Table; #[test] - fn test_related_reference() { + fn test_related_one_reference() { let data = json!([{ "name": "John", "surname": "Doe"}, { "name": "Jane", "surname": "Doe"}]); let data_source = MockDataSource::new(&data); diff --git a/vantage/src/sql/table/with_columns.rs b/vantage/src/sql/table/with_columns.rs index 2d8f20a..0e6fad1 100644 --- a/vantage/src/sql/table/with_columns.rs +++ b/vantage/src/sql/table/with_columns.rs @@ -1,14 +1,15 @@ use anyhow::{anyhow, Context}; use indexmap::IndexMap; use serde_json::Value; -use std::ops::Deref; use std::sync::Arc; use super::column::SqlColumn; -use super::{Column, RelatedTable}; +use super::{PgValueColumn, SqlTable}; +use crate::expr; use crate::lazy_expression::LazyExpression; use crate::prelude::Operations; use crate::sql::table::Table; +use crate::sql::Expression; use crate::traits::column::SqlField; use crate::traits::datasource::DataSource; use crate::traits::entity::Entity; @@ -113,11 +114,11 @@ use super::AnyTable; /// [`columns()`]: Table::columns() pub trait TableWithColumns: AnyTable { - fn add_column(&mut self, column_name: String, column: Column); - fn columns(&self) -> &IndexMap>; - fn get_column_with_table_alias(&self, name: &str) -> Option>; - fn id(&self) -> Arc; - fn id_with_table_alias(&self) -> Arc; + fn add_column(&mut self, column_name: String, column: PgValueColumn); + fn columns(&self) -> &IndexMap>; + // fn get_column_with_table_alias(&self, name: &str) -> Option>; + fn id(&self) -> Arc; + // fn id_with_table_alias(&self) -> Arc; fn search_for_field(&self, field_name: &str) -> Option>; } @@ -127,28 +128,26 @@ impl TableWithColumns for Table { /// Adds a new column to the table. Note, that Column may use an alias. Additional /// features may be added into [`Column`] in the future, so better use [`with_column()`] /// to keep your code portable. - fn add_column(&mut self, column_name: String, column: Column) { + fn add_column(&mut self, column_name: String, mut column: PgValueColumn) { + column.set_table_alias(&self.alias); self.columns.insert(column_name, Arc::new(column)); } /// Return all columns. See also: [`Table::get_column`]. - fn columns(&self) -> &IndexMap> { + fn columns(&self) -> &IndexMap> { &self.columns } - fn get_column_with_table_alias(&self, name: &str) -> Option> { - let mut f = self.get_column(name)?.deref().clone(); - f.set_table_alias( - self.get_alias() - .unwrap_or_else(|| self.get_table_name().unwrap()) - .clone(), - ); - Some(Arc::new(f)) - } + // fn get_column_with_table_alias(&self, name: &str) -> Option> { + // Some(Arc::new(self.get_column(name)?.with_table_alias())) + // } + // fn id_with_table_alias(&self) -> Arc { + // Arc::new(self.id().with_table_alias()) + // } /// Returns the id column. If `with_id_column` was not called, will try to find /// column called `"id"`. If not found, will panic. - fn id(&self) -> Arc { + fn id(&self) -> Arc { let id_column = if self.id_column.is_some() { let x = self.id_column.clone().unwrap(); x.clone() @@ -160,15 +159,15 @@ impl TableWithColumns for Table { .unwrap() } - fn id_with_table_alias(&self) -> Arc { - let id_column = if self.id_column.is_some() { - let x = self.id_column.clone().unwrap(); - x.clone() - } else { - "id".to_string() - }; - self.get_column_with_table_alias(&id_column).unwrap() - } + // fn id_with_table_alias(&self) -> Arc { + // let id_column = if self.id_column.is_some() { + // let x = self.id_column.clone().unwrap(); + // x.clone() + // } else { + // "id".to_string() + // }; + // self.get_column_with_table_alias(&id_column).unwrap() + // } /// In addition to `self.columns` the columns can also be defined for a joined /// table. (See [`Table::with_join()`]) or through a lazy expression (See @@ -207,11 +206,11 @@ impl TableWithColumns for Table { impl Table { /// When building a table - a way to chain column declarations. - pub fn with_column(mut self, column: impl Into) -> Self { - let mut column: Column = column.into(); - if self.table_alias.is_some() { - column.set_table_alias(self.table_alias.clone().unwrap()); - } + pub fn with_column(mut self, column: impl Into) -> Self { + let column: PgValueColumn = column.into(); + // if self.table_alias.is_some() { + // column.set_table_alias(self.table_alias.clone().unwrap()); + // } self.add_column(column.name(), column); self } @@ -263,7 +262,7 @@ mod tests { let db = MockDataSource::new(&data); let roles = Table::new("roles", db.clone()) - .with_column(Column::new("name".to_string(), Some("id".to_string()))); + .with_column(PgValueColumn::new("name").with_alias("id")); assert!(roles.get_column("qq").is_none()); assert!(roles.get_column("name").is_some()); diff --git a/vantage/src/sql/table/with_joins.rs b/vantage/src/sql/table/with_joins.rs index f3d2511..7616083 100644 --- a/vantage/src/sql/table/with_joins.rs +++ b/vantage/src/sql/table/with_joins.rs @@ -1,9 +1,11 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Context}; +use std::borrow::BorrowMut; +use std::mem::take; use std::ptr::eq; use std::sync::Arc; -use super::{Join, TableWithColumns}; -use crate::prelude::Chunk; +use super::{Join, SqlTable, TableWithColumns}; +use crate::prelude::{Chunk, EmptyEntity}; use crate::sql::query::{JoinQuery, JoinType, QueryConditions}; use crate::sql::table::Table; use crate::sql::Operations; @@ -186,9 +188,24 @@ impl Table { self.into_entity::() } + /// Given 2 tables, where each may be having some kind of joins - this will merge + /// all the joins into self, emptying their_table.joins. Also re-assigns aliases + /// for all the tables involved. + pub fn link(&mut self, their_table: &mut Table) { + let i1 = take(&mut self.joins); + let i2 = take(&mut their_table.joins); + + self.alias.enforce_table_in_field_queries(); + their_table.alias.enforce_table_in_field_queries(); + + self.joins = self.alias._reassign_alias(i1, i2).unwrap(); + + their_table.alias.set_short_alias(); + } + pub fn add_join( &mut self, - mut their_table: Table, + their_table: Table, our_foreign_id: &str, ) -> Arc> { //! Combine two tables with 1 to 1 relationship into a single table. @@ -197,58 +214,28 @@ impl Table { //! but we still have to specify foreign key in our own table. For more complex //! joins use `join_table` method. //! before joining, make sure there are no alias clashes - if eq(&*self.table_aliases, &*their_table.table_aliases) { - panic!( - "Tables are already joined: {}, {}", - self.table_name, their_table.table_name - ) - } - if their_table - .table_aliases - .lock() - .unwrap() - .has_conflict(&self.table_aliases.lock().unwrap()) - { + if self.alias.is_same_id_vendor(&their_table.alias) { panic!( - "Table alias conflict while joining: {}, {}", + "Tables are already joined: {}, {}", self.table_name, their_table.table_name ) } - self.table_aliases - .lock() - .unwrap() - .merge(their_table.table_aliases.lock().unwrap().to_owned()); + let mut their_table: Table = their_table.into_entity(); // Get information about their_table let their_table_name = their_table.table_name.clone(); - if their_table.table_alias.is_none() { - let their_table_alias = self - .table_aliases - .lock() - .unwrap() - .get_one_of_uniq_id(UniqueIdVendor::all_prefixes(&their_table_name)); - their_table.set_alias(&their_table_alias); - }; - let their_table_id = their_table.id(); - - // Give alias to our table as well - if self.table_alias.is_none() { - let our_table_alias = self - .table_aliases - .lock() - .unwrap() - .get_one_of_uniq_id(UniqueIdVendor::all_prefixes(&self.table_name)); - self.set_alias(&our_table_alias); - } - let their_table_alias = their_table.table_alias.as_ref().unwrap().clone(); + let their_table_id = their_table.id().with_table_alias(); + + self.link(&mut their_table); let mut on_condition = QueryConditions::on(); on_condition.add_condition( self.get_column(our_foreign_id) .ok_or_else(|| anyhow!("Table '{}' has no field '{}'", &self, &our_foreign_id)) .unwrap() + .with_table_alias() .eq(&their_table_id) .render_chunk(), ); @@ -262,18 +249,21 @@ impl Table { // Create a join let join = JoinQuery::new( JoinType::Left, - crate::sql::query::QuerySource::Table( + crate::sql::query::QuerySource::TableWithAlias( their_table_name, - Some(their_table_alias.clone()), + Arc::new(their_table.alias.clone()), ), on_condition, ); + let their_table_alias = their_table.alias.get(); self.joins.insert( - their_table_alias.clone(), - Arc::new(Join::new(their_table.into_entity(), join)), + their_table.alias.get(), + Arc::new(Join::new(their_table, join)), ); - self.get_join(&their_table_alias).unwrap() + self.get_join(&their_table_alias) + .context(anyhow!("Join canot be re-fetched")) + .unwrap() } } @@ -435,7 +425,7 @@ mod tests { query.0, "SELECT u.name, u.role_id, r.id AS r_id, r.role_type AS r_role_type FROM users AS u \ LEFT JOIN roles AS r ON (u.role_id = r.id) AND \ - ((r.role_type = {}) OR (role_type = {}))" + ((r.role_type = {}) OR (r.role_type = {}))" ); assert_eq!(query.1[0], json!("admin")); } @@ -452,4 +442,32 @@ mod tests { // will panic, both tables want "u" alias user_table.with_join::(role_table, "role_id"); } + + #[test] + fn test_table_joins() { + let data = json!([]); + let data_source = MockDataSource::new(&data); + let mut users = Table::new("users", data_source.clone()) + .with_column("name") + .with_column("role_id"); + let roles = Table::new("roles", data_source.clone()) + .with_id_column("id") + .with_column("name"); + + assert_eq!( + &users + .field_query(users.get_column("name").unwrap()) + .preview(), + "SELECT name FROM users" + ); + + users.add_join(roles, "role_id"); + + assert_eq!( + &users + .field_query(users.get_column("name").unwrap()) + .preview(), + "SELECT u.name FROM users AS u LEFT JOIN roles AS r ON (u.role_id = r.id)" + ); + } } diff --git a/vantage/src/sql/table/with_queries.rs b/vantage/src/sql/table/with_queries.rs index 76dc918..7239ede 100644 --- a/vantage/src/sql/table/with_queries.rs +++ b/vantage/src/sql/table/with_queries.rs @@ -4,7 +4,7 @@ use serde_json::{to_value, Value}; use std::sync::Arc; use super::column::SqlColumn; -use super::{AnyTable, Column, TableWithColumns}; +use super::{AnyTable, PgValueColumn, SqlTable, TableWithColumns}; use crate::prelude::AssociatedQuery; use crate::sql::query::{QueryType, SqlQuery}; use crate::sql::table::Table; @@ -28,7 +28,8 @@ pub trait TableWithQueries: AnyTable { impl TableWithQueries for Table { fn get_empty_query(&self) -> Query { - let mut query = Query::new().with_table(&self.table_name, self.table_alias.clone()); + let mut query = + Query::new().with_table(&self.table_name, self.get_alias().try_get_for_from()); for condition in self.conditions.iter() { query = query.with_condition(condition.clone()); } @@ -75,7 +76,7 @@ impl TableWithQueries for Table { } impl Table { - pub fn field_query(&self, field: Arc) -> AssociatedQuery { + pub fn field_query(&self, field: Arc) -> AssociatedQuery { // let query = self.get_select_query_for_field(field); let query = self.get_empty_query().with_field(field.name(), field); AssociatedQuery::new(query, self.data_source.clone()) @@ -140,7 +141,7 @@ impl Table { }; for (field, _) in &self.columns { - let field_object = Arc::new(Column::new(field.clone(), self.table_alias.clone())); + let field_object = Arc::new(PgValueColumn::new(&field)); if field_object.calculated() { continue; @@ -168,7 +169,7 @@ impl Table { }; for (field, _) in &self.columns { - let field_object = Arc::new(Column::new(field.clone(), self.table_alias.clone())); + let field_object = Arc::new(PgValueColumn::new(&field.clone())); if field_object.calculated() { continue; diff --git a/vantage/src/uniqid.rs b/vantage/src/uniqid.rs index 614d7a3..96f15ac 100644 --- a/vantage/src/uniqid.rs +++ b/vantage/src/uniqid.rs @@ -1,17 +1,19 @@ use std::collections::HashSet; +use anyhow::{anyhow, Result}; +use env_logger::fmt::style::Reset; use indexmap::IndexMap; #[derive(Debug, Clone)] pub struct UniqueIdVendor { - map: IndexMap, + // map: IndexMap, avoid: HashSet, } impl UniqueIdVendor { pub fn new() -> UniqueIdVendor { UniqueIdVendor { - map: IndexMap::new(), + // map: IndexMap::new(), avoid: HashSet::new(), } } @@ -20,32 +22,54 @@ impl UniqueIdVendor { pub fn get_uniq_id(&mut self, desired_name: &str) -> String { let mut name = desired_name.to_string(); let mut i = 2; - while self.avoid.contains(&name) || self.map.contains_key(&name) { + while self.avoid.contains(&name) { name = format!("{}_{}", desired_name, i); i += 1; } - self.map.insert(name.clone(), name.clone()); + self.avoid(&name).unwrap(); name } - pub fn avoid(&mut self, name: &str) { + // Shortens name to a single letter, or more letters if necessary + pub fn get_short_uniq_id(&mut self, desired_name: &str) -> String { + let mut variants = UniqueIdVendor::all_prefixes(desired_name); + variants.push(desired_name); + + self.get_one_of_uniq_id(variants) + } + + pub fn avoid(&mut self, name: &str) -> Result<()> { + if self.avoid.contains(name) { + return Err(anyhow!( + "avoid: {} is already reserved by someone else", + name + )); + } self.avoid.insert(name.to_string()); + Ok(()) } - pub fn dont_avoid(&mut self, name: &str) { + pub fn dont_avoid(&mut self, name: &str) -> Result<()> { + if !self.avoid.contains(name) { + return Err(anyhow!( + "Unable to remove {} from avoid list - it's not there", + name + )); + } self.avoid.remove(name); + Ok(()) } // Provided desired names ("n", "na", "nam") find available one // If none are available, will add _2, _3 to last option. - pub fn get_one_of_uniq_id(&mut self, desired_names: Vec<&str>) -> String { + fn get_one_of_uniq_id(&mut self, desired_names: Vec<&str>) -> String { for name in &desired_names { if self.avoid.contains(&name.to_string()) { continue; } - if !self.map.contains_key(*name) { - self.map.insert(name.to_string(), name.to_string()); + if !self.avoid.contains(*name) { + self.avoid.insert(name.to_string()); return name.to_string(); } } @@ -54,7 +78,7 @@ impl UniqueIdVendor { self.get_uniq_id(last_option) } - pub fn all_prefixes(name: &str) -> Vec<&str> { + fn all_prefixes(name: &str) -> Vec<&str> { (1..name.len()).into_iter().map(|i| &name[..i]).collect() } @@ -62,14 +86,7 @@ impl UniqueIdVendor { pub fn has_conflict(&self, other: &UniqueIdVendor) -> bool { // Check if any key in self.avoid is in other.avoid or other.map for key in &self.avoid { - if other.avoid.contains(key) || other.map.contains_key(key) { - return true; - } - } - - // Check if any key in self.map is in other.avoid or other.map - for key in self.map.keys() { - if other.avoid.contains(key) || other.map.contains_key(key) { + if other.avoid.contains(key) { return true; } } @@ -77,12 +94,9 @@ impl UniqueIdVendor { false } - pub fn merge(&mut self, other: UniqueIdVendor) { - for (key, value) in other.map { - self.map.insert(key, value); - } - for key in other.avoid { - self.avoid.insert(key); + pub fn merge(&mut self, other: &mut UniqueIdVendor) { + for key in &other.avoid { + self.avoid.insert(key.clone()); } } } @@ -96,32 +110,36 @@ mod conflict_tests { fn test_has_conflict() { let mut vendor1 = UniqueIdVendor::new(); let mut vendor2 = UniqueIdVendor::new(); + let mut vendor3 = UniqueIdVendor::new(); - vendor1.avoid("conflict"); - vendor2 - .map - .insert("conflict".to_string(), "value".to_string()); + vendor1.avoid("conflict").unwrap(); + vendor2.avoid("conflict").unwrap(); + vendor3.get_uniq_id("conflict"); assert!(vendor1.has_conflict(&vendor2)); + assert!(vendor1.has_conflict(&vendor3)); } #[test] fn test_no_conflict() { let mut vendor1 = UniqueIdVendor::new(); let mut vendor2 = UniqueIdVendor::new(); + let mut vendor3 = UniqueIdVendor::new(); - vendor1.avoid("unique1"); - vendor2 - .map - .insert("unique2".to_string(), "value".to_string()); + vendor1.avoid("unique1").unwrap(); + vendor2.avoid("unique2").unwrap(); + vendor3.get_uniq_id("unique3"); assert!(!vendor1.has_conflict(&vendor2)); + assert!(!vendor1.has_conflict(&vendor3)); } -} -#[cfg(test)] -mod tests { - use super::*; + #[test] + fn test_double_avoid() { + let mut vendor = UniqueIdVendor::new(); + vendor.avoid("name").unwrap(); + assert!(vendor.avoid("name").is_err()); + } #[test] fn test_unique_id() { @@ -141,7 +159,7 @@ mod tests { #[test] fn test_avoid() { let mut vendor = UniqueIdVendor::new(); - vendor.avoid("name"); + vendor.avoid("name").unwrap(); assert_eq!(vendor.get_uniq_id("name"), "name_2"); } @@ -149,7 +167,7 @@ mod tests { #[test] fn test_one_of_uniq_id() { let mut vendor = UniqueIdVendor::new(); - vendor.avoid("nam"); + vendor.avoid("nam").unwrap(); assert_eq!( vendor.get_one_of_uniq_id(UniqueIdVendor::all_prefixes("name")), @@ -159,6 +177,11 @@ mod tests { vendor.get_one_of_uniq_id(UniqueIdVendor::all_prefixes("name")), "na" ); + // avoided! + // assert_eq!( + // vendor.get_one_of_uniq_id(UniqueIdVendor::all_prefixes("name")), + // "nam" + // ); assert_eq!( vendor.get_one_of_uniq_id(UniqueIdVendor::all_prefixes("name")), "nam_2" @@ -168,4 +191,15 @@ mod tests { "nam_3" ); } + + #[test] + fn test_short_uniq_id() { + let mut vendor = UniqueIdVendor::new(); + + assert_eq!(vendor.get_short_uniq_id("name"), "n"); + assert_eq!(vendor.get_short_uniq_id("name"), "na"); + assert_eq!(vendor.get_short_uniq_id("name"), "nam"); + assert_eq!(vendor.get_short_uniq_id("name"), "name"); + assert_eq!(vendor.get_short_uniq_id("name"), "name_2"); + } } diff --git a/vantage/tests/types_test.rs b/vantage/tests/types_test.rs index 58d2e4c..f2e573f 100644 --- a/vantage/tests/types_test.rs +++ b/vantage/tests/types_test.rs @@ -55,6 +55,7 @@ struct TestStruct { } impl Entity for TestStruct {} +#[ignore] #[tokio::test] async fn my_test() -> Result<()> { let p = postgres();