1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct LateralJoin {
8 pub alias: String,
10 pub subquery: String,
12 pub join_type: LateralJoinType,
14 pub on_condition: Option<String>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum LateralJoinType {
21 Inner,
23 Left,
25 Right,
27 Full,
29}
30
31impl LateralJoin {
32 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 pub fn inner(mut self) -> Self {
55 self.join_type = LateralJoinType::Inner;
56 self
57 }
58 pub fn left(mut self) -> Self {
61 self.join_type = LateralJoinType::Left;
62 self
63 }
64 pub fn on(mut self, condition: impl Into<String>) -> Self {
67 self.on_condition = Some(condition.into());
68 self
69 }
70 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 pub fn to_mysql_sql(&self) -> String {
94 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
115pub struct LateralJoinBuilder {
117 alias: String,
118 subquery: String,
119 join_type: LateralJoinType,
120 on_condition: Option<String>,
121}
122
123impl LateralJoinBuilder {
124 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 pub fn subquery(mut self, query: impl Into<String>) -> Self {
145 self.subquery = query.into();
146 self
147 }
148 pub fn inner(mut self) -> Self {
151 self.join_type = LateralJoinType::Inner;
152 self
153 }
154 pub fn left(mut self) -> Self {
157 self.join_type = LateralJoinType::Left;
158 self
159 }
160 pub fn on(mut self, condition: impl Into<String>) -> Self {
163 self.on_condition = Some(condition.into());
164 self
165 }
166 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
178pub struct LateralJoinPatterns;
180
181impl LateralJoinPatterns {
182 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 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 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 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 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 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 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#[derive(Debug, Clone, Default)]
306pub struct LateralJoins {
307 joins: Vec<LateralJoin>,
308}
309
310impl LateralJoins {
311 pub fn new() -> Self {
323 Self { joins: Vec::new() }
324 }
325 pub fn add(&mut self, join: LateralJoin) {
328 self.joins.push(join);
329 }
330 pub fn is_empty(&self) -> bool {
333 self.joins.is_empty()
334 }
335 pub fn len(&self) -> usize {
338 self.joins.len()
339 }
340 pub fn to_sql(&self) -> Vec<String> {
343 self.joins.iter().map(|j| j.to_sql()).collect()
344 }
345 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 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}