Skip to main content

reinhardt_db/orm/
lateral_join.rs

1/// LATERAL JOIN support for correlated subqueries as JOINs
2/// Available in PostgreSQL 9.3+, MySQL 8.0.14+, SQL Server 2017+
3use serde::{Deserialize, Serialize};
4
5/// Represents a LATERAL JOIN clause
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct LateralJoin {
8	/// The alias.
9	pub alias: String,
10	/// The subquery.
11	pub subquery: String,
12	/// The join type.
13	pub join_type: LateralJoinType,
14	/// The on condition.
15	pub on_condition: Option<String>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19/// Defines possible lateral join type values.
20pub enum LateralJoinType {
21	/// Inner variant.
22	Inner,
23	/// Left variant.
24	Left,
25	/// Right variant.
26	Right,
27	/// Full variant.
28	Full,
29}
30
31impl LateralJoin {
32	/// Create a new LATERAL JOIN for correlated subqueries
33	///
34	/// # Examples
35	///
36	/// ```
37	/// use reinhardt_db::orm::lateral_join::{LateralJoin, LateralJoinType};
38	///
39	/// let join = LateralJoin::new("sub", "SELECT * FROM orders WHERE user_id = users.id");
40	/// assert_eq!(join.alias, "sub");
41	/// assert_eq!(join.join_type, LateralJoinType::Left); // Default is LEFT JOIN
42	/// assert!(join.on_condition.is_none());
43	/// ```
44	pub fn new(alias: impl Into<String>, subquery: impl Into<String>) -> Self {
45		Self {
46			alias: alias.into(),
47			subquery: subquery.into(),
48			join_type: LateralJoinType::Left,
49			on_condition: None,
50		}
51	}
52	/// Documentation for `inner`
53	///
54	pub fn inner(mut self) -> Self {
55		self.join_type = LateralJoinType::Inner;
56		self
57	}
58	/// Documentation for `left`
59	///
60	pub fn left(mut self) -> Self {
61		self.join_type = LateralJoinType::Left;
62		self
63	}
64	/// Documentation for `on`
65	///
66	pub fn on(mut self, condition: impl Into<String>) -> Self {
67		self.on_condition = Some(condition.into());
68		self
69	}
70	/// Generate SQL for LATERAL JOIN
71	///
72	pub fn to_sql(&self) -> String {
73		let join_keyword = match self.join_type {
74			LateralJoinType::Inner => "INNER JOIN",
75			LateralJoinType::Left => "LEFT JOIN",
76			LateralJoinType::Right => "RIGHT JOIN",
77			LateralJoinType::Full => "FULL JOIN",
78		};
79
80		let on_clause = self
81			.on_condition
82			.as_ref()
83			.map(|c| format!(" ON {}", c))
84			.unwrap_or_else(|| " ON true".to_string());
85
86		format!(
87			"{} LATERAL ({}) AS {}{}",
88			join_keyword, self.subquery, self.alias, on_clause
89		)
90	}
91	/// Generate SQL for MySQL (uses different syntax)
92	///
93	pub fn to_mysql_sql(&self) -> String {
94		// MySQL doesn't use LATERAL keyword but supports similar functionality
95		let join_keyword = match self.join_type {
96			LateralJoinType::Inner => "INNER JOIN",
97			LateralJoinType::Left => "LEFT JOIN",
98			LateralJoinType::Right => "RIGHT JOIN",
99			LateralJoinType::Full => "FULL JOIN",
100		};
101
102		let on_clause = self
103			.on_condition
104			.as_ref()
105			.map(|c| format!(" ON {}", c))
106			.unwrap_or_else(|| " ON true".to_string());
107
108		format!(
109			"{} ({}) AS {}{}",
110			join_keyword, self.subquery, self.alias, on_clause
111		)
112	}
113}
114
115/// Builder for LATERAL JOINs
116pub struct LateralJoinBuilder {
117	alias: String,
118	subquery: String,
119	join_type: LateralJoinType,
120	on_condition: Option<String>,
121}
122
123impl LateralJoinBuilder {
124	/// Create a new builder for constructing LATERAL JOINs
125	///
126	/// # Examples
127	///
128	/// ```
129	/// use reinhardt_db::orm::lateral_join::LateralJoinBuilder;
130	///
131	/// let builder = LateralJoinBuilder::new("latest");
132	/// // Builder methods can be chained: .subquery().left().on().build()
133	/// ```
134	pub fn new(alias: impl Into<String>) -> Self {
135		Self {
136			alias: alias.into(),
137			subquery: String::new(),
138			join_type: LateralJoinType::Left,
139			on_condition: None,
140		}
141	}
142	/// Documentation for `subquery`
143	///
144	pub fn subquery(mut self, query: impl Into<String>) -> Self {
145		self.subquery = query.into();
146		self
147	}
148	/// Documentation for `inner`
149	///
150	pub fn inner(mut self) -> Self {
151		self.join_type = LateralJoinType::Inner;
152		self
153	}
154	/// Documentation for `left`
155	///
156	pub fn left(mut self) -> Self {
157		self.join_type = LateralJoinType::Left;
158		self
159	}
160	/// Documentation for `on`
161	///
162	pub fn on(mut self, condition: impl Into<String>) -> Self {
163		self.on_condition = Some(condition.into());
164		self
165	}
166	/// Documentation for `build`
167	///
168	pub fn build(self) -> LateralJoin {
169		LateralJoin {
170			alias: self.alias,
171			subquery: self.subquery,
172			join_type: self.join_type,
173			on_condition: self.on_condition,
174		}
175	}
176}
177
178/// Common LATERAL JOIN patterns
179pub struct LateralJoinPatterns;
180
181impl LateralJoinPatterns {
182	/// Get top N related records per parent
183	///
184	pub fn top_n_per_group(
185		alias: &str,
186		table: &str,
187		foreign_key: &str,
188		parent_key: &str,
189		order_by: &str,
190		limit: usize,
191	) -> LateralJoin {
192		let subquery = format!(
193			"SELECT * FROM {} WHERE {} = {}.{} ORDER BY {} LIMIT {}",
194			table, foreign_key, parent_key, "id", order_by, limit
195		);
196
197		LateralJoin::new(alias, subquery).left()
198	}
199
200	/// Get latest record per parent
201	pub fn latest_per_parent(
202		alias: &str,
203		table: &str,
204		foreign_key: &str,
205		parent_table: &str,
206		date_field: &str,
207	) -> LateralJoin {
208		let subquery = format!(
209			"SELECT * FROM {} WHERE {} = {}.id ORDER BY {} DESC LIMIT 1",
210			table, foreign_key, parent_table, date_field
211		);
212
213		LateralJoin::new(alias, subquery).left()
214	}
215	/// Aggregate calculation per parent
216	///
217	pub fn aggregate_per_parent(
218		alias: &str,
219		table: &str,
220		foreign_key: &str,
221		parent_table: &str,
222		aggregate_expr: &str,
223	) -> LateralJoin {
224		let subquery = format!(
225			"SELECT {} FROM {} WHERE {} = {}.id",
226			aggregate_expr, table, foreign_key, parent_table
227		);
228
229		LateralJoin::new(alias, subquery).left()
230	}
231	/// Ranked results per parent
232	///
233	pub fn ranked_per_parent(
234		alias: &str,
235		table: &str,
236		foreign_key: &str,
237		parent_table: &str,
238		rank_expr: &str,
239		limit: usize,
240	) -> LateralJoin {
241		let subquery = format!(
242			r#"
243            SELECT *, ROW_NUMBER() OVER (ORDER BY {}) as rank
244            FROM {}
245            WHERE {} = {}.id
246            ORDER BY {}
247            LIMIT {}
248            "#,
249			rank_expr, table, foreign_key, parent_table, rank_expr, limit
250		);
251
252		LateralJoin::new(alias, subquery.trim()).left()
253	}
254	/// Conditional aggregation per parent
255	///
256	pub fn conditional_aggregate(
257		alias: &str,
258		table: &str,
259		foreign_key: &str,
260		parent_table: &str,
261		condition: &str,
262		aggregate_expr: &str,
263	) -> LateralJoin {
264		let subquery = format!(
265			"SELECT {} FROM {} WHERE {} = {}.id AND {}",
266			aggregate_expr, table, foreign_key, parent_table, condition
267		);
268
269		LateralJoin::new(alias, subquery).left()
270	}
271	/// Cross-apply style (get first match)
272	///
273	pub fn first_match(
274		alias: &str,
275		table: &str,
276		join_condition: &str,
277		order_by: &str,
278	) -> LateralJoin {
279		let subquery = format!(
280			"SELECT * FROM {} WHERE {} ORDER BY {} LIMIT 1",
281			table, join_condition, order_by
282		);
283
284		LateralJoin::new(alias, subquery).inner()
285	}
286	/// Window function with LATERAL
287	///
288	pub fn window_aggregate(
289		alias: &str,
290		table: &str,
291		foreign_key: &str,
292		parent_table: &str,
293		window_expr: &str,
294	) -> LateralJoin {
295		let subquery = format!(
296			"SELECT *, {} FROM {} WHERE {} = {}.id",
297			window_expr, table, foreign_key, parent_table
298		);
299
300		LateralJoin::new(alias, subquery).left()
301	}
302}
303
304/// Collection of LATERAL JOINs
305#[derive(Debug, Clone, Default)]
306pub struct LateralJoins {
307	joins: Vec<LateralJoin>,
308}
309
310impl LateralJoins {
311	/// Create a new collection of LATERAL JOINs
312	///
313	/// # Examples
314	///
315	/// ```
316	/// use reinhardt_db::orm::lateral_join::LateralJoins;
317	///
318	/// let joins = LateralJoins::new();
319	/// assert!(joins.is_empty());
320	/// assert_eq!(joins.len(), 0);
321	/// ```
322	pub fn new() -> Self {
323		Self { joins: Vec::new() }
324	}
325	/// Documentation for `add`
326	///
327	pub fn add(&mut self, join: LateralJoin) {
328		self.joins.push(join);
329	}
330	/// Documentation for `is_empty`
331	///
332	pub fn is_empty(&self) -> bool {
333		self.joins.is_empty()
334	}
335	/// Documentation for `len`
336	///
337	pub fn len(&self) -> usize {
338		self.joins.len()
339	}
340	/// Documentation for `to_sql`
341	///
342	pub fn to_sql(&self) -> Vec<String> {
343		self.joins.iter().map(|j| j.to_sql()).collect()
344	}
345	/// Documentation for `to_mysql_sql`
346	///
347	pub fn to_mysql_sql(&self) -> Vec<String> {
348		self.joins.iter().map(|j| j.to_mysql_sql()).collect()
349	}
350}
351
352#[cfg(test)]
353mod tests {
354	use super::*;
355
356	#[test]
357	fn test_lateral_join_creation() {
358		let join = LateralJoin::new("sub", "SELECT * FROM orders WHERE user_id = users.id");
359		assert_eq!(join.alias, "sub");
360		assert_eq!(join.join_type, LateralJoinType::Left);
361	}
362
363	#[test]
364	fn test_lateral_join_types() {
365		let inner = LateralJoin::new("sub", "SELECT 1").inner();
366		assert_eq!(inner.join_type, LateralJoinType::Inner);
367
368		let left = LateralJoin::new("sub", "SELECT 1").left();
369		assert_eq!(left.join_type, LateralJoinType::Left);
370	}
371
372	#[test]
373	fn test_lateral_join_sql() {
374		let join = LateralJoin::new(
375			"recent_orders",
376			"SELECT * FROM orders WHERE user_id = users.id LIMIT 5",
377		)
378		.left();
379
380		let sql = join.to_sql();
381		assert!(sql.contains("LEFT JOIN LATERAL"));
382		assert!(sql.contains("recent_orders"));
383		assert!(sql.contains("ON true"));
384	}
385
386	#[test]
387	fn test_lateral_join_with_on_condition() {
388		let join =
389			LateralJoin::new("sub", "SELECT * FROM items").on("sub.category_id = categories.id");
390
391		let sql = join.to_sql();
392		assert!(sql.contains("ON sub.category_id = categories.id"));
393	}
394
395	#[test]
396	fn test_lateral_join_builder_pattern() {
397		let join = LateralJoinBuilder::new("latest")
398			.subquery(
399				"SELECT * FROM events WHERE user_id = users.id ORDER BY created_at DESC LIMIT 1",
400			)
401			.left()
402			.build();
403
404		assert_eq!(join.alias, "latest");
405		assert!(join.subquery.contains("ORDER BY"));
406	}
407
408	#[test]
409	fn test_top_n_pattern() {
410		let join = LateralJoinPatterns::top_n_per_group(
411			"top_products",
412			"products",
413			"category_id",
414			"categories",
415			"sales DESC",
416			3,
417		);
418
419		let sql = join.to_sql();
420		assert!(sql.contains("LIMIT 3"));
421		assert!(sql.contains("ORDER BY sales DESC"));
422	}
423
424	#[test]
425	fn test_latest_per_parent_pattern() {
426		let join = LateralJoinPatterns::latest_per_parent(
427			"latest_order",
428			"orders",
429			"customer_id",
430			"customers",
431			"created_at",
432		);
433
434		let sql = join.to_sql();
435		assert!(sql.contains("LIMIT 1"));
436		assert!(sql.contains("ORDER BY created_at DESC"));
437	}
438
439	#[test]
440	fn test_aggregate_per_parent_pattern() {
441		let join = LateralJoinPatterns::aggregate_per_parent(
442			"order_stats",
443			"orders",
444			"customer_id",
445			"customers",
446			"COUNT(*) as order_count, SUM(total) as total_spent",
447		);
448
449		let sql = join.to_sql();
450		assert!(sql.contains("COUNT(*)"));
451		assert!(sql.contains("SUM(total)"));
452	}
453
454	#[test]
455	fn test_ranked_per_parent_pattern() {
456		let join = LateralJoinPatterns::ranked_per_parent(
457			"ranked_reviews",
458			"reviews",
459			"product_id",
460			"products",
461			"rating DESC, helpful_count DESC",
462			5,
463		);
464
465		let sql = join.to_sql();
466		assert!(sql.contains("ROW_NUMBER()"));
467		assert!(sql.contains("LIMIT 5"));
468	}
469
470	#[test]
471	fn test_conditional_aggregate_pattern() {
472		let join = LateralJoinPatterns::conditional_aggregate(
473			"high_value_orders",
474			"orders",
475			"customer_id",
476			"customers",
477			"total > 1000",
478			"COUNT(*) as high_value_count, SUM(total) as high_value_total",
479		);
480
481		let sql = join.to_sql();
482		assert!(sql.contains("total > 1000"));
483		assert!(sql.contains("COUNT(*)"));
484	}
485
486	#[test]
487	fn test_first_match_pattern() {
488		let join = LateralJoinPatterns::first_match(
489			"matching_promo",
490			"promotions",
491			"promotions.category = products.category",
492			"priority DESC",
493		);
494
495		assert_eq!(join.join_type, LateralJoinType::Inner);
496		let sql = join.to_sql();
497		assert!(sql.contains("LIMIT 1"));
498	}
499
500	#[test]
501	fn test_lateral_join_mysql_sql() {
502		let join = LateralJoin::new("sub", "SELECT * FROM orders LIMIT 5");
503		let sql = join.to_mysql_sql();
504
505		// MySQL doesn't use LATERAL keyword
506		assert!(!sql.contains("LATERAL"));
507		assert!(sql.contains("LEFT JOIN"));
508	}
509
510	#[test]
511	fn test_lateral_joins_collection() {
512		let mut joins = LateralJoins::new();
513
514		joins.add(LateralJoin::new("j1", "SELECT 1"));
515		joins.add(LateralJoin::new("j2", "SELECT 2"));
516
517		assert_eq!(joins.len(), 2);
518
519		let sqls = joins.to_sql();
520		assert_eq!(sqls.len(), 2);
521	}
522
523	#[test]
524	fn test_lateral_join_empty_collection() {
525		let joins = LateralJoins::new();
526		assert!(joins.is_empty());
527		assert_eq!(joins.len(), 0);
528	}
529
530	#[test]
531	fn test_window_aggregate_pattern() {
532		let join = LateralJoinPatterns::window_aggregate(
533			"windowed",
534			"sales",
535			"product_id",
536			"products",
537			"AVG(amount) OVER (ORDER BY date ROWS 7 PRECEDING) as moving_avg",
538		);
539
540		let sql = join.to_sql();
541		assert!(sql.contains("AVG(amount) OVER"));
542		assert!(sql.contains("ROWS 7 PRECEDING"));
543	}
544}