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