use crate::typed::{TypedColumn, TypedTable};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinKind {
Inner,
Left,
Right,
Full,
Cross,
}
impl JoinKind {
pub fn as_sql(&self) -> &'static str {
match self {
JoinKind::Inner => "INNER JOIN",
JoinKind::Left => "LEFT JOIN",
JoinKind::Right => "RIGHT JOIN",
JoinKind::Full => "FULL OUTER JOIN",
JoinKind::Cross => "CROSS JOIN",
}
}
}
pub struct JoinBuilder {
kind: JoinKind,
}
impl JoinBuilder {
pub fn new(kind: JoinKind) -> Self {
Self { kind }
}
pub fn table<T: TypedTable>(self) -> JoinOn {
JoinOn {
kind: self.kind,
right_table: T::NAME,
}
}
}
pub struct JoinOn {
kind: JoinKind,
right_table: &'static str,
}
impl JoinOn {
pub fn on<L, R>(self) -> JoinBuilt
where
L: TypedColumn,
R: TypedColumn,
{
JoinBuilt {
kind: self.kind,
right_table: self.right_table,
left_column: L::NAME,
right_column: R::NAME,
}
}
pub fn build_no_on(self) -> String {
format!("{} {}", self.kind.as_sql(), self.right_table)
}
}
pub struct JoinBuilt {
kind: JoinKind,
right_table: &'static str,
left_column: &'static str,
right_column: &'static str,
}
impl JoinBuilt {
pub fn build(&self) -> String {
format!(
"{} {} ON {} = {}",
self.kind.as_sql(),
self.right_table,
self.left_column,
self.right_column
)
}
pub fn build_with_prefix(&self, left_table: &str) -> String {
format!(
"{} {} ON {}.{} = {}.{}",
self.kind.as_sql(),
self.right_table,
left_table,
self.left_column,
self.right_table,
self.right_column
)
}
}
pub fn inner_join<T: TypedTable>() -> JoinOn {
JoinBuilder::new(JoinKind::Inner).table::<T>()
}
pub fn left_join<T: TypedTable>() -> JoinOn {
JoinBuilder::new(JoinKind::Left).table::<T>()
}
pub fn right_join<T: TypedTable>() -> JoinOn {
JoinBuilder::new(JoinKind::Right).table::<T>()
}
pub fn full_join<T: TypedTable>() -> JoinOn {
JoinBuilder::new(JoinKind::Full).table::<T>()
}
pub fn cross_join<T: TypedTable>() -> JoinOn {
JoinBuilder::new(JoinKind::Cross).table::<T>()
}
#[cfg(test)]
mod tests {
use super::*;
struct UsersTable;
impl TypedTable for UsersTable {
const NAME: &'static str = "users";
}
struct OrdersTable;
impl TypedTable for OrdersTable {
const NAME: &'static str = "orders";
}
struct ColUserId;
impl TypedColumn for ColUserId {
const NAME: &'static str = "id";
type Table = UsersTable;
type RustType = i64;
type SqlType = crate::typed_ast::Untyped;
}
struct ColOrderUserId;
impl TypedColumn for ColOrderUserId {
const NAME: &'static str = "user_id";
type Table = OrdersTable;
type RustType = i64;
type SqlType = crate::typed_ast::Untyped;
}
struct ColOrderId;
impl TypedColumn for ColOrderId {
const NAME: &'static str = "id";
type Table = OrdersTable;
type RustType = i64;
type SqlType = crate::typed_ast::Untyped;
}
#[test]
fn test_join_kind_as_sql() {
assert_eq!(JoinKind::Inner.as_sql(), "INNER JOIN");
assert_eq!(JoinKind::Left.as_sql(), "LEFT JOIN");
assert_eq!(JoinKind::Right.as_sql(), "RIGHT JOIN");
assert_eq!(JoinKind::Full.as_sql(), "FULL OUTER JOIN");
assert_eq!(JoinKind::Cross.as_sql(), "CROSS JOIN");
}
#[test]
fn test_inner_join_basic() {
let join = JoinBuilder::new(JoinKind::Inner)
.table::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build();
assert_eq!(join, "INNER JOIN orders ON id = user_id");
}
#[test]
fn test_left_join_with_prefix() {
let join = JoinBuilder::new(JoinKind::Left)
.table::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build_with_prefix("users");
assert_eq!(join, "LEFT JOIN orders ON users.id = orders.user_id");
}
#[test]
fn test_right_join() {
let join = JoinBuilder::new(JoinKind::Right)
.table::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build();
assert_eq!(join, "RIGHT JOIN orders ON id = user_id");
}
#[test]
fn test_full_join() {
let join = JoinBuilder::new(JoinKind::Full)
.table::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build();
assert_eq!(join, "FULL OUTER JOIN orders ON id = user_id");
}
#[test]
fn test_cross_join_no_on() {
let join = JoinBuilder::new(JoinKind::Cross)
.table::<OrdersTable>()
.build_no_on();
assert_eq!(join, "CROSS JOIN orders");
}
#[test]
fn test_inner_join_helper() {
let join = inner_join::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build();
assert_eq!(join, "INNER JOIN orders ON id = user_id");
}
#[test]
fn test_left_join_helper() {
let join = left_join::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build();
assert_eq!(join, "LEFT JOIN orders ON id = user_id");
}
#[test]
fn test_right_join_helper() {
let join = right_join::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build();
assert_eq!(join, "RIGHT JOIN orders ON id = user_id");
}
#[test]
fn test_full_join_helper() {
let join = full_join::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build();
assert_eq!(join, "FULL OUTER JOIN orders ON id = user_id");
}
#[test]
fn test_cross_join_helper() {
let join = cross_join::<OrdersTable>().build_no_on();
assert_eq!(join, "CROSS JOIN orders");
}
#[test]
fn test_compile_time_table_association() {
let join = inner_join::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build();
assert!(!join.is_empty());
}
#[test]
fn test_self_join_same_table() {
let join = inner_join::<OrdersTable>()
.on::<ColOrderId, ColOrderUserId>()
.build();
assert_eq!(join, "INNER JOIN orders ON id = user_id");
}
#[test]
fn test_multiple_joins_concat() {
let j1 = inner_join::<OrdersTable>()
.on::<ColUserId, ColOrderUserId>()
.build_with_prefix("users");
let j2 = left_join::<OrdersTable>()
.on::<ColUserId, ColOrderId>()
.build_with_prefix("users");
let sql = format!("SELECT * FROM users {} {}", j1, j2);
assert!(sql.contains("INNER JOIN orders ON users.id = orders.user_id"));
assert!(sql.contains("LEFT JOIN orders ON users.id = orders.id"));
}
}