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        let mut out = format!("insert {table} {}", rows.join(", "));
533        if self.returning_clause()? {
534            out.push_str(" returning");
535        }
536        Ok(out)
537    }
538
539    fn update(&mut self) -> Result<String, ParseError> {
540        self.expect_kw("update")?;
541        let table = self.expect_ident("table name")?;
542        self.expect_kw("set")?;
543        let assigns = self.assignment_list_until(&["where", "returning"])?;
544        let filter = if self.eat_kw("where") {
545            Some(self.expr_until(&["returning"])?)
546        } else {
547            None
548        };
549        let mut out = table;
550        if let Some(f) = filter {
551            out.push_str(" filter ");
552            out.push_str(&f);
553        }
554        out.push_str(" update { ");
555        out.push_str(&assigns.join(", "));
556        out.push_str(" }");
557        if self.returning_clause()? {
558            out.push_str(" returning");
559        }
560        Ok(out)
561    }
562
563    fn delete(&mut self) -> Result<String, ParseError> {
564        self.expect_kw("delete")?;
565        self.expect_kw("from")?;
566        let table = self.expect_ident("table name")?;
567        let filter = if self.eat_kw("where") {
568            Some(self.expr_until(&["returning"])?)
569        } else {
570            None
571        };
572        let mut out = table;
573        if let Some(f) = filter {
574            out.push_str(" filter ");
575            out.push_str(&f);
576        }
577        out.push_str(" delete");
578        if self.returning_clause()? {
579            out.push_str(" returning");
580        }
581        Ok(out)
582    }
583
584    /// Parse a single literal following a column `DEFAULT`, rendered as PowQL
585    /// literal text. Only scalar literals are accepted (no expression
586    /// defaults), matching the PowQL `default` modifier.
587    fn default_literal(&mut self) -> Result<String, ParseError> {
588        match self.bump() {
589            Some(SqlTok::Number(n)) => Ok(n),
590            Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
591            Some(SqlTok::Word(w))
592                if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
593            {
594                Ok(w.to_ascii_lowercase())
595            }
596            other => Err(ParseError::Syntax {
597                message: format!(
598                    "DEFAULT requires a literal value, got {}",
599                    other.map(|t| t.display()).unwrap_or_else(|| "<eof>".into())
600                ),
601            }),
602        }
603    }
604
605    /// Parse an optional trailing `RETURNING *`, returning whether it was
606    /// present. PowQL's `returning` clause always yields every column, so a
607    /// projected `RETURNING a, b` is rejected rather than silently widened to
608    /// all columns.
609    fn returning_clause(&mut self) -> Result<bool, ParseError> {
610        if !self.eat_kw("returning") {
611            return Ok(false);
612        }
613        if !self.eat_sym('*') {
614            return Err(ParseError::Syntax {
615                message: "RETURNING currently supports only `RETURNING *` \
616                          (column projection is not yet supported)"
617                    .into(),
618            });
619        }
620        Ok(true)
621    }
622
623    fn create(&mut self) -> Result<String, ParseError> {
624        self.expect_kw("create")?;
625        if self.eat_kw("table") {
626            let table = self.expect_ident("table name")?;
627            self.expect_sym('(')?;
628            let mut fields = Vec::new();
629            while !self.eat_sym(')') {
630                if self.is_kw("primary") || self.is_kw("foreign") || self.is_kw("constraint") {
631                    return Err(ParseError::Unsupported { feature: "SQL table constraints are not supported; declare UNIQUE columns or add indexes explicitly".into() });
632                }
633                let name = self.expect_ident("column name")?;
634                let ty = self.sql_type()?;
635                let mut required = false;
636                let mut unique = false;
637                let mut auto = false;
638                let mut default: Option<String> = None;
639                loop {
640                    if self.eat_kw("not") {
641                        self.expect_kw("null")?;
642                        required = true;
643                    } else if self.eat_kw("unique") {
644                        unique = true;
645                    } else if self.eat_kw("autoincrement") || self.eat_kw("auto_increment") {
646                        auto = true;
647                    } else if self.eat_kw("default") {
648                        default = Some(self.default_literal()?);
649                    } else if self.eat_kw("null") {
650                    } else {
651                        break;
652                    }
653                }
654                let mut mods = Vec::new();
655                if required {
656                    mods.push("required");
657                }
658                if unique {
659                    mods.push("unique");
660                }
661                if auto {
662                    mods.push("auto");
663                }
664                let prefix = if mods.is_empty() {
665                    String::new()
666                } else {
667                    format!("{} ", mods.join(" "))
668                };
669                let suffix = match default {
670                    Some(lit) => format!(" default {lit}"),
671                    None => String::new(),
672                };
673                fields.push(format!("{prefix}{name}: {ty}{suffix}"));
674                let _ = self.eat_sym(',');
675            }
676            return Ok(format!("type {table} {{ {} }}", fields.join(", ")));
677        }
678        let unique = self.eat_kw("unique");
679        self.expect_kw("index")?;
680        let _idx = self.expect_ident("index name")?;
681        self.expect_kw("on")?;
682        let table = self.expect_ident("table name")?;
683        self.expect_sym('(')?;
684        let col = self.expect_ident("column name")?;
685        self.expect_sym(')')?;
686        Ok(if unique {
687            format!("alter {table} add unique .{col}")
688        } else {
689            format!("alter {table} add index .{col}")
690        })
691    }
692
693    fn drop_stmt(&mut self) -> Result<String, ParseError> {
694        self.expect_kw("drop")?;
695        if self.eat_kw("table") {
696            let table = self.expect_ident("table name")?;
697            Ok(format!("drop {table}"))
698        } else if self.eat_kw("view") {
699            let view = self.expect_ident("view name")?;
700            Ok(format!("drop view {view}"))
701        } else {
702            Err(ParseError::UnexpectedToken {
703                expected: "TABLE or VIEW".into(),
704                got: self
705                    .peek()
706                    .map(|t| t.display())
707                    .unwrap_or_else(|| "<eof>".into()),
708            })
709        }
710    }
711
712    fn alter(&mut self) -> Result<String, ParseError> {
713        self.expect_kw("alter")?;
714        self.expect_kw("table")?;
715        let table = self.expect_ident("table name")?;
716        if self.eat_kw("add") {
717            let _ = self.eat_kw("column");
718            let name = self.expect_ident("column name")?;
719            let ty = self.sql_type()?;
720            let mut required = false;
721            if self.eat_kw("not") {
722                self.expect_kw("null")?;
723                required = true;
724            }
725            let prefix = if required { "required " } else { "" };
726            Ok(format!("alter {table} add column {prefix}{name}: {ty}"))
727        } else if self.eat_kw("drop") {
728            let _ = self.eat_kw("column");
729            let name = self.expect_ident("column name")?;
730            Ok(format!("alter {table} drop column {name}"))
731        } else {
732            Err(ParseError::UnexpectedToken {
733                expected: "ADD or DROP".into(),
734                got: self
735                    .peek()
736                    .map(|t| t.display())
737                    .unwrap_or_else(|| "<eof>".into()),
738            })
739        }
740    }
741
742    fn sql_type(&mut self) -> Result<String, ParseError> {
743        let raw = self.expect_ident("type name")?;
744        // Ignore VARCHAR(255)-style length specifiers.
745        if self.eat_sym('(') {
746            while !self.eat_sym(')') {
747                if self.at_end() {
748                    return Err(ParseError::Syntax {
749                        message: "unterminated SQL type length".into(),
750                    });
751                }
752                self.bump();
753            }
754        }
755        let ty = match raw.to_ascii_lowercase().as_str() {
756            "text" | "varchar" | "char" | "string" | "str" => "str",
757            "int" | "integer" | "bigint" | "smallint" => "int",
758            "real" | "double" | "float" | "decimal" | "numeric" => "float",
759            "bool" | "boolean" => "bool",
760            "datetime" | "timestamp" => "datetime",
761            "uuid" => "uuid",
762            "blob" | "bytes" => "bytes",
763            other => {
764                return Err(ParseError::Unsupported {
765                    feature: format!("unsupported SQL type `{other}`"),
766                })
767            }
768        };
769        Ok(ty.into())
770    }
771
772    fn assignment_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
773        let mut out = Vec::new();
774        loop {
775            let name = self.expect_ident("column name")?;
776            match self.bump() {
777                Some(SqlTok::Op(op)) if op == "=" => {}
778                Some(t) => {
779                    return Err(ParseError::UnexpectedToken {
780                        expected: "=".into(),
781                        got: t.display(),
782                    })
783                }
784                None => {
785                    return Err(ParseError::UnexpectedToken {
786                        expected: "=".into(),
787                        got: "<eof>".into(),
788                    })
789                }
790            }
791            let v = self.expr_until(stop)?;
792            out.push(format!("{name} := {v}"));
793            if !self.eat_sym(',') {
794                break;
795            }
796        }
797        Ok(out)
798    }
799
800    fn field_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
801        let mut fields = Vec::new();
802        loop {
803            fields.push(self.field_ref()?);
804            if !self.eat_sym(',') || self.next_is_stop(stop) {
805                break;
806            }
807        }
808        Ok(fields)
809    }
810
811    fn order_list_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
812        let mut parts = Vec::new();
813        loop {
814            let mut p = self.field_ref()?;
815            if self.eat_kw("desc") {
816                p.push_str(" desc");
817            } else if self.eat_kw("asc") {
818                p.push_str(" asc");
819            }
820            parts.push(p);
821            if !self.eat_sym(',') || self.next_is_stop(stop) {
822                break;
823            }
824        }
825        Ok(parts.join(", "))
826    }
827
828    fn field_ref(&mut self) -> Result<String, ParseError> {
829        let first = self.expect_ident("column name")?;
830        if self.eat_sym('.') {
831            let second = self.expect_ident("qualified column name")?;
832            Ok(format!("{first}.{second}"))
833        } else {
834            Ok(format!(".{first}"))
835        }
836    }
837
838    fn expr_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
839        self.expr_bp(0, stop)
840    }
841
842    fn expr_bp(&mut self, min_bp: u8, stop: &[&str]) -> Result<String, ParseError> {
843        // Guard the recursive descent against stack overflow. Error paths below
844        // abort the whole parse, so only the success path needs to restore the
845        // counter (done right before the final `Ok`).
846        self.depth += 1;
847        if self.depth > MAX_SQL_NESTING_DEPTH {
848            return Err(ParseError::NestingDepthExceeded {
849                max: MAX_SQL_NESTING_DEPTH,
850            });
851        }
852        let mut lhs = if self.eat_kw("not") {
853            // Standard SQL: `NOT` binds looser than comparison, so `NOT x = 1`
854            // is `NOT (x = 1)`. Parse the comparison (min_bp 5 admits `=`/`<`/…
855            // but stops before AND/OR) and parenthesize it so the canonical
856            // PowQL re-parse is unambiguous regardless of PowQL's own NOT
857            // precedence.
858            format!("not ({})", self.expr_bp(5, stop)?)
859        } else if self.eat_kw("exists") {
860            if self.eat_sym('(') {
861                if self.is_kw("select") {
862                    return Err(ParseError::Unsupported {
863                        feature:
864                            "SQL EXISTS subqueries are not supported yet; use PowQL EXISTS for now"
865                                .into(),
866                    });
867                }
868                return Err(ParseError::Syntax {
869                    message: "expected subquery after EXISTS".into(),
870                });
871            }
872            return Err(ParseError::Syntax {
873                message: "expected EXISTS (...)".into(),
874            });
875        } else if self.eat_sym('(') {
876            if self.is_kw("select") {
877                return Err(ParseError::Unsupported {
878                    feature:
879                        "SQL scalar subqueries are not supported yet; use PowQL subqueries for now"
880                            .into(),
881                });
882            }
883            let inner = self.expr_bp(0, stop)?;
884            self.expect_sym(')')?;
885            format!("({inner})")
886        } else {
887            self.primary_expr()?
888        };
889
890        loop {
891            if self.next_is_stop(stop)
892                || self.at_end()
893                || matches!(self.peek(), Some(SqlTok::Symbol(')' | ',')))
894            {
895                break;
896            }
897            if self.eat_kw("is") {
898                let not = self.eat_kw("not");
899                self.expect_kw("null")?;
900                lhs = if not {
901                    format!("{lhs} != null")
902                } else {
903                    format!("{lhs} = null")
904                };
905                continue;
906            }
907            if self.eat_kw("not") {
908                if self.eat_kw("in") {
909                    return Err(ParseError::Unsupported {
910                        feature:
911                            "SQL IN lists/subqueries are not supported yet in the SQL frontend"
912                                .into(),
913                    });
914                }
915                if self.eat_kw("like") {
916                    let rhs = self.expr_bp(6, stop)?;
917                    lhs = format!("{lhs} not like {rhs}");
918                    continue;
919                }
920                if self.eat_kw("between") {
921                    return Err(ParseError::Unsupported {
922                        feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
923                    });
924                }
925                return Err(ParseError::UnexpectedToken {
926                    expected: "IN, LIKE, or BETWEEN after NOT".into(),
927                    got: self
928                        .peek()
929                        .map(|t| t.display())
930                        .unwrap_or_else(|| "<eof>".into()),
931                });
932            }
933            if self.eat_kw("in") {
934                return Err(ParseError::Unsupported {
935                    feature: "SQL IN lists/subqueries are not supported yet in the SQL frontend"
936                        .into(),
937                });
938            }
939            if self.eat_kw("between") {
940                return Err(ParseError::Unsupported {
941                    feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
942                });
943            }
944            if self.eat_kw("like") {
945                let (l_bp, r_bp) = (5, 6);
946                if l_bp < min_bp {
947                    self.pos -= 1;
948                    break;
949                }
950                let rhs = self.expr_bp(r_bp, stop)?;
951                lhs = format!("{lhs} like {rhs}");
952                continue;
953            }
954
955            let op = if self.eat_kw("or") {
956                "or".to_string()
957            } else if self.eat_kw("and") {
958                "and".to_string()
959            } else if let Some(SqlTok::Op(op)) = self.peek().cloned() {
960                self.pos += 1;
961                op
962            } else if self.eat_sym('*') {
963                "*".into()
964            } else {
965                break;
966            };
967            let (l_bp, r_bp) = infix_bp(&op).ok_or_else(|| ParseError::Syntax {
968                message: format!("unsupported SQL operator `{op}`"),
969            })?;
970            if l_bp < min_bp {
971                self.pos -= 1;
972                break;
973            }
974            let rhs = self.expr_bp(r_bp, stop)?;
975            lhs = format!("{lhs} {op} {rhs}");
976        }
977        self.depth -= 1;
978        Ok(lhs)
979    }
980
981    fn primary_expr(&mut self) -> Result<String, ParseError> {
982        match self.bump() {
983            Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("null") => Ok("null".into()),
984            Some(SqlTok::Word(w))
985                if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
986            {
987                Ok(w.to_ascii_lowercase())
988            }
989            Some(SqlTok::Word(w)) => {
990                if self.eat_sym('(') {
991                    let func = w.to_ascii_lowercase();
992                    if func == "count" && self.eat_sym('*') {
993                        self.expect_sym(')')?;
994                        return Ok("count(*)".into());
995                    }
996                    let mut args = Vec::new();
997                    while !self.eat_sym(')') {
998                        args.push(self.expr_bp(0, &[])?);
999                        let _ = self.eat_sym(',');
1000                    }
1001                    return Ok(format!("{}({})", func, args.join(", ")));
1002                }
1003                if self.eat_sym('.') {
1004                    let f = self.expect_ident("qualified column name")?;
1005                    Ok(format!("{w}.{f}"))
1006                } else {
1007                    Ok(format!(".{w}"))
1008                }
1009            }
1010            Some(SqlTok::Number(n)) => Ok(n),
1011            Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
1012            Some(SqlTok::Param(p)) => Ok(format!("${p}")),
1013            Some(SqlTok::Symbol('*')) => Ok("*".into()),
1014            Some(t) => Err(ParseError::Syntax {
1015                message: format!("unexpected SQL token in expression: {}", t.display()),
1016            }),
1017            None => Err(ParseError::UnexpectedToken {
1018                expected: "expression".into(),
1019                got: "<eof>".into(),
1020            }),
1021        }
1022    }
1023
1024    fn next_is_stop(&self, stop: &[&str]) -> bool {
1025        matches!(self.peek(), Some(SqlTok::Word(w)) if stop.iter().any(|kw| w.eq_ignore_ascii_case(kw)))
1026    }
1027}
1028
1029fn infix_bp(op: &str) -> Option<(u8, u8)> {
1030    Some(match op.to_ascii_lowercase().as_str() {
1031        "or" => (1, 2),
1032        "and" => (3, 4),
1033        "=" | "!=" | "<" | ">" | "<=" | ">=" => (5, 6),
1034        "+" | "-" => (7, 8),
1035        "*" | "/" => (9, 10),
1036        _ => return None,
1037    })
1038}
1039
1040fn quote_powql_string(s: &str) -> String {
1041    format!(
1042        "\"{}\"",
1043        s.replace('\\', "\\\\")
1044            .replace('"', "\\\"")
1045            .replace('\n', "\\n")
1046            .replace('\t', "\\t")
1047    )
1048}
1049
1050fn is_clause_kw(w: &str) -> bool {
1051    matches!(
1052        w.to_ascii_lowercase().as_str(),
1053        "where"
1054            | "group"
1055            | "having"
1056            | "order"
1057            | "limit"
1058            | "offset"
1059            | "join"
1060            | "inner"
1061            | "left"
1062            | "right"
1063            | "cross"
1064            | "on"
1065            | "values"
1066            | "set"
1067    )
1068}
1069fn is_join_modifier(w: &str) -> bool {
1070    matches!(
1071        w.to_ascii_lowercase().as_str(),
1072        "join" | "inner" | "left" | "right" | "cross" | "outer"
1073    )
1074}
1075fn is_reserved_identifier(w: &str) -> bool {
1076    matches!(
1077        w.to_ascii_lowercase().as_str(),
1078        "select"
1079            | "from"
1080            | "where"
1081            | "insert"
1082            | "into"
1083            | "values"
1084            | "update"
1085            | "set"
1086            | "delete"
1087            | "create"
1088            | "table"
1089            | "drop"
1090            | "alter"
1091    )
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096    use super::*;
1097
1098    #[test]
1099    fn select_lowers_to_powql_ast() {
1100        let sql = parse_sql_with_canonical(
1101            "SELECT name, age FROM User WHERE age > 25 ORDER BY age DESC LIMIT 10",
1102        )
1103        .unwrap();
1104        assert_eq!(
1105            sql.canonical_powql,
1106            "User filter .age > 25 order .age desc limit 10 { .name, .age }"
1107        );
1108        assert_eq!(
1109            sql.statement,
1110            parser::parse("User filter .age > 25 order .age desc limit 10 { .name, .age }")
1111                .unwrap()
1112        );
1113    }
1114
1115    #[test]
1116    fn insert_update_delete_and_ddl_lower_to_existing_ast() {
1117        assert!(matches!(
1118            parse_sql("CREATE TABLE User (id INTEGER NOT NULL UNIQUE, name TEXT)").unwrap(),
1119            Statement::CreateType(_)
1120        ));
1121        assert!(matches!(
1122            parse_sql("INSERT INTO User (id, name) VALUES (1, 'Ada')").unwrap(),
1123            Statement::Insert(_)
1124        ));
1125        assert!(matches!(
1126            parse_sql("UPDATE User SET name = 'Grace' WHERE id = 1").unwrap(),
1127            Statement::UpdateQuery(_)
1128        ));
1129        assert!(matches!(
1130            parse_sql("DELETE FROM User WHERE id = 1").unwrap(),
1131            Statement::DeleteQuery(_)
1132        ));
1133    }
1134
1135    #[test]
1136    fn unsupported_sql_gets_explicit_error() {
1137        let err = parse_sql("SELECT name FROM User WHERE id IN (SELECT user_id FROM Orders)")
1138            .unwrap_err();
1139        assert!(err.to_string().contains("SQL IN"));
1140    }
1141}