diff --git a/njord/db/select_join.db b/njord/db/select_join.db new file mode 100644 index 00000000..63e62dd7 Binary files /dev/null and b/njord/db/select_join.db differ diff --git a/njord/src/condition.rs b/njord/src/condition.rs index 0ed8a256..4c21ac7c 100644 --- a/njord/src/condition.rs +++ b/njord/src/condition.rs @@ -74,7 +74,10 @@ impl Condition { pub fn build(&self) -> String { match self { Condition::Eq(column, value) => { - if Condition::is_numeric(value) { + // If contains a dot, assume it's a table.column + if column.contains('.') { + format!("{} = {}", column, value) + } else if Condition::is_numeric(value) { format!("{} = {}", column, value) } else { format!("{} = '{}'", column, value) diff --git a/njord/src/sqlite/select.rs b/njord/src/sqlite/select.rs index f5ac8e4e..81d3b216 100644 --- a/njord/src/sqlite/select.rs +++ b/njord/src/sqlite/select.rs @@ -35,14 +35,14 @@ use crate::{ generate_order_by_str, generate_where_condition_str, }, }; -use std::collections::HashMap; - use rusqlite::{Connection, Result}; +use std::{collections::HashMap, sync::Arc}; use log::info; use rusqlite::types::Value; use crate::table::Table; +use crate::util::{Join, JoinType}; /// Constructs a new SELECT query builder. /// @@ -76,6 +76,7 @@ pub struct SelectQueryBuilder<'a, T: Table + Default> { having_condition: Option, except_clauses: Option>>, union_clauses: Option>>, + joins: Option>, } impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> { @@ -99,6 +100,7 @@ impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> { having_condition: None, except_clauses: None, union_clauses: None, + joins: None, } } @@ -234,6 +236,35 @@ impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> { self } + /// Adds a JOIN clause to the query, allowing you to combine rows from two or more tables based on a related column. + /// + /// This method modifies the current query builder to include a join clause with the specified join type, + /// target table, and condition for the join. If there are already existing JOIN clauses, the new clause + /// will be added to the list. If no JOIN clauses exist, a new list will be created with the provided + /// join information. + /// + /// # Arguments + /// + /// * `join_type` - The type of join to perform (e.g., INNER, LEFT, RIGHT, FULL). + /// * `table` - The table to join with the current table. + /// * `on_condition` - The condition that specifies how the tables are related (the ON clause). + /// + /// # Returns + /// + /// Returns the modified `SelectQueryBuilder` instance with the new JOIN clause added. + pub fn join( + mut self, + join_type: JoinType, + table: Arc, + on_condition: Condition, + ) -> Self { + match self.joins { + Some(ref mut joins) => joins.push(Join::new(join_type, table, on_condition)), + None => self.joins = Some(vec![Join::new(join_type, table, on_condition)]), + } + self + } + /// Builds the query string, this function should be used internally. pub fn build_query(&self) -> String { let columns_str = self @@ -249,6 +280,29 @@ impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> { .map(|t| t.get_name().to_string()) .unwrap_or("".to_string()); + // Generate JOIN clauses, if any + let join_clauses: Vec = match &self.joins { + Some(joins) => joins + .iter() + .map(|join| { + let join_type_str = match join.join_type { + JoinType::Inner => "INNER JOIN", + JoinType::Left => "LEFT JOIN", + JoinType::Right => "RIGHT JOIN", + JoinType::Full => "FULL OUTER JOIN", + }; + format!( + "{} {} ON {}", + join_type_str, + join.table.get_name(), + generate_where_condition_str(Some(join.on_condition.clone())) + .replace("WHERE", "") + ) + }) + .collect(), + None => Vec::new(), + }; + let distinct_str = if self.distinct { "DISTINCT " } else { "" }; let where_condition_str = generate_where_condition_str(self.where_condition.clone()); let group_by_str = generate_group_by_str(&self.group_by); @@ -258,11 +312,19 @@ impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> { let having_str = generate_having_str(self.group_by.is_some(), self.having_condition.as_ref()); + // Create the JOIN clause or an empty string + let join_clause = if !join_clauses.is_empty() { + join_clauses.join(" ") + } else { + String::new() + }; + let mut query = format!( - "SELECT {}{} FROM {} {} {} {} {} {}", + "SELECT {}{} FROM {} {} {} {} {} {} {}", distinct_str, columns_str, table_name, + join_clause, where_condition_str, group_by_str, having_str, diff --git a/njord/src/util.rs b/njord/src/util.rs index bb202ed3..cb9bc3b2 100644 --- a/njord/src/util.rs +++ b/njord/src/util.rs @@ -27,6 +27,36 @@ //! 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. +use std::sync::Arc; + +use crate::condition::Condition; +use crate::table::Table; + +#[derive(Clone)] +pub enum JoinType { + Inner, + Left, + Right, + Full, +} + +#[derive(Clone)] +pub struct Join { + pub join_type: JoinType, + pub table: Arc, + pub on_condition: Condition, +} + +impl Join { + pub fn new(join_type: JoinType, table: Arc, on_condition: Condition) -> Self { + Join { + join_type, + table, + on_condition, + } + } +} + /// Converts values for SQL INSERT /// /// # Arguments diff --git a/njord/tests/sqlite/delete_test.rs b/njord/tests/sqlite/delete_test.rs index 513cc65c..90355367 100644 --- a/njord/tests/sqlite/delete_test.rs +++ b/njord/tests/sqlite/delete_test.rs @@ -1,8 +1,8 @@ +use super::User; use njord::condition::Condition; use njord::sqlite; use std::collections::HashMap; use std::path::Path; -use super::User; #[test] fn delete() { @@ -31,4 +31,4 @@ fn delete() { panic!("Failed to DELETE: {:?}", e); } } -} \ No newline at end of file +} diff --git a/njord/tests/sqlite/insert_test.rs b/njord/tests/sqlite/insert_test.rs index 48aa2b3f..1502a8cd 100644 --- a/njord/tests/sqlite/insert_test.rs +++ b/njord/tests/sqlite/insert_test.rs @@ -1,7 +1,7 @@ +use super::User; use njord::keys::AutoIncrementPrimaryKey; use njord::sqlite; use std::path::Path; -use super::User; #[test] fn insert_row() { @@ -25,4 +25,4 @@ fn insert_row() { panic!("Failed to INSERT: {:?}", e); } } -} \ No newline at end of file +} diff --git a/njord/tests/sqlite/mod.rs b/njord/tests/sqlite/mod.rs index c4bf4e54..a8a49f93 100644 --- a/njord/tests/sqlite/mod.rs +++ b/njord/tests/sqlite/mod.rs @@ -1,8 +1,9 @@ -mod open_test; -mod insert_test; -mod update_test; mod delete_test; +mod insert_test; +mod open_test; +mod select_joins_test; mod select_test; +mod update_test; use njord::keys::{AutoIncrementPrimaryKey, PrimaryKey}; use njord::table::Table; @@ -44,4 +45,12 @@ pub struct Product { pub stock_quantity: usize, pub category: Category, // one-to-one relationship pub discount: f64, -} \ No newline at end of file +} + +#[derive(Table)] +#[table_name = "users"] +pub struct UsersWithJoin { + username: String, + price: f64, + name: String, +} diff --git a/njord/tests/sqlite/open_test.rs b/njord/tests/sqlite/open_test.rs index 3ec057ec..34cb145c 100644 --- a/njord/tests/sqlite/open_test.rs +++ b/njord/tests/sqlite/open_test.rs @@ -8,4 +8,4 @@ fn open_db() { let result = sqlite::open(db_path); assert!(result.is_ok()); -} \ No newline at end of file +} diff --git a/njord/tests/sqlite/select_joins_test.rs b/njord/tests/sqlite/select_joins_test.rs new file mode 100644 index 00000000..21ff5503 --- /dev/null +++ b/njord/tests/sqlite/select_joins_test.rs @@ -0,0 +1,81 @@ +use njord::column::Column; +use njord::condition::Condition; +use njord::sqlite; +use njord::util::JoinType; +use std::path::Path; +use std::sync::Arc; + +use crate::{Product, UsersWithJoin}; + +#[test] +fn select_inner_join() { + let db_relative_path = "./db/select_join.db"; + let db_path = Path::new(&db_relative_path); + let conn = sqlite::open(db_path); + + // Assume we have pre-inserted some data into the users and products tables + let columns = vec![ + Column::Text("users.username".to_string()), + Column::Text("products.name".to_string()), + Column::Text("products.price".to_string()), + ]; + + // Assuming a hypothetical join condition: users.id = products.user_id + let join_condition = Condition::Eq("users.id".to_string(), "products.user_id".to_string()); + match conn { + Ok(c) => { + let result = sqlite::select(&c, columns) + .from(UsersWithJoin::default()) + .join( + JoinType::Inner, + Arc::new(Product::default()), + join_condition, + ) + .build(); + match result { + Ok(r) => { + // Check the number of results and assert against expected values + assert!(!r.is_empty(), "Expected results, but got none."); + // Further assertions on expected data can be made here based on inserted data + } + Err(e) => panic!("Failed to SELECT with JOIN: {:?}", e), + }; + } + Err(e) => panic!("Failed to SELECT: {:?}", e), + } +} + +#[test] +fn select_left_join() { + let db_relative_path = "./db/select_join.db"; + let db_path = Path::new(&db_relative_path); + let conn = sqlite::open(db_path); + + // Assume we have pre-inserted some data into the users and products tables + let columns = vec![ + Column::Text("users.username".to_string()), + Column::Text("products.name".to_string()), + Column::Text("products.price".to_string()), + ]; + + // Assuming a hypothetical join condition: users.id = products.user_id + let join_condition = Condition::Eq("users.id".to_string(), "products.user_id".to_string()); + match conn { + Ok(c) => { + let result = sqlite::select(&c, columns) + .from(UsersWithJoin::default()) + .join(JoinType::Left, Arc::new(Product::default()), join_condition) + .build(); + match result { + Ok(r) => { + // Check the number of results and assert against expected values + assert!(!r.is_empty(), "Expected results, but got none."); + assert_eq!(r.len(), 2, "Expected 2 results from the LEFT JOIN query."); + // Further assertions on expected data can be made here based on inserted data + } + Err(e) => panic!("Failed to SELECT with JOIN: {:?}", e), + }; + } + Err(e) => panic!("Failed to SELECT: {:?}", e), + } +} diff --git a/njord/tests/sqlite/select_test.rs b/njord/tests/sqlite/select_test.rs index f574734c..8f750013 100644 --- a/njord/tests/sqlite/select_test.rs +++ b/njord/tests/sqlite/select_test.rs @@ -1,10 +1,111 @@ use njord::column::Column; use njord::condition::Condition; -use njord::sqlite; use njord::keys::AutoIncrementPrimaryKey; +use njord::sqlite; use std::collections::HashMap; use std::path::Path; -use super::{User, UserWithSubQuery}; + +use crate::{User, UserWithSubQuery}; + +#[test] +fn open_db() { + let db_relative_path = "./db/open.db"; + let db_path = Path::new(&db_relative_path); + + let result = sqlite::open(db_path); + assert!(result.is_ok()); +} + +#[test] +fn insert_row() { + let db_relative_path = "./db/insert.db"; + let db_path = Path::new(&db_relative_path); + let conn = sqlite::open(db_path); + + let table_row: User = User { + id: AutoIncrementPrimaryKey::default(), + username: "mjovanc".to_string(), + email: "mjovanc@icloud.com".to_string(), + address: "Some Random Address 1".to_string(), + }; + + match conn { + Ok(c) => { + let result = sqlite::insert(c, vec![table_row]); + assert!(result.is_ok()); + } + Err(e) => { + panic!("Failed to INSERT: {:?}", e); + } + } +} + +#[test] +fn update() { + let db_relative_path = "./db/insert.db"; + let db_path = Path::new(&db_relative_path); + let conn = sqlite::open(db_path); + + let columns = vec!["username".to_string()]; + + let condition = Condition::Eq("username".to_string(), "mjovanc".to_string()); + + let table_row: User = User { + id: AutoIncrementPrimaryKey::::new(Some(0)), + username: "mjovanc".to_string(), + email: "mjovanc@icloud.com".to_string(), + address: "Some Random Address 1".to_string(), + }; + + let mut order = HashMap::new(); + order.insert(vec!["id".to_string()], "DESC".to_string()); + + match conn { + Ok(c) => { + let result = sqlite::update(&c, table_row) + .set(columns) + .where_clause(condition) + .order_by(order) + .limit(4) + .offset(0) + .build(); + println!("{:?}", result); + assert!(result.is_ok()); + } + Err(e) => { + panic!("Failed to UPDATE: {:?}", e); + } + } +} + +#[test] +fn delete() { + let db_relative_path = "./db/insert.db"; + let db_path = Path::new(&db_relative_path); + let conn = sqlite::open(db_path); + + let condition = Condition::Eq("address".to_string(), "Some Random Address 1".to_string()); + + let mut order = HashMap::new(); + order.insert(vec!["id".to_string()], "DESC".to_string()); + + match conn { + Ok(c) => { + let result = sqlite::delete(c) + .from(User::default()) + .where_clause(condition) + .order_by(order) + .limit(20) + .offset(0) + .build(); + println!("{:?}", result); + assert!(result.is_ok()); + } + Err(e) => { + panic!("Failed to DELETE: {:?}", e); + } + } +} #[test] fn select() { @@ -387,4 +488,4 @@ fn select_in() { } Err(e) => panic!("Failed to SELECT: {:?}", e), }; -} \ No newline at end of file +} diff --git a/njord/tests/sqlite/update_test.rs b/njord/tests/sqlite/update_test.rs index 42fdf726..6b39c196 100644 --- a/njord/tests/sqlite/update_test.rs +++ b/njord/tests/sqlite/update_test.rs @@ -1,3 +1,4 @@ +use super::User; use njord::column::Column; use njord::condition::Condition; use njord::keys::AutoIncrementPrimaryKey; @@ -5,7 +6,6 @@ use njord::sqlite::select::SelectQueryBuilder; use njord::sqlite::{self}; use std::collections::HashMap; use std::path::Path; -use super::{User}; #[test] fn update() { @@ -83,4 +83,4 @@ fn update_with_sub_queries() { } Err(e) => panic!("Failed to UPDATE: {:?}", e), }; -} \ No newline at end of file +}