Skip to content

Commit

Permalink
22 add join (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
mjovanc authored Oct 8, 2024
2 parents 6aafc5e + 83d3eb6 commit f290060
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 18 deletions.
Binary file added njord/db/select_join.db
Binary file not shown.
5 changes: 4 additions & 1 deletion njord/src/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
68 changes: 65 additions & 3 deletions njord/src/sqlite/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -76,6 +76,7 @@ pub struct SelectQueryBuilder<'a, T: Table + Default> {
having_condition: Option<Condition>,
except_clauses: Option<Vec<SelectQueryBuilder<'a, T>>>,
union_clauses: Option<Vec<SelectQueryBuilder<'a, T>>>,
joins: Option<Vec<Join>>,
}

impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> {
Expand All @@ -99,6 +100,7 @@ impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> {
having_condition: None,
except_clauses: None,
union_clauses: None,
joins: None,
}
}

Expand Down Expand Up @@ -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<dyn Table>,
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
Expand All @@ -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<String> = 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);
Expand All @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions njord/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Table>,
pub on_condition: Condition,
}

impl Join {
pub fn new(join_type: JoinType, table: Arc<dyn Table>, on_condition: Condition) -> Self {
Join {
join_type,
table,
on_condition,
}
}
}

/// Converts values for SQL INSERT
///
/// # Arguments
Expand Down
4 changes: 2 additions & 2 deletions njord/tests/sqlite/delete_test.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -31,4 +31,4 @@ fn delete() {
panic!("Failed to DELETE: {:?}", e);
}
}
}
}
4 changes: 2 additions & 2 deletions njord/tests/sqlite/insert_test.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -25,4 +25,4 @@ fn insert_row() {
panic!("Failed to INSERT: {:?}", e);
}
}
}
}
17 changes: 13 additions & 4 deletions njord/tests/sqlite/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -44,4 +45,12 @@ pub struct Product {
pub stock_quantity: usize,
pub category: Category, // one-to-one relationship
pub discount: f64,
}
}

#[derive(Table)]
#[table_name = "users"]
pub struct UsersWithJoin {
username: String,
price: f64,
name: String,
}
2 changes: 1 addition & 1 deletion njord/tests/sqlite/open_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ fn open_db() {

let result = sqlite::open(db_path);
assert!(result.is_ok());
}
}
81 changes: 81 additions & 0 deletions njord/tests/sqlite/select_joins_test.rs
Original file line number Diff line number Diff line change
@@ -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),
}
}
Loading

0 comments on commit f290060

Please sign in to comment.