From 7a8856df1004f4d3fc25ccefcb19f4ff18a58de9 Mon Sep 17 00:00:00 2001 From: Victor Mignot Date: Mon, 6 Jan 2025 22:21:55 +0100 Subject: [PATCH] WIP: Introduce the zou library --- Cargo.lock | 4 + Cargo.toml | 1 + zou/Cargo.toml | 14 + zou/LICENSE | 11 + zou/src/lib.rs | 775 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 805 insertions(+) create mode 100644 zou/Cargo.toml create mode 100644 zou/LICENSE create mode 100644 zou/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 52910ad..5bd6ad6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2663,3 +2663,7 @@ dependencies = [ "quote", "syn 2.0.95", ] + +[[package]] +name = "zou" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c59101e..070b2e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ resolver = "2" members = [ "yorokobot", + "zou" ] diff --git a/zou/Cargo.toml b/zou/Cargo.toml new file mode 100644 index 0000000..49e8a3b --- /dev/null +++ b/zou/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zou" +version = "0.1.0" +authors = [ "Victor Mignot " ] +license = "BSD-3-Clause" +repository = "https://git.dalaran.fr/dala/yorokobot" +homepage = "https://git.dalaran.fr/dala/yorokobot/src/branch/main/zou" +edition = "2021" + +[dependencies] + +[lints.rust] +unsafe_code= "forbid" +missing_docs = "deny" diff --git a/zou/LICENSE b/zou/LICENSE new file mode 100644 index 0000000..ef538da --- /dev/null +++ b/zou/LICENSE @@ -0,0 +1,11 @@ +Copyright 2025 Victor Mignot + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/zou/src/lib.rs b/zou/src/lib.rs new file mode 100644 index 0000000..a69b6ed --- /dev/null +++ b/zou/src/lib.rs @@ -0,0 +1,775 @@ +//! zou is a lightweight ORM for Postgres database. +//! zou is licensed under the BSD 3-Clause License. + +// WARN: Delete this +#![warn(missing_docs)] + +/// Trait to be implemented for structs to be stored in your postgres Database. +pub trait ObjectModel: Sized { + /// The name of the table to store this the implementing struct. + fn table_name() -> &'static str; +} + +enum QueryOperation { + Select, +} + +impl QueryOperation { + fn format(&self) -> &'static str { + match self { + QueryOperation::Select => "SELECT $f FROM $t $j $w $o;", + } + } +} + +type Operand = String; + +enum Filter { + Equal { + op1: Operand, + op2: Operand, + }, + Greater { + op1: Operand, + op2: Operand, + }, + Less { + op1: Operand, + op2: Operand, + }, + GreaterOrEqual { + op1: Operand, + op2: Operand, + }, + LessOrEqual { + op1: Operand, + op2: Operand, + }, + NotEqual { + op1: Operand, + op2: Operand, + }, + And { + op1: Operand, + op2: Operand, + }, + Or { + op1: Operand, + op2: Operand, + }, + In { + value: Operand, + valid_range: Vec, + }, + Between { + value: Operand, + lower: Operand, + upper: Operand, + }, + Like { + value: Operand, + pattern: Operand, + }, + IsNull { + value: Operand, + }, + IsNotNull { + value: Operand, + }, + Not { + op: Operand, + }, +} + +impl ToString for Filter { + fn to_string(&self) -> String { + match self { + Filter::Equal { op1, op2 } => format!("{} = {}", op1.to_string(), op2.to_string()), + Filter::Greater { op1, op2 } => format!("{} > {}", op1.to_string(), op2.to_string()), + Filter::Less { op1, op2 } => format!("{} < {}", op1.to_string(), op2.to_string()), + Filter::GreaterOrEqual { op1, op2 } => { + format!("{} >= {}", op1.to_string(), op2.to_string()) + } + Filter::LessOrEqual { op1, op2 } => { + format!("{} <= {}", op1.to_string(), op2.to_string()) + } + Filter::NotEqual { op1, op2 } => format!("{} != {}", op1.to_string(), op2.to_string()), + Filter::And { op1, op2 } => format!("{} AND {}", op1.to_string(), op2.to_string()), + Filter::Or { op1, op2 } => format!("{} OR {}", op1.to_string(), op2.to_string()), + Filter::In { value, valid_range } => { + let mut formatted_range: String = "(".to_string(); + + for element in valid_range { + formatted_range.push_str(&element.to_string()); + formatted_range.push(','); + formatted_range.push(' '); + } + + if valid_range.len() > 0 { + formatted_range.pop(); + formatted_range.pop(); + }; + + formatted_range.push(')'); + format!("{} in {formatted_range}", value.to_string()) + } + Filter::Between { + value, + lower, + upper, + } => format!( + "{} BETWEEN {} AND {}", + value.to_string(), + lower.to_string(), + upper.to_string() + ), + Filter::Like { value, pattern } => { + format!("{} LIKE {}", value.to_string(), pattern.to_string()) + } + Filter::IsNull { value } => format!("{} IS NULL", value.to_string()), + Filter::IsNotNull { value } => format!("{} IS NOT NULL", value.to_string()), + Filter::Not { op } => format!("NOT {}", op.to_string()), + } + } +} + +pub struct FilterData(Option); +impl FilterData { + fn new() -> Self { + FilterData(None) + } + + pub fn equal(mut self, op1: T, op2: T) -> Self { + self.0 = Some(Filter::Equal { + op1: op1.to_string(), + op2: op2.to_string(), + }); + self + } + + pub fn greater(mut self, op1: T, op2: T) -> Self { + self.0 = Some(Filter::Greater { + op1: op1.to_string(), + op2: op2.to_string(), + }); + + self + } + + pub fn less(mut self, op1: T, op2: T) -> Self { + self.0 = Some(Filter::Less { + op1: op1.to_string(), + op2: op2.to_string(), + }); + + self + } + + pub fn greater_or_equal(mut self, op1: T, op2: T) -> Self { + self.0 = Some(Filter::GreaterOrEqual { + op1: op1.to_string(), + op2: op2.to_string(), + }); + + self + } + + pub fn less_or_equal(mut self, op1: T, op2: T) -> Self { + self.0 = Some(Filter::LessOrEqual { + op1: op1.to_string(), + op2: op2.to_string(), + }); + + self + } + + pub fn not_equal(mut self, op1: T, op2: T) -> Self { + self.0 = Some(Filter::NotEqual { + op1: op1.to_string(), + op2: op2.to_string(), + }); + + self + } + + pub fn and( + mut self, + filter1: impl Fn(FilterData) -> FilterData, + filter2: impl Fn(FilterData) -> FilterData, + ) -> Self { + let filter_data_1 = filter1(Self::new()); + let filter_data_2 = filter2(Self::new()); + + if filter_data_1.0.is_none() && filter_data_2.0.is_none() { + self.0 = None + } else if filter_data_1.0.is_none() && filter_data_2.0.is_some() { + self.0 = filter_data_2.0; + } else if filter_data_1.0.is_some() && filter_data_2.0.is_none() { + self.0 = filter_data_1.0; + } else { + self.0 = Some(Filter::And { + op1: filter_data_1.0.unwrap().to_string(), + op2: filter_data_2.0.unwrap().to_string(), + }); + } + + self + } + + // TODO: Refactor with `and` + pub fn or( + mut self, + filter1: impl Fn(FilterData) -> FilterData, + filter2: impl Fn(FilterData) -> FilterData, + ) -> Self { + let filter_data_1 = filter1(Self::new()); + let filter_data_2 = filter2(Self::new()); + + if filter_data_1.0.is_none() && filter_data_2.0.is_none() { + self.0 = None + } else if filter_data_1.0.is_none() && filter_data_2.0.is_some() { + self.0 = filter_data_2.0; + } else if filter_data_1.0.is_some() && filter_data_2.0.is_none() { + self.0 = filter_data_1.0; + } else { + self.0 = Some(Filter::Or { + op1: filter_data_1.0.unwrap().to_string(), + op2: filter_data_2.0.unwrap().to_string(), + }); + } + + self + } + + pub fn in_values(mut self, value: T, valid_values: Vec) -> Self { + self.0 = Some(Filter::In { + value: value.to_string(), + valid_range: valid_values.iter().map(|v| v.to_string()).collect(), + }); + + self + } + + pub fn between(mut self, value: T, lower: T, upper: T) -> Self { + self.0 = Some(Filter::Between { + value: value.to_string(), + lower: lower.to_string(), + upper: upper.to_string(), + }); + + self + } + + pub fn like(mut self, value: T, pattern: T) -> Self { + self.0 = Some(Filter::Like { + value: value.to_string(), + pattern: pattern.to_string(), + }); + + self + } + + pub fn is_null(mut self, value: T) -> Self { + self.0 = Some(Filter::IsNull { + value: value.to_string(), + }); + + self + } + + pub fn is_not_null(mut self, value: T) -> Self { + self.0 = Some(Filter::IsNotNull { + value: value.to_string(), + }); + + self + } + + // TODO: Handle case whern NOT is an expression with a nice interface + pub fn not(mut self, value: T) -> Self { + self.0 = Some(Filter::Not { + op: value.to_string(), + }); + + self + } +} + +enum Order { + Asc, + Desc, +} + +impl ToString for Order { + fn to_string(&self) -> String { + match self { + Order::Asc => "ASC", + Order::Desc => "DESC", + } + .to_string() + } +} + +struct OrderingClause { + field: String, + order: Order, +} + +impl ToString for OrderingClause { + fn to_string(&self) -> String { + format!("{} {}", self.field, self.order.to_string()) + } +} + +enum JoinKind { + Inner, + Left, + Right, + Full, +} + +impl ToString for JoinKind { + fn to_string(&self) -> String { + match self { + JoinKind::Inner => "INNER JOIN", + JoinKind::Left => "LEFT JOIN", + JoinKind::Right => "RIGHT JOIN", + JoinKind::Full => "FULL JOIN", + } + .to_string() + } +} + +struct Join { + kind: JoinKind, + dest_table: String, + join_columns: (String, String), +} + +impl ToString for Join { + fn to_string(&self) -> String { + format!( + " {} {} ON {} = {}", + self.kind.to_string(), + self.dest_table, + self.join_columns.0, + self.join_columns.1 + ) + } +} + +struct Query { + operation: QueryOperation, + table: String, + filter: Option, + ordering: Vec, + join: Option, +} + +impl Query { + pub fn new(operation: QueryOperation) -> Self { + Query { + operation, + table: T::table_name().to_string(), + filter: None, + ordering: vec![], + join: None, + } + } + + pub fn filter FilterData>(mut self, filter_data_callback: F) -> Self { + let filter_data = FilterData::new(); + self.filter = filter_data_callback(filter_data).0; + + self + } + + pub fn order(mut self, field: String, order: Order) -> Self { + self.ordering.push(OrderingClause { field, order }); + self + } + + pub fn join( + mut self, + kind: JoinKind, + dest_table: String, + join_columns: (String, String), + ) -> Self { + self.join = Some(Join { + kind, + dest_table, + join_columns, + }); + + self + } + + pub fn build(self) -> String { + let where_clause = if let Some(f) = self.filter { + format!(" WHERE {}", f.to_string()) + } else { + "".to_string() + }; + + let order_clause = if self.ordering.len() != 0 { + let mut clause_str = String::from(" ORDER BY "); + + for o in self.ordering { + clause_str.push_str(&o.to_string()); + clause_str.push(','); + clause_str.push(' '); + } + + clause_str.pop(); + clause_str.pop(); + + clause_str + } else { + "".to_string() + }; + + let join_clause = if let Some(j) = self.join { + j.to_string() + } else { + "".to_string() + }; + + self.operation + .format() + .replace("$f", "*") + .replace("$t", &self.table) + .replace(" $j", &join_clause) + .replace(" $w", &where_clause) + .replace(" $o", &order_clause) + } +} + +#[cfg(test)] +mod tests { + use super::ObjectModel; + + use super::Query; + + struct TestModel; + impl ObjectModel for TestModel { + fn table_name() -> &'static str { + "test_objects" + } + } + + #[test] + fn select_all() { + let query = Query::new::(super::QueryOperation::Select).build(); + + assert_eq!(query, "SELECT * FROM test_objects;") + } + + #[test] + fn select_join_inner() { + let query = Query::new::(super::QueryOperation::Select) + .join( + crate::JoinKind::Inner, + "test_join".to_owned(), + ("a".to_owned(), "b".to_owned()), + ) + .build(); + + assert_eq!( + query, + "SELECT * FROM test_objects INNER JOIN test_join ON a = b;", + ); + } + + #[test] + fn select_join_left() { + let query = Query::new::(super::QueryOperation::Select) + .join( + crate::JoinKind::Left, + "test_join".to_owned(), + ("a".to_owned(), "b".to_owned()), + ) + .build(); + + assert_eq!( + query, + "SELECT * FROM test_objects LEFT JOIN test_join ON a = b;", + ); + } + + #[test] + fn select_join_right() { + let query = Query::new::(super::QueryOperation::Select) + .join( + crate::JoinKind::Right, + "test_join".to_owned(), + ("a".to_owned(), "b".to_owned()), + ) + .build(); + + assert_eq!( + query, + "SELECT * FROM test_objects RIGHT JOIN test_join ON a = b;", + ); + } + + #[test] + fn select_join_full() { + let query = Query::new::(super::QueryOperation::Select) + .join( + crate::JoinKind::Full, + "test_join".to_owned(), + ("a".to_owned(), "b".to_owned()), + ) + .build(); + + assert_eq!( + query, + "SELECT * FROM test_objects FULL JOIN test_join ON a = b;", + ); + } + + #[test] + fn select_ordering_single_asc() { + let query = Query::new::(super::QueryOperation::Select) + .order("a".to_owned(), crate::Order::Asc) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects ORDER BY a ASC;"); + } + + #[test] + fn select_ordering_single_desc() { + let query = Query::new::(super::QueryOperation::Select) + .order("a".to_owned(), crate::Order::Desc) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects ORDER BY a DESC;"); + } + + #[test] + fn select_ordering_multiple() { + let query = Query::new::(super::QueryOperation::Select) + .order("a".to_owned(), crate::Order::Desc) + .order("b".to_owned(), crate::Order::Asc) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects ORDER BY a DESC, b ASC;"); + } + + #[test] + fn select_where_equal() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.equal(1, 1)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 = 1;"); + } + + #[test] + fn select_where_not_equal() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.not_equal(1, 2)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 != 2;") + } + + #[test] + fn select_where_greater() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.greater(2, 1)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 2 > 1;") + } + + #[test] + fn select_where_greater_or_equal() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.greater_or_equal(1, 1)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 >= 1;") + } + + #[test] + fn select_where_less() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.less(1, 2)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 < 2;") + } + + #[test] + fn select_where_less_or_equal() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.less_or_equal(2, 2)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 2 <= 2;") + } + + #[test] + fn select_where_and_two_valid_operand() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.and(|f1| f1.equal(1, 1), |f2| f2.equal(2, 2))) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 = 1 AND 2 = 2;") + } + + #[test] + fn select_where_and_one_valid_operand_only() { + let mut query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.and(|f1| f1.equal(1, 1), |f2| f2)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 = 1;"); + + query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.and(|f1| f1, |f2| f2.equal(1, 1))) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 = 1;"); + } + + #[test] + fn select_where_and_no_valid_operand() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.and(|f1| f1, |f2| f2)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects;"); + } + + #[test] + fn select_where_and_nested() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| { + f.and( + |f1| f1.and(|n1| n1.equal(1, 1), |n2| n2.equal(2, 2)), + |f2| f2.equal(3, 3), + ) + }) + .build(); + + assert_eq!( + query, + "SELECT * FROM test_objects WHERE 1 = 1 AND 2 = 2 AND 3 = 3;" + ) + } + + #[test] + fn select_where_or_two_valid_operand() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.or(|f1| f1.equal(1, 1), |f2| f2.equal(2, 2))) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 = 1 OR 2 = 2;"); + } + + #[test] + fn select_where_or_one_valid_operand() { + let mut query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.or(|f1| f1, |f2| f2.equal(2, 2))) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 2 = 2;"); + + query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.or(|f1| f1.equal(1, 1), |f2| f2)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 = 1;"); + } + + #[test] + fn select_where_or_no_valid_operand() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.or(|f1| f1, |f2| f2)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects;"); + } + + #[test] + fn select_where_or_nested() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| { + f.or( + |f1| f1.and(|n1| n1.equal(1, 1), |n2| n2.equal(2, 2)), + |f2| f2.equal(3, 3), + ) + }) + .build(); + + assert_eq!( + query, + "SELECT * FROM test_objects WHERE 1 = 1 AND 2 = 2 OR 3 = 3;" + ) + } + + #[test] + fn select_where_in_values() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.in_values(1, vec![1, 2, 3])) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 in (1, 2, 3);") + } + + #[test] + fn select_where_in_empty_values() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.in_values(1, vec![])) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 in ();") + } + + #[test] + fn select_where_between() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.between(1, 0, 5)) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE 1 BETWEEN 0 AND 5;"); + } + + #[test] + fn select_where_like() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.like("test", "'test'")) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE test LIKE 'test';"); + } + + #[test] + fn select_where_is_null() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.is_null("null")) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE null IS NULL;"); + } + + #[test] + fn select_where_is_not_null() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.is_not_null("null")) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE null IS NOT NULL;"); + } + + #[test] + fn select_where_not_primitive() { + let query = Query::new::(super::QueryOperation::Select) + .filter(|f| f.not("null")) + .build(); + + assert_eq!(query, "SELECT * FROM test_objects WHERE NOT null;") + } +}