Skip to main content

reddb_server/storage/query/parser/
join.rs

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