Skip to main content

sql_cli/sql/parser/expressions/
comparison.rs

1// Comparison expression parsing
2// Handles comparison operators, BETWEEN, IN/NOT IN, LIKE, IS NULL/IS NOT NULL
3
4use crate::sql::parser::ast::SqlExpression;
5use crate::sql::parser::lexer::Token;
6use tracing::debug;
7
8use super::{log_parse_decision, trace_parse_entry, trace_parse_exit};
9
10/// Parse a comparison expression
11/// This handles comparison operators and special SQL operators
12pub fn parse_comparison<P>(parser: &mut P) -> Result<SqlExpression, String>
13where
14    P: ParseComparison + ?Sized,
15{
16    trace_parse_entry("parse_comparison", parser.current_token());
17
18    let mut left = parser.parse_additive()?;
19
20    // Handle BETWEEN operator
21    if matches!(parser.current_token(), Token::Between) {
22        debug!("BETWEEN operator detected");
23        log_parse_decision(
24            "parse_comparison",
25            parser.current_token(),
26            "BETWEEN operator - parsing range bounds",
27        );
28
29        parser.advance(); // consume BETWEEN
30        let lower = parser.parse_additive()?;
31        parser.consume(Token::And)?; // BETWEEN requires AND
32        let upper = parser.parse_additive()?;
33
34        let result = Ok(SqlExpression::Between {
35            expr: Box::new(left),
36            lower: Box::new(lower),
37            upper: Box::new(upper),
38        });
39        trace_parse_exit("parse_comparison", &result);
40        return result;
41    }
42
43    // Handle NOT IN operator
44    if matches!(parser.current_token(), Token::Not) {
45        // Peek ahead to see if this is NOT IN
46        parser.advance(); // consume NOT
47        if matches!(parser.current_token(), Token::In) {
48            debug!("NOT IN operator detected");
49            log_parse_decision(
50                "parse_comparison",
51                parser.current_token(),
52                "NOT IN operator - parsing value list",
53            );
54
55            parser.advance(); // consume IN
56            parser.consume(Token::LeftParen)?;
57
58            // Check if this is a subquery (starts with SELECT, or WITH for a
59            // CTE in expression position — P12).
60            if matches!(parser.current_token(), Token::Select | Token::With) {
61                debug!("Detected NOT IN subquery");
62                let subquery = parser.parse_subquery()?;
63                parser.consume(Token::RightParen)?;
64
65                let result = Ok(SqlExpression::NotInSubquery {
66                    expr: Box::new(left),
67                    subquery: Box::new(subquery),
68                });
69                trace_parse_exit("parse_comparison", &result);
70                return result;
71            } else {
72                // Regular NOT IN with value list
73                let values = parser.parse_expression_list()?;
74                parser.consume(Token::RightParen)?;
75
76                let result = Ok(SqlExpression::NotInList {
77                    expr: Box::new(left),
78                    values,
79                });
80                trace_parse_exit("parse_comparison", &result);
81                return result;
82            }
83        } else {
84            return Err("Expected IN after NOT".to_string());
85        }
86    }
87
88    // Handle IN operator (P29/P30).
89    //
90    // IN binds at the comparison level, exactly like the NOT IN form above.
91    // It used to be applied at the top of `parse_expression`, *outside* the
92    // OR/AND hierarchy, which mis-parsed both operand orders:
93    //   `a = 1 AND b IN (..)` became InList{ expr: (a = 1 AND b), .. } -> 0 rows
94    //   `b IN (..) AND a = 1` left `AND a = 1` unconsumed -> parse error
95    if matches!(parser.current_token(), Token::In) {
96        let result = parse_in_operator(parser, left);
97        trace_parse_exit("parse_comparison", &result);
98        return result;
99    }
100
101    // Handle IS NULL / IS NOT NULL
102    if matches!(parser.current_token(), Token::Is) {
103        parser.advance(); // consume IS
104
105        if matches!(parser.current_token(), Token::Not) {
106            parser.advance(); // consume NOT
107            if matches!(parser.current_token(), Token::Null) {
108                debug!("IS NOT NULL operator detected");
109                log_parse_decision(
110                    "parse_comparison",
111                    parser.current_token(),
112                    "IS NOT NULL operator",
113                );
114
115                parser.advance(); // consume NULL
116                left = SqlExpression::BinaryOp {
117                    left: Box::new(left),
118                    op: "IS NOT NULL".to_string(),
119                    right: Box::new(SqlExpression::Null),
120                };
121            } else {
122                return Err("Expected NULL after IS NOT".to_string());
123            }
124        } else if matches!(parser.current_token(), Token::Null) {
125            debug!("IS NULL operator detected");
126            log_parse_decision(
127                "parse_comparison",
128                parser.current_token(),
129                "IS NULL operator",
130            );
131
132            parser.advance(); // consume NULL
133            left = SqlExpression::BinaryOp {
134                left: Box::new(left),
135                op: "IS NULL".to_string(),
136                right: Box::new(SqlExpression::Null),
137            };
138        } else {
139            return Err("Expected NULL or NOT after IS".to_string());
140        }
141    }
142    // Handle comparison operators
143    else if let Some(op) = get_comparison_op(parser.current_token()) {
144        log_parse_decision(
145            "parse_comparison",
146            parser.current_token(),
147            &format!("Comparison operator '{}' found", op),
148        );
149
150        debug!(operator = %op, "Processing comparison operator");
151
152        parser.advance();
153        let right = parser.parse_additive()?;
154        left = SqlExpression::BinaryOp {
155            left: Box::new(left),
156            op,
157            right: Box::new(right),
158        };
159    }
160
161    let result = Ok(left);
162    trace_parse_exit("parse_comparison", &result);
163    result
164}
165
166/// Parse an expression that may contain IN operator
167/// This is called from parse_expression to handle IN after other comparisons
168pub fn parse_in_operator<P>(parser: &mut P, expr: SqlExpression) -> Result<SqlExpression, String>
169where
170    P: ParseComparison + ?Sized,
171{
172    trace_parse_entry("parse_in_operator", parser.current_token());
173
174    if matches!(parser.current_token(), Token::In) {
175        debug!("IN operator detected");
176        log_parse_decision(
177            "parse_in_operator",
178            parser.current_token(),
179            "IN operator - parsing value list",
180        );
181
182        parser.advance(); // consume IN
183        parser.consume(Token::LeftParen)?;
184
185        // Check if this is a subquery (starts with SELECT, or WITH for a CTE
186        // in expression position — P12).
187        if matches!(parser.current_token(), Token::Select | Token::With) {
188            debug!("Detected IN subquery");
189            let subquery = parser.parse_subquery()?;
190            parser.consume(Token::RightParen)?;
191
192            let result = Ok(SqlExpression::InSubquery {
193                expr: Box::new(expr),
194                subquery: Box::new(subquery),
195            });
196            trace_parse_exit("parse_in_operator", &result);
197            return result;
198        } else {
199            // Regular IN with value list
200            let values = parser.parse_expression_list()?;
201            parser.consume(Token::RightParen)?;
202
203            let result = Ok(SqlExpression::InList {
204                expr: Box::new(expr),
205                values,
206            });
207            trace_parse_exit("parse_in_operator", &result);
208            return result;
209        }
210    } else {
211        Ok(expr)
212    }
213}
214
215/// Get comparison operator from token
216fn get_comparison_op(token: &Token) -> Option<String> {
217    match token {
218        Token::Equal => Some("=".to_string()),
219        Token::NotEqual => Some("!=".to_string()),
220        Token::LessThan => Some("<".to_string()),
221        Token::GreaterThan => Some(">".to_string()),
222        Token::LessThanOrEqual => Some("<=".to_string()),
223        Token::GreaterThanOrEqual => Some(">=".to_string()),
224        Token::Like => Some("LIKE".to_string()),
225        Token::ILike => Some("ILIKE".to_string()),
226        _ => None,
227    }
228}
229
230/// Trait that parsers must implement to use comparison expression parsing
231pub trait ParseComparison {
232    fn current_token(&self) -> &Token;
233    fn advance(&mut self);
234    fn consume(&mut self, expected: Token) -> Result<(), String>;
235
236    // These methods are called from comparison parsing
237    fn parse_primary(&mut self) -> Result<SqlExpression, String>;
238    fn parse_additive(&mut self) -> Result<SqlExpression, String>;
239    fn parse_expression_list(&mut self) -> Result<Vec<SqlExpression>, String>;
240
241    // For subquery parsing (without parenthesis balance validation)
242    fn parse_subquery(&mut self) -> Result<crate::sql::parser::ast::SelectStatement, String>;
243}