Skip to main content

reinhardt_query/types/
join.rs

1//! Join types for SQL queries.
2//!
3//! This module provides types for JOIN operations:
4//!
5//! - [`JoinType`]: The type of join (INNER, LEFT, RIGHT, etc.)
6//! - [`JoinOn`]: Join condition specification
7//! - [`JoinExpr`]: Complete join expression
8
9use super::{iden::DynIden, table_ref::TableRef};
10use crate::expr::{Condition, IntoCondition};
11
12/// SQL JOIN types.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum JoinType {
15	/// INNER JOIN - returns rows when there is a match in both tables
16	Join,
17	/// INNER JOIN (explicit)
18	InnerJoin,
19	/// LEFT JOIN - returns all rows from the left table
20	LeftJoin,
21	/// RIGHT JOIN - returns all rows from the right table
22	RightJoin,
23	/// FULL OUTER JOIN - returns rows when there is a match in one of the tables
24	FullOuterJoin,
25	/// CROSS JOIN - cartesian product of both tables
26	CrossJoin,
27}
28
29impl JoinType {
30	/// Returns the SQL representation of this join type.
31	#[must_use]
32	pub fn as_str(&self) -> &'static str {
33		match self {
34			Self::Join => "JOIN",
35			Self::InnerJoin => "INNER JOIN",
36			Self::LeftJoin => "LEFT JOIN",
37			Self::RightJoin => "RIGHT JOIN",
38			Self::FullOuterJoin => "FULL OUTER JOIN",
39			Self::CrossJoin => "CROSS JOIN",
40		}
41	}
42}
43
44/// Join condition specification.
45///
46/// Represents the ON or USING clause of a JOIN.
47#[derive(Debug, Clone)]
48pub enum JoinOn {
49	/// ON condition with two column references (simple case)
50	Columns(ColumnPair),
51	/// ON condition with complex expression
52	Condition(Condition),
53	/// USING (column_list) clause
54	Using(Vec<DynIden>),
55}
56
57/// A pair of columns for join conditions.
58#[derive(Debug, Clone)]
59pub struct ColumnPair {
60	/// Left column (from the main table)
61	pub left: ColumnSpec,
62	/// Right column (from the joined table)
63	pub right: ColumnSpec,
64}
65
66/// Column specification that can be simple or qualified.
67#[derive(Debug, Clone)]
68pub enum ColumnSpec {
69	/// Simple column name
70	Column(DynIden),
71	/// Table-qualified column (table.column)
72	TableColumn(DynIden, DynIden),
73}
74
75impl ColumnSpec {
76	/// Create a simple column specification.
77	pub fn column<I: super::iden::IntoIden>(column: I) -> Self {
78		Self::Column(column.into_iden())
79	}
80
81	/// Create a table-qualified column specification.
82	pub fn table_column<T: super::iden::IntoIden, C: super::iden::IntoIden>(
83		table: T,
84		column: C,
85	) -> Self {
86		Self::TableColumn(table.into_iden(), column.into_iden())
87	}
88}
89
90/// A complete join expression.
91///
92/// This represents a complete JOIN clause including the join type,
93/// target table, and join condition.
94#[derive(Debug, Clone)]
95pub struct JoinExpr {
96	/// The type of join
97	pub join: JoinType,
98	/// The table to join
99	pub table: TableRef,
100	/// The join condition
101	pub on: Option<JoinOn>,
102}
103
104impl JoinExpr {
105	/// Create a new join expression with an INNER JOIN.
106	pub fn new(table: TableRef) -> Self {
107		Self {
108			join: JoinType::InnerJoin,
109			table,
110			on: None,
111		}
112	}
113
114	/// Set the join type.
115	#[must_use]
116	pub fn join_type(mut self, join: JoinType) -> Self {
117		self.join = join;
118		self
119	}
120
121	/// Set the join condition.
122	#[must_use]
123	pub fn on(mut self, condition: JoinOn) -> Self {
124		self.on = Some(condition);
125		self
126	}
127
128	/// Create a join condition on two columns.
129	#[must_use]
130	pub fn on_columns(mut self, left: ColumnSpec, right: ColumnSpec) -> Self {
131		self.on = Some(JoinOn::Columns(ColumnPair { left, right }));
132		self
133	}
134
135	/// Create a join condition with a complex expression.
136	#[must_use]
137	pub fn on_condition<C: IntoCondition>(mut self, condition: C) -> Self {
138		self.on = Some(JoinOn::Condition(condition.into_condition()));
139		self
140	}
141
142	/// Create a USING clause with column names.
143	#[must_use]
144	pub fn using_columns<I, C>(mut self, columns: I) -> Self
145	where
146		I: IntoIterator<Item = C>,
147		C: super::iden::IntoIden,
148	{
149		let cols: Vec<DynIden> = columns.into_iter().map(|c| c.into_iden()).collect();
150		self.on = Some(JoinOn::Using(cols));
151		self
152	}
153}
154
155#[cfg(test)]
156mod tests {
157	use super::*;
158	use crate::types::iden::IntoIden;
159	use rstest::rstest;
160
161	#[rstest]
162	#[case(JoinType::Join, "JOIN")]
163	#[case(JoinType::InnerJoin, "INNER JOIN")]
164	#[case(JoinType::LeftJoin, "LEFT JOIN")]
165	#[case(JoinType::RightJoin, "RIGHT JOIN")]
166	#[case(JoinType::FullOuterJoin, "FULL OUTER JOIN")]
167	#[case(JoinType::CrossJoin, "CROSS JOIN")]
168	fn test_join_type_as_str(#[case] join_type: JoinType, #[case] expected: &str) {
169		assert_eq!(join_type.as_str(), expected);
170	}
171
172	#[rstest]
173	fn test_column_spec_simple() {
174		let _spec = ColumnSpec::column("id");
175	}
176
177	#[rstest]
178	fn test_column_spec_qualified() {
179		let _spec = ColumnSpec::table_column("users", "id");
180	}
181
182	#[rstest]
183	fn test_join_expr_builder() {
184		use crate::types::Alias;
185
186		let table = TableRef::Table(Alias::new("posts").into_iden());
187		let join = JoinExpr::new(table)
188			.join_type(JoinType::LeftJoin)
189			.on_columns(
190				ColumnSpec::table_column("users", "id"),
191				ColumnSpec::table_column("posts", "user_id"),
192			);
193
194		assert_eq!(join.join, JoinType::LeftJoin);
195		assert!(join.on.is_some());
196	}
197}