Skip to main content

reddb_rql/parser/
join.rs

1//! Join query parsing (FROM ... JOIN GRAPH/PATH/TABLE/VECTOR ...)
2
3use super::error::ParseError;
4use super::Parser;
5use crate::ast::{
6    FieldRef, GraphQuery, JoinCondition, JoinQuery, JoinType, Projection, QueryExpr, SelectItem,
7    TableQuery,
8};
9use crate::lexer::Token;
10use crate::sql_lowering::{filter_to_expr, projection_to_select_item};
11impl<'a> Parser<'a> {
12    /// Parse FROM ... JOIN (GRAPH / PATH / TABLE / VECTOR / HYBRID) query
13    pub fn parse_from_query(&mut self) -> Result<QueryExpr, ParseError> {
14        self.expect(Token::From)?;
15
16        // Fase 1.7 unlock: `FROM (SELECT … FROM t) AS alias` — subquery
17        // in FROM position. Detect by peeking LParen before falling
18        // through to the legacy identifier-only parse_table_source.
19        // The subquery branch builds the outer TableQuery via
20        // `TableQuery::from_subquery` which sets `source` to
21        // `TableSource::Subquery` and marks `table` with a sentinel
22        // `__subq_<alias>` so code that still reads `table.as_str()`
23        // errors loudly instead of silently mis-resolving.
24        let mut table_query = if self.check(&Token::LParen) {
25            self.advance()?; // consume `(`
26                             // Only SELECT is allowed in a FROM subquery — reject
27                             // `FROM (MATCH … RETURN)` and other non-SELECT shapes.
28            if !self.check(&Token::Select) {
29                return Err(ParseError::new(
30                    "subquery in FROM must start with SELECT".to_string(),
31                    self.position(),
32                ));
33            }
34            let inner = self.parse_select_query()?;
35            self.expect(Token::RParen)?;
36            let alias = if self.consume(&Token::As)?
37                || (self.check(&Token::Ident("".into())) && !self.is_join_keyword())
38            {
39                Some(self.expect_ident()?)
40            } else {
41                None
42            };
43            TableQuery::from_subquery(inner, alias)
44        } else {
45            // Parse table name and alias
46            let table = self.parse_table_source()?;
47            let alias = if self.consume(&Token::As)?
48                || (self.check(&Token::Ident("".into())) && !self.is_join_keyword())
49            {
50                Some(self.expect_ident()?)
51            } else {
52                None
53            };
54            TableQuery {
55                table,
56                source: None,
57                alias,
58                select_items: Vec::new(),
59                columns: Vec::new(),
60                where_expr: None,
61                filter: None,
62                group_by_exprs: Vec::new(),
63                group_by: Vec::new(),
64                having_expr: None,
65                having: None,
66                order_by: Vec::new(),
67                limit: None,
68                limit_param: None,
69                offset: None,
70                offset_param: None,
71                expand: None,
72                as_of: None,
73                sessionize: None,
74                distinct: false,
75            }
76        };
77
78        // Check for JOIN
79        if self.is_join_keyword() {
80            return self.parse_join_query(QueryExpr::Table(table_query));
81        }
82
83        // Parse optional WHERE clause
84        if self.consume(&Token::Where)? {
85            let filter = self.parse_filter()?;
86            table_query.where_expr = Some(filter_to_expr(&filter));
87            table_query.filter = Some(filter);
88        }
89
90        // Parse optional ORDER BY
91        if self.consume(&Token::Order)? {
92            self.expect(Token::By)?;
93            table_query.order_by = self.parse_order_by_list()?;
94        }
95
96        // Parse optional LIMIT/OFFSET
97        if self.consume(&Token::Limit)? {
98            table_query.limit = Some(self.parse_integer()? as u64);
99        }
100        if self.consume(&Token::Offset)? {
101            table_query.offset = Some(self.parse_integer()? as u64);
102        }
103
104        // Check for RETURN (shorthand for column selection)
105        if self.consume(&Token::Return)? {
106            let (select_items, columns) = self.parse_select_items_and_projections()?;
107            table_query.select_items = select_items;
108            table_query.columns = columns;
109        }
110
111        Ok(QueryExpr::Table(table_query))
112    }
113
114    /// Check if current token is a join keyword
115    pub fn is_join_keyword(&self) -> bool {
116        matches!(
117            self.peek(),
118            Token::Join | Token::Inner | Token::Left | Token::Right | Token::Full | Token::Cross
119        )
120    }
121
122    /// Parse JOIN query
123    pub(crate) fn parse_join_query(&mut self, left: QueryExpr) -> Result<QueryExpr, ParseError> {
124        // Parse join type
125        let join_type = if self.consume(&Token::Inner)? {
126            self.expect(Token::Join)?;
127            JoinType::Inner
128        } else if self.consume(&Token::Left)? {
129            self.consume(&Token::Outer)?;
130            self.expect(Token::Join)?;
131            JoinType::LeftOuter
132        } else if self.consume(&Token::Right)? {
133            self.consume(&Token::Outer)?;
134            self.expect(Token::Join)?;
135            JoinType::RightOuter
136        } else if self.consume(&Token::Full)? {
137            // `FULL JOIN` and `FULL OUTER JOIN` are aliases.
138            self.consume(&Token::Outer)?;
139            self.expect(Token::Join)?;
140            JoinType::FullOuter
141        } else if self.consume(&Token::Cross)? {
142            self.expect(Token::Join)?;
143            JoinType::Cross
144        } else {
145            self.expect(Token::Join)?;
146            JoinType::Inner
147        };
148
149        if self.consume(&Token::Graph)? {
150            return self.parse_graph_join_query(left, join_type);
151        }
152        if self.check(&Token::Path) {
153            return self.parse_path_join_query(left, join_type);
154        }
155        if self.check(&Token::Vector) {
156            return self.parse_vector_join_query(left, join_type);
157        }
158        if self.check(&Token::Hybrid) {
159            return self.parse_hybrid_join_query(left, join_type);
160        }
161
162        self.parse_table_join_query(left, join_type)
163    }
164
165    fn parse_graph_join_query(
166        &mut self,
167        left: QueryExpr,
168        join_type: JoinType,
169    ) -> Result<QueryExpr, ParseError> {
170        // Parse graph pattern
171        let pattern = self.parse_graph_pattern()?;
172        let alias = self.parse_join_rhs_alias()?;
173
174        // Parse ON condition
175        self.expect(Token::On)?;
176        let on = self.parse_graph_join_condition()?;
177        let (filter, order_by, limit, offset, return_items, return_) =
178            self.parse_join_post_clauses()?;
179        let graph_query = GraphQuery {
180            alias,
181            pattern,
182            filter: None,
183            return_: Vec::new(),
184            limit: None,
185        };
186
187        Ok(QueryExpr::Join(JoinQuery {
188            left: Box::new(left),
189            right: Box::new(QueryExpr::Graph(graph_query)),
190            join_type,
191            on,
192            filter,
193            order_by,
194            limit,
195            offset,
196            return_items,
197            return_,
198        }))
199    }
200
201    fn parse_table_join_query(
202        &mut self,
203        left: QueryExpr,
204        join_type: JoinType,
205    ) -> Result<QueryExpr, ParseError> {
206        let table = self.parse_table_source()?;
207        let alias = if self.consume(&Token::As)?
208            || (self.check(&Token::Ident("".into())) && !self.is_clause_keyword())
209        {
210            Some(self.expect_ident()?)
211        } else {
212            None
213        };
214
215        // CROSS JOIN has no ON clause — emit a sentinel JoinCondition
216        // that the runtime join loops treat as "always matches".
217        let on = if matches!(join_type, JoinType::Cross) {
218            cross_join_sentinel()
219        } else {
220            self.expect(Token::On)?;
221            self.parse_table_join_condition()?
222        };
223        let table_query = TableQuery {
224            table,
225            source: None,
226            alias,
227            select_items: Vec::new(),
228            columns: Vec::new(),
229            where_expr: None,
230            filter: None,
231            group_by_exprs: Vec::new(),
232            group_by: Vec::new(),
233            having_expr: None,
234            having: None,
235            order_by: Vec::new(),
236            limit: None,
237            limit_param: None,
238            offset: None,
239            offset_param: None,
240            expand: None,
241            as_of: None,
242            sessionize: None,
243            distinct: false,
244        };
245
246        let mut expr = QueryExpr::Join(JoinQuery {
247            left: Box::new(left),
248            right: Box::new(QueryExpr::Table(table_query)),
249            join_type,
250            on,
251            filter: None,
252            order_by: Vec::new(),
253            limit: None,
254            offset: None,
255            return_items: Vec::new(),
256            return_: Vec::new(),
257        });
258
259        if self.is_join_keyword() {
260            return self.parse_join_query(expr);
261        }
262
263        let (filter, order_by, limit, offset, _, return_) = self.parse_join_post_clauses()?;
264        let return_ = normalize_table_join_return_projections(return_);
265        let return_items = return_
266            .iter()
267            .filter_map(projection_to_select_item)
268            .collect();
269        if let QueryExpr::Join(join) = &mut expr {
270            join.filter = filter;
271            join.order_by = order_by;
272            join.limit = limit;
273            join.offset = offset;
274            join.return_items = return_items;
275            join.return_ = return_;
276        }
277        Ok(expr)
278    }
279
280    fn parse_vector_join_query(
281        &mut self,
282        left: QueryExpr,
283        join_type: JoinType,
284    ) -> Result<QueryExpr, ParseError> {
285        let mut right = match self.parse_vector_query()? {
286            QueryExpr::Vector(query) => query,
287            _ => unreachable!("vector parser must return QueryExpr::Vector"),
288        };
289        right.alias = self.parse_join_rhs_alias()?;
290        self.expect(Token::On)?;
291        let on = self.parse_table_join_condition()?;
292        let (filter, order_by, limit, offset, return_items, return_) =
293            self.parse_join_post_clauses()?;
294
295        Ok(QueryExpr::Join(JoinQuery {
296            left: Box::new(left),
297            right: Box::new(QueryExpr::Vector(right)),
298            join_type,
299            on,
300            filter,
301            order_by,
302            limit,
303            offset,
304            return_items,
305            return_,
306        }))
307    }
308
309    fn parse_path_join_query(
310        &mut self,
311        left: QueryExpr,
312        join_type: JoinType,
313    ) -> Result<QueryExpr, ParseError> {
314        let mut right = match self.parse_path_query()? {
315            QueryExpr::Path(query) => query,
316            _ => unreachable!("path parser must return QueryExpr::Path"),
317        };
318        right.alias = self.parse_join_rhs_alias()?;
319        self.expect(Token::On)?;
320        let on = self.parse_table_join_condition()?;
321        let (filter, order_by, limit, offset, return_items, return_) =
322            self.parse_join_post_clauses()?;
323
324        Ok(QueryExpr::Join(JoinQuery {
325            left: Box::new(left),
326            right: Box::new(QueryExpr::Path(right)),
327            join_type,
328            on,
329            filter,
330            order_by,
331            limit,
332            offset,
333            return_items,
334            return_,
335        }))
336    }
337
338    fn parse_hybrid_join_query(
339        &mut self,
340        left: QueryExpr,
341        join_type: JoinType,
342    ) -> Result<QueryExpr, ParseError> {
343        let mut right = match self.parse_hybrid_query()? {
344            QueryExpr::Hybrid(query) => query,
345            _ => unreachable!("hybrid parser must return QueryExpr::Hybrid"),
346        };
347        right.alias = self.parse_join_rhs_alias()?;
348        self.expect(Token::On)?;
349        let on = self.parse_table_join_condition()?;
350        let (filter, order_by, limit, offset, return_items, return_) =
351            self.parse_join_post_clauses()?;
352
353        Ok(QueryExpr::Join(JoinQuery {
354            left: Box::new(left),
355            right: Box::new(QueryExpr::Hybrid(right)),
356            join_type,
357            on,
358            filter,
359            order_by,
360            limit,
361            offset,
362            return_items,
363            return_,
364        }))
365    }
366
367    fn parse_join_post_clauses(
368        &mut self,
369    ) -> Result<
370        (
371            Option<crate::ast::Filter>,
372            Vec<crate::ast::OrderByClause>,
373            Option<u64>,
374            Option<u64>,
375            Vec<SelectItem>,
376            Vec<crate::ast::Projection>,
377        ),
378        ParseError,
379    > {
380        let mut filter = None;
381        let mut order_by = Vec::new();
382        let mut limit = None;
383        let mut offset = None;
384        let mut return_items = Vec::new();
385        let mut return_ = Vec::new();
386
387        loop {
388            if self.consume(&Token::Where)? {
389                filter = Some(self.parse_filter()?);
390            } else if self.consume(&Token::Order)? {
391                self.expect(Token::By)?;
392                order_by = self.parse_order_by_list()?;
393            } else if self.consume(&Token::Limit)? {
394                limit = Some(self.parse_integer()? as u64);
395            } else if self.consume(&Token::Offset)? {
396                offset = Some(self.parse_integer()? as u64);
397            } else if self.consume(&Token::Return)? {
398                return_ = self.parse_return_list()?;
399                return_items = return_
400                    .iter()
401                    .filter_map(projection_to_select_item)
402                    .collect();
403            } else {
404                break;
405            }
406        }
407
408        Ok((filter, order_by, limit, offset, return_items, return_))
409    }
410
411    fn parse_join_rhs_alias(&mut self) -> Result<Option<String>, ParseError> {
412        if self.consume(&Token::As)?
413            || (self.check(&Token::Ident("".into())) && !self.is_join_rhs_clause_keyword())
414        {
415            Ok(Some(self.expect_ident()?))
416        } else {
417            Ok(None)
418        }
419    }
420
421    fn is_join_rhs_clause_keyword(&self) -> bool {
422        matches!(
423            self.peek(),
424            Token::On
425                | Token::Where
426                | Token::Order
427                | Token::Limit
428                | Token::Offset
429                | Token::Return
430                | Token::Join
431                | Token::Inner
432                | Token::Left
433                | Token::Right
434        )
435    }
436
437    fn parse_table_source(&mut self) -> Result<String, ParseError> {
438        if self.consume(&Token::Star)? {
439            Ok("*".to_string())
440        } else if self.consume(&Token::All)? {
441            Ok("all".to_string())
442        } else {
443            self.expect_ident()
444        }
445    }
446
447    /// Parse join condition: table.col = node.prop
448    fn parse_graph_join_condition(&mut self) -> Result<JoinCondition, ParseError> {
449        let left_field = self.parse_field_ref()?;
450        self.expect(Token::Eq)?;
451        let right_first = self.expect_ident()?;
452        self.expect(Token::Dot)?;
453        let right_second = self.expect_ident()?;
454
455        // Try to determine if right side is node property or ID
456        let right_field = if right_second == "id" {
457            FieldRef::NodeId { alias: right_first }
458        } else {
459            FieldRef::NodeProperty {
460                alias: right_first,
461                property: right_second,
462            }
463        };
464
465        Ok(JoinCondition {
466            left_field,
467            right_field,
468        })
469    }
470
471    fn parse_table_join_condition(&mut self) -> Result<JoinCondition, ParseError> {
472        let left_field = self.parse_field_ref()?;
473        self.expect(Token::Eq)?;
474        let right_field = self.parse_field_ref()?;
475
476        Ok(JoinCondition {
477            left_field,
478            right_field,
479        })
480    }
481}
482
483fn normalize_table_join_return_projections(projections: Vec<Projection>) -> Vec<Projection> {
484    projections
485        .into_iter()
486        .map(|projection| match projection {
487            Projection::Field(FieldRef::NodeProperty { alias, property }, output_alias) => {
488                let output_alias = output_alias.or_else(|| Some(property.clone()));
489                Projection::Field(
490                    FieldRef::TableColumn {
491                        table: alias,
492                        column: property,
493                    },
494                    output_alias,
495                )
496            }
497            other => other,
498        })
499        .collect()
500}
501
502/// Sentinel JoinCondition used for CROSS JOIN — neither field references
503/// anything real. The runtime join loops must detect
504/// `join_type == JoinType::Cross` and skip predicate evaluation entirely
505/// rather than resolving these empty fields.
506fn cross_join_sentinel() -> JoinCondition {
507    JoinCondition {
508        left_field: FieldRef::TableColumn {
509            table: String::new(),
510            column: String::new(),
511        },
512        right_field: FieldRef::TableColumn {
513            table: String::new(),
514            column: String::new(),
515        },
516    }
517}