Skip to main content

powdb_query/
sql.rs

1//! SQL frontend for PowDB.
2//!
3//! This module intentionally keeps SQL as a frontend: it parses a supported
4//! SQL subset, lowers it to PowDB's existing statement AST, and records the
5//! equivalent canonical PowQL text so plan-cache entries are shared with the
6//! native PowQL spelling.
7
8use crate::ast::Statement;
9use crate::parser::{self, ParseError};
10
11#[derive(Debug, Clone)]
12pub struct ParsedSql {
13    pub statement: Statement,
14    pub canonical_powql: String,
15}
16
17pub fn parse_sql(input: &str) -> Result<Statement, ParseError> {
18    parse_sql_with_canonical(input).map(|p| p.statement)
19}
20
21pub fn parse_sql_with_canonical(input: &str) -> Result<ParsedSql, ParseError> {
22    let toks = lex_sql(input)?;
23    let mut p = SqlParser {
24        toks,
25        pos: 0,
26        depth: 0,
27    };
28    let canonical_powql = p.statement()?;
29    if !p.at_end() {
30        return Err(ParseError::Syntax {
31            message: format!(
32                "unexpected trailing SQL token: {}",
33                p.peek()
34                    .map(|t| t.display())
35                    .unwrap_or_else(|| "<eof>".into())
36            ),
37        });
38    }
39    let statement = parser::parse(&canonical_powql)?;
40    Ok(ParsedSql {
41        statement,
42        canonical_powql,
43    })
44}
45
46#[derive(Debug, Clone, PartialEq)]
47enum SqlTok {
48    Word(String),
49    Number(String),
50    String(String),
51    Symbol(char),
52    Op(String),
53    Param(String),
54}
55
56impl SqlTok {
57    fn display(&self) -> String {
58        match self {
59            SqlTok::Word(s) => s.clone(),
60            SqlTok::Number(s) => s.clone(),
61            SqlTok::String(s) => format!("'{s}'"),
62            SqlTok::Symbol(c) => c.to_string(),
63            SqlTok::Op(s) => s.clone(),
64            SqlTok::Param(s) => format!("${s}"),
65        }
66    }
67}
68
69fn lex_sql(input: &str) -> Result<Vec<SqlTok>, ParseError> {
70    let mut out = Vec::new();
71    let chars: Vec<char> = input.chars().collect();
72    let mut i = 0usize;
73    while i < chars.len() {
74        let c = chars[i];
75        if c.is_whitespace() {
76            i += 1;
77            continue;
78        }
79        if c == '-' && chars.get(i + 1) == Some(&'-') {
80            i += 2;
81            while i < chars.len() && chars[i] != '\n' {
82                i += 1;
83            }
84            continue;
85        }
86        if c == '/' && chars.get(i + 1) == Some(&'*') {
87            i += 2;
88            while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {
89                i += 1;
90            }
91            if i + 1 >= chars.len() {
92                return Err(ParseError::Lex {
93                    message: "unterminated block comment".into(),
94                    position: i,
95                });
96            }
97            i += 2;
98            continue;
99        }
100        if c == '\'' || c == '"' {
101            let quote = c;
102            i += 1;
103            let mut s = String::new();
104            while i < chars.len() {
105                if chars[i] == quote {
106                    if quote == '\'' && chars.get(i + 1) == Some(&'\'') {
107                        s.push('\'');
108                        i += 2;
109                        continue;
110                    }
111                    i += 1;
112                    break;
113                }
114                if chars[i] == '\\' && i + 1 < chars.len() {
115                    let next = chars[i + 1];
116                    match next {
117                        'n' => s.push('\n'),
118                        't' => s.push('\t'),
119                        other => s.push(other),
120                    }
121                    i += 2;
122                } else {
123                    s.push(chars[i]);
124                    i += 1;
125                }
126            }
127            if i > chars.len() || chars.get(i.saturating_sub(1)) != Some(&quote) {
128                return Err(ParseError::Lex {
129                    message: "unterminated string".into(),
130                    position: i,
131                });
132            }
133            out.push(SqlTok::String(s));
134            continue;
135        }
136        if c == '$' {
137            i += 1;
138            let start = i;
139            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
140                i += 1;
141            }
142            out.push(SqlTok::Param(chars[start..i].iter().collect()));
143            continue;
144        }
145        if c.is_ascii_digit() || (c == '-' && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit()))
146        {
147            let start = i;
148            i += 1;
149            while i < chars.len() && chars[i].is_ascii_digit() {
150                i += 1;
151            }
152            if i < chars.len()
153                && chars[i] == '.'
154                && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit())
155            {
156                i += 1;
157                while i < chars.len() && chars[i].is_ascii_digit() {
158                    i += 1;
159                }
160            }
161            out.push(SqlTok::Number(chars[start..i].iter().collect()));
162            continue;
163        }
164        if c.is_alphabetic() || c == '_' {
165            let start = i;
166            i += 1;
167            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
168                i += 1;
169            }
170            out.push(SqlTok::Word(chars[start..i].iter().collect()));
171            continue;
172        }
173        if matches!(c, '(' | ')' | ',' | '*' | '.') {
174            out.push(SqlTok::Symbol(c));
175            i += 1;
176            continue;
177        }
178        if matches!(c, '=' | '<' | '>' | '!') {
179            let mut op = String::new();
180            op.push(c);
181            if matches!(chars.get(i + 1), Some('=') | Some('>')) {
182                op.push(chars[i + 1]);
183                i += 2;
184            } else {
185                i += 1;
186            }
187            if op == "<>" {
188                op = "!=".into();
189            }
190            out.push(SqlTok::Op(op));
191            continue;
192        }
193        if matches!(c, '+' | '-' | '/') {
194            out.push(SqlTok::Op(c.to_string()));
195            i += 1;
196            continue;
197        }
198        return Err(ParseError::Lex {
199            message: format!("unexpected SQL character `{c}`"),
200            position: i,
201        });
202    }
203    Ok(out)
204}
205
206/// Bound on SQL expression-parser recursion. The from-scratch SQL pre-parser
207/// recurses on parentheses / `NOT` / operator right-hand sides before the
208/// canonical text is handed to the PowQL parser, so its own guard must match
209/// PowQL's `MAX_NESTING_DEPTH` (64). Without it, a deeply nested SQL string
210/// arriving over the wire overflows the stack and — with panic=abort — aborts
211/// the whole server process.
212const MAX_SQL_NESTING_DEPTH: usize = 64;
213
214struct SqlParser {
215    toks: Vec<SqlTok>,
216    pos: usize,
217    depth: usize,
218}
219
220impl SqlParser {
221    fn at_end(&self) -> bool {
222        self.pos >= self.toks.len()
223    }
224    fn peek(&self) -> Option<&SqlTok> {
225        self.toks.get(self.pos)
226    }
227    fn bump(&mut self) -> Option<SqlTok> {
228        let t = self.toks.get(self.pos).cloned();
229        if t.is_some() {
230            self.pos += 1;
231        }
232        t
233    }
234    fn is_kw(&self, kw: &str) -> bool {
235        matches!(self.peek(), Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case(kw))
236    }
237    fn eat_kw(&mut self, kw: &str) -> bool {
238        if self.is_kw(kw) {
239            self.pos += 1;
240            true
241        } else {
242            false
243        }
244    }
245    fn expect_kw(&mut self, kw: &str) -> Result<(), ParseError> {
246        if self.eat_kw(kw) {
247            Ok(())
248        } else {
249            Err(ParseError::UnexpectedToken {
250                expected: kw.into(),
251                got: self
252                    .peek()
253                    .map(|t| t.display())
254                    .unwrap_or_else(|| "<eof>".into()),
255            })
256        }
257    }
258    fn eat_sym(&mut self, c: char) -> bool {
259        if matches!(self.peek(), Some(SqlTok::Symbol(got)) if *got == c) {
260            self.pos += 1;
261            true
262        } else {
263            false
264        }
265    }
266    fn expect_sym(&mut self, c: char) -> Result<(), ParseError> {
267        if self.eat_sym(c) {
268            Ok(())
269        } else {
270            Err(ParseError::UnexpectedToken {
271                expected: c.to_string(),
272                got: self
273                    .peek()
274                    .map(|t| t.display())
275                    .unwrap_or_else(|| "<eof>".into()),
276            })
277        }
278    }
279    fn expect_ident(&mut self, what: &str) -> Result<String, ParseError> {
280        match self.bump() {
281            Some(SqlTok::Word(w)) if !is_reserved_identifier(&w) => Ok(w),
282            Some(SqlTok::Word(w)) => Err(ParseError::Syntax {
283                message: format!("expected {what}, got reserved word `{w}`"),
284            }),
285            Some(t) => Err(ParseError::UnexpectedToken {
286                expected: what.into(),
287                got: t.display(),
288            }),
289            None => Err(ParseError::UnexpectedToken {
290                expected: what.into(),
291                got: "<eof>".into(),
292            }),
293        }
294    }
295
296    fn statement(&mut self) -> Result<String, ParseError> {
297        if self.is_kw("select") {
298            self.select()
299        } else if self.is_kw("insert") {
300            self.insert()
301        } else if self.is_kw("update") {
302            self.update()
303        } else if self.is_kw("delete") {
304            self.delete()
305        } else if self.is_kw("create") {
306            self.create()
307        } else if self.is_kw("drop") {
308            self.drop_stmt()
309        } else if self.is_kw("alter") {
310            self.alter()
311        } else if self.eat_kw("begin") {
312            let _ = self.eat_kw("transaction");
313            Ok("begin".into())
314        } else if self.eat_kw("commit") {
315            Ok("commit".into())
316        } else if self.eat_kw("rollback") {
317            Ok("rollback".into())
318        } else {
319            Err(ParseError::UnexpectedToken {
320                expected: "SQL statement".into(),
321                got: self
322                    .peek()
323                    .map(|t| t.display())
324                    .unwrap_or_else(|| "<eof>".into()),
325            })
326        }
327    }
328
329    fn select(&mut self) -> Result<String, ParseError> {
330        self.expect_kw("select")?;
331        let distinct = self.eat_kw("distinct");
332        let projection = self.projection_list()?;
333        self.expect_kw("from")?;
334        let source = self.table_ref()?;
335        let mut joins = Vec::new();
336        while self.starts_join() {
337            joins.push(self.join_clause()?);
338        }
339        let filter = if self.eat_kw("where") {
340            Some(self.expr_until(&["group", "having", "order", "limit", "offset"])?)
341        } else {
342            None
343        };
344        let group = if self.eat_kw("group") {
345            self.expect_kw("by")?;
346            Some(self.field_list_until(&["having", "order", "limit", "offset"])?)
347        } else {
348            None
349        };
350        let having = if self.eat_kw("having") {
351            Some(self.expr_until(&["order", "limit", "offset"])?)
352        } else {
353            None
354        };
355        let order = if self.eat_kw("order") {
356            self.expect_kw("by")?;
357            Some(self.order_list_until(&["limit", "offset"])?)
358        } else {
359            None
360        };
361        let limit = if self.eat_kw("limit") {
362            Some(self.expr_until(&["offset"])?)
363        } else {
364            None
365        };
366        let offset = if self.eat_kw("offset") {
367            Some(self.expr_until(&[])?)
368        } else {
369            None
370        };
371
372        let mut out = source;
373        for j in joins {
374            out.push(' ');
375            out.push_str(&j);
376        }
377        if distinct {
378            out.push_str(" distinct");
379        }
380        if let Some(f) = filter {
381            out.push_str(" filter ");
382            out.push_str(&f);
383        }
384        if let Some(keys) = group {
385            out.push_str(" group ");
386            out.push_str(&keys.join(", "));
387            if let Some(h) = having {
388                out.push_str(" having ");
389                out.push_str(&h);
390            }
391        } else if having.is_some() {
392            return Err(ParseError::Syntax {
393                message: "HAVING requires GROUP BY".into(),
394            });
395        }
396        if let Some(o) = order {
397            out.push_str(" order ");
398            out.push_str(&o);
399        }
400        if let Some(l) = limit {
401            out.push_str(" limit ");
402            out.push_str(&l);
403        }
404        if let Some(o) = offset {
405            out.push_str(" offset ");
406            out.push_str(&o);
407        }
408        if let Some(p) = projection {
409            out.push_str(" { ");
410            out.push_str(&p.join(", "));
411            out.push_str(" }");
412        }
413        Ok(out)
414    }
415
416    fn projection_list(&mut self) -> Result<Option<Vec<String>>, ParseError> {
417        if self.eat_sym('*') {
418            return Ok(None);
419        }
420        let mut fields = Vec::new();
421        loop {
422            let expr = self.expr_until(&["from", "as"])?;
423            let field = if self.eat_kw("as") {
424                let alias = self.expect_ident("projection alias")?;
425                format!("{alias}: {expr}")
426            } else {
427                expr
428            };
429            fields.push(field);
430            if !self.eat_sym(',') {
431                break;
432            }
433        }
434        Ok(Some(fields))
435    }
436
437    fn table_ref(&mut self) -> Result<String, ParseError> {
438        let table = self.expect_ident("table name")?;
439        let has_alias = self.eat_kw("as")
440            || matches!(self.peek(), Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w));
441        if has_alias {
442            let alias = self.expect_ident("table alias")?;
443            Ok(format!("{table} as {alias}"))
444        } else {
445            Ok(table)
446        }
447    }
448
449    fn starts_join(&self) -> bool {
450        self.is_kw("join")
451            || self.is_kw("inner")
452            || self.is_kw("left")
453            || self.is_kw("right")
454            || self.is_kw("cross")
455    }
456
457    fn join_clause(&mut self) -> Result<String, ParseError> {
458        let kind = if self.eat_kw("inner") {
459            self.expect_kw("join")?;
460            "inner join"
461        } else if self.eat_kw("left") {
462            let _ = self.eat_kw("outer");
463            self.expect_kw("join")?;
464            "left join"
465        } else if self.eat_kw("right") {
466            let _ = self.eat_kw("outer");
467            self.expect_kw("join")?;
468            "right join"
469        } else if self.eat_kw("cross") {
470            self.expect_kw("join")?;
471            "cross join"
472        } else {
473            self.expect_kw("join")?;
474            "inner join"
475        };
476        let table = self.table_ref()?;
477        if kind == "cross join" {
478            return Ok(format!("{kind} {table}"));
479        }
480        self.expect_kw("on")?;
481        let on = self.expr_until(&[
482            "join", "inner", "left", "right", "cross", "where", "group", "having", "order",
483            "limit", "offset",
484        ])?;
485        Ok(format!("{kind} {table} on {on}"))
486    }
487
488    fn insert(&mut self) -> Result<String, ParseError> {
489        self.expect_kw("insert")?;
490        self.expect_kw("into")?;
491        let table = self.expect_ident("table name")?;
492        self.expect_sym('(')?;
493        let mut cols = Vec::new();
494        loop {
495            cols.push(self.expect_ident("column name")?);
496            if !self.eat_sym(',') {
497                break;
498            }
499        }
500        self.expect_sym(')')?;
501        self.expect_kw("values")?;
502        let mut rows = Vec::new();
503        loop {
504            self.expect_sym('(')?;
505            let mut vals = Vec::new();
506            loop {
507                vals.push(self.expr_until(&[])?);
508                if !self.eat_sym(',') {
509                    break;
510                }
511            }
512            self.expect_sym(')')?;
513            if vals.len() != cols.len() {
514                return Err(ParseError::Syntax {
515                    message: format!(
516                        "INSERT has {} column(s) but {} value(s)",
517                        cols.len(),
518                        vals.len()
519                    ),
520                });
521            }
522            let assigns = cols
523                .iter()
524                .zip(vals)
525                .map(|(c, v)| format!("{c} := {v}"))
526                .collect::<Vec<_>>();
527            rows.push(format!("{{ {} }}", assigns.join(", ")));
528            if !self.eat_sym(',') {
529                break;
530            }
531        }
532        Ok(format!("insert {table} {}", rows.join(", ")))
533    }
534
535    fn update(&mut self) -> Result<String, ParseError> {
536        self.expect_kw("update")?;
537        let table = self.expect_ident("table name")?;
538        self.expect_kw("set")?;
539        let assigns = self.assignment_list_until(&["where"])?;
540        let filter = if self.eat_kw("where") {
541            Some(self.expr_until(&[])?)
542        } else {
543            None
544        };
545        let mut out = table;
546        if let Some(f) = filter {
547            out.push_str(" filter ");
548            out.push_str(&f);
549        }
550        out.push_str(" update { ");
551        out.push_str(&assigns.join(", "));
552        out.push_str(" }");
553        Ok(out)
554    }
555
556    fn delete(&mut self) -> Result<String, ParseError> {
557        self.expect_kw("delete")?;
558        self.expect_kw("from")?;
559        let table = self.expect_ident("table name")?;
560        let filter = if self.eat_kw("where") {
561            Some(self.expr_until(&[])?)
562        } else {
563            None
564        };
565        let mut out = table;
566        if let Some(f) = filter {
567            out.push_str(" filter ");
568            out.push_str(&f);
569        }
570        out.push_str(" delete");
571        Ok(out)
572    }
573
574    fn create(&mut self) -> Result<String, ParseError> {
575        self.expect_kw("create")?;
576        if self.eat_kw("table") {
577            let table = self.expect_ident("table name")?;
578            self.expect_sym('(')?;
579            let mut fields = Vec::new();
580            while !self.eat_sym(')') {
581                if self.is_kw("primary") || self.is_kw("foreign") || self.is_kw("constraint") {
582                    return Err(ParseError::Unsupported { feature: "SQL table constraints are not supported; declare UNIQUE columns or add indexes explicitly".into() });
583                }
584                let name = self.expect_ident("column name")?;
585                let ty = self.sql_type()?;
586                let mut required = false;
587                let mut unique = false;
588                loop {
589                    if self.eat_kw("not") {
590                        self.expect_kw("null")?;
591                        required = true;
592                    } else if self.eat_kw("unique") {
593                        unique = true;
594                    } else if self.eat_kw("null") {
595                    } else {
596                        break;
597                    }
598                }
599                let mut mods = Vec::new();
600                if required {
601                    mods.push("required");
602                }
603                if unique {
604                    mods.push("unique");
605                }
606                let prefix = if mods.is_empty() {
607                    String::new()
608                } else {
609                    format!("{} ", mods.join(" "))
610                };
611                fields.push(format!("{prefix}{name}: {ty}"));
612                let _ = self.eat_sym(',');
613            }
614            return Ok(format!("type {table} {{ {} }}", fields.join(", ")));
615        }
616        let unique = self.eat_kw("unique");
617        self.expect_kw("index")?;
618        let _idx = self.expect_ident("index name")?;
619        self.expect_kw("on")?;
620        let table = self.expect_ident("table name")?;
621        self.expect_sym('(')?;
622        let col = self.expect_ident("column name")?;
623        self.expect_sym(')')?;
624        Ok(if unique {
625            format!("alter {table} add unique .{col}")
626        } else {
627            format!("alter {table} add index .{col}")
628        })
629    }
630
631    fn drop_stmt(&mut self) -> Result<String, ParseError> {
632        self.expect_kw("drop")?;
633        if self.eat_kw("table") {
634            let table = self.expect_ident("table name")?;
635            Ok(format!("drop {table}"))
636        } else if self.eat_kw("view") {
637            let view = self.expect_ident("view name")?;
638            Ok(format!("drop view {view}"))
639        } else {
640            Err(ParseError::UnexpectedToken {
641                expected: "TABLE or VIEW".into(),
642                got: self
643                    .peek()
644                    .map(|t| t.display())
645                    .unwrap_or_else(|| "<eof>".into()),
646            })
647        }
648    }
649
650    fn alter(&mut self) -> Result<String, ParseError> {
651        self.expect_kw("alter")?;
652        self.expect_kw("table")?;
653        let table = self.expect_ident("table name")?;
654        if self.eat_kw("add") {
655            let _ = self.eat_kw("column");
656            let name = self.expect_ident("column name")?;
657            let ty = self.sql_type()?;
658            let mut required = false;
659            if self.eat_kw("not") {
660                self.expect_kw("null")?;
661                required = true;
662            }
663            let prefix = if required { "required " } else { "" };
664            Ok(format!("alter {table} add column {prefix}{name}: {ty}"))
665        } else if self.eat_kw("drop") {
666            let _ = self.eat_kw("column");
667            let name = self.expect_ident("column name")?;
668            Ok(format!("alter {table} drop column {name}"))
669        } else {
670            Err(ParseError::UnexpectedToken {
671                expected: "ADD or DROP".into(),
672                got: self
673                    .peek()
674                    .map(|t| t.display())
675                    .unwrap_or_else(|| "<eof>".into()),
676            })
677        }
678    }
679
680    fn sql_type(&mut self) -> Result<String, ParseError> {
681        let raw = self.expect_ident("type name")?;
682        // Ignore VARCHAR(255)-style length specifiers.
683        if self.eat_sym('(') {
684            while !self.eat_sym(')') {
685                if self.at_end() {
686                    return Err(ParseError::Syntax {
687                        message: "unterminated SQL type length".into(),
688                    });
689                }
690                self.bump();
691            }
692        }
693        let ty = match raw.to_ascii_lowercase().as_str() {
694            "text" | "varchar" | "char" | "string" | "str" => "str",
695            "int" | "integer" | "bigint" | "smallint" => "int",
696            "real" | "double" | "float" | "decimal" | "numeric" => "float",
697            "bool" | "boolean" => "bool",
698            "datetime" | "timestamp" => "datetime",
699            "uuid" => "uuid",
700            "blob" | "bytes" => "bytes",
701            other => {
702                return Err(ParseError::Unsupported {
703                    feature: format!("unsupported SQL type `{other}`"),
704                })
705            }
706        };
707        Ok(ty.into())
708    }
709
710    fn assignment_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
711        let mut out = Vec::new();
712        loop {
713            let name = self.expect_ident("column name")?;
714            match self.bump() {
715                Some(SqlTok::Op(op)) if op == "=" => {}
716                Some(t) => {
717                    return Err(ParseError::UnexpectedToken {
718                        expected: "=".into(),
719                        got: t.display(),
720                    })
721                }
722                None => {
723                    return Err(ParseError::UnexpectedToken {
724                        expected: "=".into(),
725                        got: "<eof>".into(),
726                    })
727                }
728            }
729            let v = self.expr_until(stop)?;
730            out.push(format!("{name} := {v}"));
731            if !self.eat_sym(',') {
732                break;
733            }
734        }
735        Ok(out)
736    }
737
738    fn field_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
739        let mut fields = Vec::new();
740        loop {
741            fields.push(self.field_ref()?);
742            if !self.eat_sym(',') || self.next_is_stop(stop) {
743                break;
744            }
745        }
746        Ok(fields)
747    }
748
749    fn order_list_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
750        let mut parts = Vec::new();
751        loop {
752            let mut p = self.field_ref()?;
753            if self.eat_kw("desc") {
754                p.push_str(" desc");
755            } else if self.eat_kw("asc") {
756                p.push_str(" asc");
757            }
758            parts.push(p);
759            if !self.eat_sym(',') || self.next_is_stop(stop) {
760                break;
761            }
762        }
763        Ok(parts.join(", "))
764    }
765
766    fn field_ref(&mut self) -> Result<String, ParseError> {
767        let first = self.expect_ident("column name")?;
768        if self.eat_sym('.') {
769            let second = self.expect_ident("qualified column name")?;
770            Ok(format!("{first}.{second}"))
771        } else {
772            Ok(format!(".{first}"))
773        }
774    }
775
776    fn expr_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
777        self.expr_bp(0, stop)
778    }
779
780    fn expr_bp(&mut self, min_bp: u8, stop: &[&str]) -> Result<String, ParseError> {
781        // Guard the recursive descent against stack overflow. Error paths below
782        // abort the whole parse, so only the success path needs to restore the
783        // counter (done right before the final `Ok`).
784        self.depth += 1;
785        if self.depth > MAX_SQL_NESTING_DEPTH {
786            return Err(ParseError::NestingDepthExceeded {
787                max: MAX_SQL_NESTING_DEPTH,
788            });
789        }
790        let mut lhs = if self.eat_kw("not") {
791            // Standard SQL: `NOT` binds looser than comparison, so `NOT x = 1`
792            // is `NOT (x = 1)`. Parse the comparison (min_bp 5 admits `=`/`<`/…
793            // but stops before AND/OR) and parenthesize it so the canonical
794            // PowQL re-parse is unambiguous regardless of PowQL's own NOT
795            // precedence.
796            format!("not ({})", self.expr_bp(5, stop)?)
797        } else if self.eat_kw("exists") {
798            if self.eat_sym('(') {
799                if self.is_kw("select") {
800                    return Err(ParseError::Unsupported {
801                        feature:
802                            "SQL EXISTS subqueries are not supported yet; use PowQL EXISTS for now"
803                                .into(),
804                    });
805                }
806                return Err(ParseError::Syntax {
807                    message: "expected subquery after EXISTS".into(),
808                });
809            }
810            return Err(ParseError::Syntax {
811                message: "expected EXISTS (...)".into(),
812            });
813        } else if self.eat_sym('(') {
814            if self.is_kw("select") {
815                return Err(ParseError::Unsupported {
816                    feature:
817                        "SQL scalar subqueries are not supported yet; use PowQL subqueries for now"
818                            .into(),
819                });
820            }
821            let inner = self.expr_bp(0, stop)?;
822            self.expect_sym(')')?;
823            format!("({inner})")
824        } else {
825            self.primary_expr()?
826        };
827
828        loop {
829            if self.next_is_stop(stop)
830                || self.at_end()
831                || matches!(self.peek(), Some(SqlTok::Symbol(')' | ',')))
832            {
833                break;
834            }
835            if self.eat_kw("is") {
836                let not = self.eat_kw("not");
837                self.expect_kw("null")?;
838                lhs = if not {
839                    format!("{lhs} != null")
840                } else {
841                    format!("{lhs} = null")
842                };
843                continue;
844            }
845            if self.eat_kw("not") {
846                if self.eat_kw("in") {
847                    return Err(ParseError::Unsupported {
848                        feature:
849                            "SQL IN lists/subqueries are not supported yet in the SQL frontend"
850                                .into(),
851                    });
852                }
853                if self.eat_kw("like") {
854                    let rhs = self.expr_bp(6, stop)?;
855                    lhs = format!("{lhs} not like {rhs}");
856                    continue;
857                }
858                if self.eat_kw("between") {
859                    return Err(ParseError::Unsupported {
860                        feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
861                    });
862                }
863                return Err(ParseError::UnexpectedToken {
864                    expected: "IN, LIKE, or BETWEEN after NOT".into(),
865                    got: self
866                        .peek()
867                        .map(|t| t.display())
868                        .unwrap_or_else(|| "<eof>".into()),
869                });
870            }
871            if self.eat_kw("in") {
872                return Err(ParseError::Unsupported {
873                    feature: "SQL IN lists/subqueries are not supported yet in the SQL frontend"
874                        .into(),
875                });
876            }
877            if self.eat_kw("between") {
878                return Err(ParseError::Unsupported {
879                    feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
880                });
881            }
882            if self.eat_kw("like") {
883                let (l_bp, r_bp) = (5, 6);
884                if l_bp < min_bp {
885                    self.pos -= 1;
886                    break;
887                }
888                let rhs = self.expr_bp(r_bp, stop)?;
889                lhs = format!("{lhs} like {rhs}");
890                continue;
891            }
892
893            let op = if self.eat_kw("or") {
894                "or".to_string()
895            } else if self.eat_kw("and") {
896                "and".to_string()
897            } else if let Some(SqlTok::Op(op)) = self.peek().cloned() {
898                self.pos += 1;
899                op
900            } else if self.eat_sym('*') {
901                "*".into()
902            } else {
903                break;
904            };
905            let (l_bp, r_bp) = infix_bp(&op).ok_or_else(|| ParseError::Syntax {
906                message: format!("unsupported SQL operator `{op}`"),
907            })?;
908            if l_bp < min_bp {
909                self.pos -= 1;
910                break;
911            }
912            let rhs = self.expr_bp(r_bp, stop)?;
913            lhs = format!("{lhs} {op} {rhs}");
914        }
915        self.depth -= 1;
916        Ok(lhs)
917    }
918
919    fn primary_expr(&mut self) -> Result<String, ParseError> {
920        match self.bump() {
921            Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("null") => Ok("null".into()),
922            Some(SqlTok::Word(w))
923                if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
924            {
925                Ok(w.to_ascii_lowercase())
926            }
927            Some(SqlTok::Word(w)) => {
928                if self.eat_sym('(') {
929                    let func = w.to_ascii_lowercase();
930                    if func == "count" && self.eat_sym('*') {
931                        self.expect_sym(')')?;
932                        return Ok("count(*)".into());
933                    }
934                    let mut args = Vec::new();
935                    while !self.eat_sym(')') {
936                        args.push(self.expr_bp(0, &[])?);
937                        let _ = self.eat_sym(',');
938                    }
939                    return Ok(format!("{}({})", func, args.join(", ")));
940                }
941                if self.eat_sym('.') {
942                    let f = self.expect_ident("qualified column name")?;
943                    Ok(format!("{w}.{f}"))
944                } else {
945                    Ok(format!(".{w}"))
946                }
947            }
948            Some(SqlTok::Number(n)) => Ok(n),
949            Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
950            Some(SqlTok::Param(p)) => Ok(format!("${p}")),
951            Some(SqlTok::Symbol('*')) => Ok("*".into()),
952            Some(t) => Err(ParseError::Syntax {
953                message: format!("unexpected SQL token in expression: {}", t.display()),
954            }),
955            None => Err(ParseError::UnexpectedToken {
956                expected: "expression".into(),
957                got: "<eof>".into(),
958            }),
959        }
960    }
961
962    fn next_is_stop(&self, stop: &[&str]) -> bool {
963        matches!(self.peek(), Some(SqlTok::Word(w)) if stop.iter().any(|kw| w.eq_ignore_ascii_case(kw)))
964    }
965}
966
967fn infix_bp(op: &str) -> Option<(u8, u8)> {
968    Some(match op.to_ascii_lowercase().as_str() {
969        "or" => (1, 2),
970        "and" => (3, 4),
971        "=" | "!=" | "<" | ">" | "<=" | ">=" => (5, 6),
972        "+" | "-" => (7, 8),
973        "*" | "/" => (9, 10),
974        _ => return None,
975    })
976}
977
978fn quote_powql_string(s: &str) -> String {
979    format!(
980        "\"{}\"",
981        s.replace('\\', "\\\\")
982            .replace('"', "\\\"")
983            .replace('\n', "\\n")
984            .replace('\t', "\\t")
985    )
986}
987
988fn is_clause_kw(w: &str) -> bool {
989    matches!(
990        w.to_ascii_lowercase().as_str(),
991        "where"
992            | "group"
993            | "having"
994            | "order"
995            | "limit"
996            | "offset"
997            | "join"
998            | "inner"
999            | "left"
1000            | "right"
1001            | "cross"
1002            | "on"
1003            | "values"
1004            | "set"
1005    )
1006}
1007fn is_join_modifier(w: &str) -> bool {
1008    matches!(
1009        w.to_ascii_lowercase().as_str(),
1010        "join" | "inner" | "left" | "right" | "cross" | "outer"
1011    )
1012}
1013fn is_reserved_identifier(w: &str) -> bool {
1014    matches!(
1015        w.to_ascii_lowercase().as_str(),
1016        "select"
1017            | "from"
1018            | "where"
1019            | "insert"
1020            | "into"
1021            | "values"
1022            | "update"
1023            | "set"
1024            | "delete"
1025            | "create"
1026            | "table"
1027            | "drop"
1028            | "alter"
1029    )
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use super::*;
1035
1036    #[test]
1037    fn select_lowers_to_powql_ast() {
1038        let sql = parse_sql_with_canonical(
1039            "SELECT name, age FROM User WHERE age > 25 ORDER BY age DESC LIMIT 10",
1040        )
1041        .unwrap();
1042        assert_eq!(
1043            sql.canonical_powql,
1044            "User filter .age > 25 order .age desc limit 10 { .name, .age }"
1045        );
1046        assert_eq!(
1047            sql.statement,
1048            parser::parse("User filter .age > 25 order .age desc limit 10 { .name, .age }")
1049                .unwrap()
1050        );
1051    }
1052
1053    #[test]
1054    fn insert_update_delete_and_ddl_lower_to_existing_ast() {
1055        assert!(matches!(
1056            parse_sql("CREATE TABLE User (id INTEGER NOT NULL UNIQUE, name TEXT)").unwrap(),
1057            Statement::CreateType(_)
1058        ));
1059        assert!(matches!(
1060            parse_sql("INSERT INTO User (id, name) VALUES (1, 'Ada')").unwrap(),
1061            Statement::Insert(_)
1062        ));
1063        assert!(matches!(
1064            parse_sql("UPDATE User SET name = 'Grace' WHERE id = 1").unwrap(),
1065            Statement::UpdateQuery(_)
1066        ));
1067        assert!(matches!(
1068            parse_sql("DELETE FROM User WHERE id = 1").unwrap(),
1069            Statement::DeleteQuery(_)
1070        ));
1071    }
1072
1073    #[test]
1074    fn unsupported_sql_gets_explicit_error() {
1075        let err = parse_sql("SELECT name FROM User WHERE id IN (SELECT user_id FROM Orders)")
1076            .unwrap_err();
1077        assert!(err.to_string().contains("SQL IN"));
1078    }
1079}