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
220/// One item in a SELECT projection, after lowering to canonical PowQL text.
221struct Projection {
222    /// Canonical PowQL for this item, e.g. `count(*)`, `sum(.x)`, `n: .x + 1`.
223    /// Used for the row/grouped projection path (`Table { ... }`).
224    text: String,
225    /// Set when the whole item is a single aggregate call. Drives the rewrite
226    /// of an ungrouped aggregate SELECT into PowQL's aggregate form
227    /// (`count(Table filter ...)`), which the row-projection path can't express.
228    agg: Option<AggCall>,
229}
230
231/// A standalone aggregate call in a projection (`count(*)`, `sum(x)`, ...).
232struct AggCall {
233    /// Lowercased function name: `count` | `sum` | `avg` | `min` | `max`.
234    func: String,
235    arg: AggArg,
236}
237
238enum AggArg {
239    /// `count(*)`.
240    Star,
241    /// `sum(x)` etc. — the lowered PowQL field reference (e.g. `.x`).
242    Field(String),
243}
244
245impl AggCall {
246    /// Canonical PowQL text for the grouped/row projection path.
247    fn canonical(&self) -> String {
248        match &self.arg {
249            AggArg::Star => format!("{}(*)", self.func),
250            AggArg::Field(f) => format!("{}({f})", self.func),
251        }
252    }
253}
254
255/// Lower a single ungrouped aggregate over `inner` (an already-lowered PowQL
256/// source pipeline, e.g. `T filter .x > 3`) into PowQL's aggregate form.
257/// `count(*)`/`count(col)` both count rows; the non-null nuance of SQL
258/// `count(col)` is not yet modeled. The other aggregates carry their column in
259/// a trailing PowQL projection (`sum(T { .x })`).
260fn build_ungrouped_aggregate(agg: &AggCall, inner: &str) -> Result<String, ParseError> {
261    match agg.func.as_str() {
262        "count" => Ok(format!("count({inner})")),
263        "sum" | "avg" | "min" | "max" => match &agg.arg {
264            AggArg::Field(f) => Ok(format!("{}({inner} {{ {f} }})", agg.func)),
265            AggArg::Star => Err(ParseError::Unsupported {
266                feature: format!("{0}(*) is not valid; {0}() needs a column", agg.func),
267            }),
268        },
269        // try_aggregate only constructs the five names above.
270        other => Err(ParseError::Syntax {
271            message: format!("unknown aggregate function `{other}`"),
272        }),
273    }
274}
275
276impl SqlParser {
277    fn at_end(&self) -> bool {
278        self.pos >= self.toks.len()
279    }
280    fn peek(&self) -> Option<&SqlTok> {
281        self.toks.get(self.pos)
282    }
283    fn bump(&mut self) -> Option<SqlTok> {
284        let t = self.toks.get(self.pos).cloned();
285        if t.is_some() {
286            self.pos += 1;
287        }
288        t
289    }
290    fn is_kw(&self, kw: &str) -> bool {
291        matches!(self.peek(), Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case(kw))
292    }
293    fn eat_kw(&mut self, kw: &str) -> bool {
294        if self.is_kw(kw) {
295            self.pos += 1;
296            true
297        } else {
298            false
299        }
300    }
301    fn expect_kw(&mut self, kw: &str) -> Result<(), ParseError> {
302        if self.eat_kw(kw) {
303            Ok(())
304        } else {
305            Err(ParseError::UnexpectedToken {
306                expected: kw.into(),
307                got: self
308                    .peek()
309                    .map(|t| t.display())
310                    .unwrap_or_else(|| "<eof>".into()),
311            })
312        }
313    }
314    fn eat_sym(&mut self, c: char) -> bool {
315        if matches!(self.peek(), Some(SqlTok::Symbol(got)) if *got == c) {
316            self.pos += 1;
317            true
318        } else {
319            false
320        }
321    }
322    fn expect_sym(&mut self, c: char) -> Result<(), ParseError> {
323        if self.eat_sym(c) {
324            Ok(())
325        } else {
326            Err(ParseError::UnexpectedToken {
327                expected: c.to_string(),
328                got: self
329                    .peek()
330                    .map(|t| t.display())
331                    .unwrap_or_else(|| "<eof>".into()),
332            })
333        }
334    }
335    fn expect_ident(&mut self, what: &str) -> Result<String, ParseError> {
336        match self.bump() {
337            Some(SqlTok::Word(w)) if !is_reserved_identifier(&w) => Ok(w),
338            Some(SqlTok::Word(w)) => Err(ParseError::Syntax {
339                message: format!("expected {what}, got reserved word `{w}`"),
340            }),
341            Some(t) => Err(ParseError::UnexpectedToken {
342                expected: what.into(),
343                got: t.display(),
344            }),
345            None => Err(ParseError::UnexpectedToken {
346                expected: what.into(),
347                got: "<eof>".into(),
348            }),
349        }
350    }
351
352    fn statement(&mut self) -> Result<String, ParseError> {
353        if self.is_kw("select") {
354            self.select()
355        } else if self.is_kw("insert") {
356            self.insert()
357        } else if self.is_kw("update") {
358            self.update()
359        } else if self.is_kw("delete") {
360            self.delete()
361        } else if self.is_kw("create") {
362            self.create()
363        } else if self.is_kw("drop") {
364            self.drop_stmt()
365        } else if self.is_kw("alter") {
366            self.alter()
367        } else if self.eat_kw("begin") {
368            let _ = self.eat_kw("transaction");
369            Ok("begin".into())
370        } else if self.eat_kw("commit") {
371            Ok("commit".into())
372        } else if self.eat_kw("rollback") {
373            Ok("rollback".into())
374        } else {
375            Err(ParseError::UnexpectedToken {
376                expected: "SQL statement".into(),
377                got: self
378                    .peek()
379                    .map(|t| t.display())
380                    .unwrap_or_else(|| "<eof>".into()),
381            })
382        }
383    }
384
385    fn select(&mut self) -> Result<String, ParseError> {
386        self.expect_kw("select")?;
387        let distinct = self.eat_kw("distinct");
388        let projection = self.projection_list()?;
389        self.expect_kw("from")?;
390        let source = self.table_ref()?;
391        let mut joins = Vec::new();
392        while self.starts_join() {
393            joins.push(self.join_clause()?);
394        }
395        let filter = if self.eat_kw("where") {
396            Some(self.expr_until(&["group", "having", "order", "limit", "offset"])?)
397        } else {
398            None
399        };
400        let group = if self.eat_kw("group") {
401            self.expect_kw("by")?;
402            Some(self.field_list_until(&["having", "order", "limit", "offset"])?)
403        } else {
404            None
405        };
406        let having = if self.eat_kw("having") {
407            Some(self.expr_until(&["order", "limit", "offset"])?)
408        } else {
409            None
410        };
411        let order = if self.eat_kw("order") {
412            self.expect_kw("by")?;
413            Some(self.order_list_until(&["limit", "offset"])?)
414        } else {
415            None
416        };
417        let limit = if self.eat_kw("limit") {
418            Some(self.expr_until(&["offset"])?)
419        } else {
420            None
421        };
422        let offset = if self.eat_kw("offset") {
423            Some(self.expr_until(&[])?)
424        } else {
425            None
426        };
427
428        let has_joins = !joins.is_empty();
429        let has_group = group.is_some();
430
431        let mut out = source;
432        for j in joins {
433            out.push(' ');
434            out.push_str(&j);
435        }
436        if distinct {
437            out.push_str(" distinct");
438        }
439        if let Some(f) = filter {
440            out.push_str(" filter ");
441            out.push_str(&f);
442        }
443        if let Some(keys) = group {
444            out.push_str(" group ");
445            out.push_str(&keys.join(", "));
446            if let Some(h) = having {
447                out.push_str(" having ");
448                out.push_str(&h);
449            }
450        } else if having.is_some() {
451            return Err(ParseError::Syntax {
452                message: "HAVING requires GROUP BY".into(),
453            });
454        }
455        if let Some(o) = order {
456            out.push_str(" order ");
457            out.push_str(&o);
458        }
459        if let Some(l) = limit {
460            out.push_str(" limit ");
461            out.push_str(&l);
462        }
463        if let Some(o) = offset {
464            out.push_str(" offset ");
465            out.push_str(&o);
466        }
467        if let Some(items) = projection {
468            // An ungrouped aggregate (`SELECT count(*) FROM t`) is not a row
469            // projection — PowQL expresses it as `count(t filter ...)`, which
470            // yields a scalar. Without this the SQL frontend lowered it to
471            // `t { count(*) }` and returned one null row per source row.
472            if !has_group && items.iter().any(|p| p.agg.is_some()) {
473                if has_joins || distinct {
474                    return Err(ParseError::Unsupported {
475                        feature: "aggregates over a join or with DISTINCT and no GROUP BY are not supported by the SQL frontend".into(),
476                    });
477                }
478                if items.len() != 1 {
479                    return Err(ParseError::Unsupported {
480                        feature: "multiple aggregates, or an aggregate mixed with plain columns, without GROUP BY are not supported; aggregate a single expression or add GROUP BY".into(),
481                    });
482                }
483                // SAFETY: len == 1 and the item is an aggregate (any() above).
484                let agg = items.into_iter().next().unwrap().agg.unwrap();
485                return build_ungrouped_aggregate(&agg, &out);
486            }
487            out.push_str(" { ");
488            out.push_str(
489                &items
490                    .iter()
491                    .map(|p| p.text.as_str())
492                    .collect::<Vec<_>>()
493                    .join(", "),
494            );
495            out.push_str(" }");
496        }
497        Ok(out)
498    }
499
500    fn projection_list(&mut self) -> Result<Option<Vec<Projection>>, ParseError> {
501        if self.eat_sym('*') {
502            return Ok(None);
503        }
504        let mut fields = Vec::new();
505        loop {
506            // Detect a standalone aggregate (`count(*)`, `sum(x)`) so an
507            // ungrouped aggregate SELECT can be rewritten into PowQL's aggregate
508            // form. Anything else (incl. an aggregate inside a larger
509            // expression) falls through to the generic expression lowering.
510            let (expr, agg) = match self.try_aggregate()? {
511                Some(a) => (a.canonical(), Some(a)),
512                None => (self.expr_until(&["from", "as"])?, None),
513            };
514            let text = if self.eat_kw("as") {
515                let alias = self.expect_ident("projection alias")?;
516                format!("{alias}: {expr}")
517            } else {
518                expr
519            };
520            fields.push(Projection { text, agg });
521            if !self.eat_sym(',') {
522                break;
523            }
524        }
525        Ok(Some(fields))
526    }
527
528    /// Parse a standalone aggregate call (`count(*)`, `count(x)`, `sum(x)`, ...)
529    /// when it is the *entire* projection item. Returns `None` (restoring the
530    /// cursor) for non-aggregates, `count(distinct ...)`, or an aggregate that
531    /// is only part of a larger expression — those take the generic path.
532    fn try_aggregate(&mut self) -> Result<Option<AggCall>, ParseError> {
533        let Some(SqlTok::Word(w)) = self.peek().cloned() else {
534            return Ok(None);
535        };
536        let func = w.to_ascii_lowercase();
537        if !matches!(func.as_str(), "count" | "sum" | "avg" | "min" | "max") {
538            return Ok(None);
539        }
540        let save = self.pos;
541        self.pos += 1; // consume the function name
542        if !self.eat_sym('(') {
543            self.pos = save;
544            return Ok(None);
545        }
546        let arg = if func == "count" && self.eat_sym('*') {
547            AggArg::Star
548        } else if func == "count" && self.is_kw("distinct") {
549            // count(distinct ...) has different semantics — let the generic path
550            // (which already understands `distinct`) handle it.
551            self.pos = save;
552            return Ok(None);
553        } else {
554            AggArg::Field(self.expr_bp(0, &[])?)
555        };
556        // Only an aggregate that fills the whole projection item is rewritable;
557        // otherwise (e.g. `count(*) + 1`) restore and reparse as an expression.
558        if self.eat_sym(')')
559            && (matches!(self.peek(), Some(SqlTok::Symbol(',')))
560                || self.is_kw("as")
561                || self.is_kw("from"))
562        {
563            Ok(Some(AggCall { func, arg }))
564        } else {
565            self.pos = save;
566            Ok(None)
567        }
568    }
569
570    fn table_ref(&mut self) -> Result<String, ParseError> {
571        let table = self.expect_ident("table name")?;
572        let has_alias = self.eat_kw("as")
573            || matches!(self.peek(), Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w));
574        if has_alias {
575            let alias = self.expect_ident("table alias")?;
576            Ok(format!("{table} as {alias}"))
577        } else {
578            Ok(table)
579        }
580    }
581
582    fn starts_join(&self) -> bool {
583        self.is_kw("join")
584            || self.is_kw("inner")
585            || self.is_kw("left")
586            || self.is_kw("right")
587            || self.is_kw("cross")
588    }
589
590    fn join_clause(&mut self) -> Result<String, ParseError> {
591        let kind = if self.eat_kw("inner") {
592            self.expect_kw("join")?;
593            "inner join"
594        } else if self.eat_kw("left") {
595            let _ = self.eat_kw("outer");
596            self.expect_kw("join")?;
597            "left join"
598        } else if self.eat_kw("right") {
599            let _ = self.eat_kw("outer");
600            self.expect_kw("join")?;
601            "right join"
602        } else if self.eat_kw("cross") {
603            self.expect_kw("join")?;
604            "cross join"
605        } else {
606            self.expect_kw("join")?;
607            "inner join"
608        };
609        let table = self.table_ref()?;
610        if kind == "cross join" {
611            return Ok(format!("{kind} {table}"));
612        }
613        self.expect_kw("on")?;
614        let on = self.expr_until(&[
615            "join", "inner", "left", "right", "cross", "where", "group", "having", "order",
616            "limit", "offset",
617        ])?;
618        Ok(format!("{kind} {table} on {on}"))
619    }
620
621    fn insert(&mut self) -> Result<String, ParseError> {
622        self.expect_kw("insert")?;
623        self.expect_kw("into")?;
624        let table = self.expect_ident("table name")?;
625        self.expect_sym('(')?;
626        let mut cols = Vec::new();
627        loop {
628            cols.push(self.expect_ident("column name")?);
629            if !self.eat_sym(',') {
630                break;
631            }
632        }
633        self.expect_sym(')')?;
634        self.expect_kw("values")?;
635        let mut rows = Vec::new();
636        loop {
637            self.expect_sym('(')?;
638            let mut vals = Vec::new();
639            loop {
640                vals.push(self.expr_until(&[])?);
641                if !self.eat_sym(',') {
642                    break;
643                }
644            }
645            self.expect_sym(')')?;
646            if vals.len() != cols.len() {
647                return Err(ParseError::Syntax {
648                    message: format!(
649                        "INSERT has {} column(s) but {} value(s)",
650                        cols.len(),
651                        vals.len()
652                    ),
653                });
654            }
655            let assigns = cols
656                .iter()
657                .zip(vals)
658                .map(|(c, v)| format!("{c} := {v}"))
659                .collect::<Vec<_>>();
660            rows.push(format!("{{ {} }}", assigns.join(", ")));
661            if !self.eat_sym(',') {
662                break;
663            }
664        }
665        let mut out = format!("insert {table} {}", rows.join(", "));
666        if self.returning_clause()? {
667            out.push_str(" returning");
668        }
669        Ok(out)
670    }
671
672    fn update(&mut self) -> Result<String, ParseError> {
673        self.expect_kw("update")?;
674        let table = self.expect_ident("table name")?;
675        self.expect_kw("set")?;
676        let assigns = self.assignment_list_until(&["where", "returning"])?;
677        let filter = if self.eat_kw("where") {
678            Some(self.expr_until(&["returning"])?)
679        } else {
680            None
681        };
682        let mut out = table;
683        if let Some(f) = filter {
684            out.push_str(" filter ");
685            out.push_str(&f);
686        }
687        out.push_str(" update { ");
688        out.push_str(&assigns.join(", "));
689        out.push_str(" }");
690        if self.returning_clause()? {
691            out.push_str(" returning");
692        }
693        Ok(out)
694    }
695
696    fn delete(&mut self) -> Result<String, ParseError> {
697        self.expect_kw("delete")?;
698        self.expect_kw("from")?;
699        let table = self.expect_ident("table name")?;
700        let filter = if self.eat_kw("where") {
701            Some(self.expr_until(&["returning"])?)
702        } else {
703            None
704        };
705        let mut out = table;
706        if let Some(f) = filter {
707            out.push_str(" filter ");
708            out.push_str(&f);
709        }
710        out.push_str(" delete");
711        if self.returning_clause()? {
712            out.push_str(" returning");
713        }
714        Ok(out)
715    }
716
717    /// Parse a single literal following a column `DEFAULT`, rendered as PowQL
718    /// literal text. Only scalar literals are accepted (no expression
719    /// defaults), matching the PowQL `default` modifier.
720    fn default_literal(&mut self) -> Result<String, ParseError> {
721        match self.bump() {
722            Some(SqlTok::Number(n)) => Ok(n),
723            Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
724            Some(SqlTok::Word(w))
725                if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
726            {
727                Ok(w.to_ascii_lowercase())
728            }
729            other => Err(ParseError::Syntax {
730                message: format!(
731                    "DEFAULT requires a literal value, got {}",
732                    other.map(|t| t.display()).unwrap_or_else(|| "<eof>".into())
733                ),
734            }),
735        }
736    }
737
738    /// Parse an optional trailing `RETURNING *`, returning whether it was
739    /// present. PowQL's `returning` clause always yields every column, so a
740    /// projected `RETURNING a, b` is rejected rather than silently widened to
741    /// all columns.
742    fn returning_clause(&mut self) -> Result<bool, ParseError> {
743        if !self.eat_kw("returning") {
744            return Ok(false);
745        }
746        if !self.eat_sym('*') {
747            return Err(ParseError::Syntax {
748                message: "RETURNING currently supports only `RETURNING *` \
749                          (column projection is not yet supported)"
750                    .into(),
751            });
752        }
753        Ok(true)
754    }
755
756    fn create(&mut self) -> Result<String, ParseError> {
757        self.expect_kw("create")?;
758        if self.eat_kw("table") {
759            let table = self.expect_ident("table name")?;
760            self.expect_sym('(')?;
761            let mut fields = Vec::new();
762            while !self.eat_sym(')') {
763                if self.is_kw("primary") || self.is_kw("foreign") || self.is_kw("constraint") {
764                    return Err(ParseError::Unsupported { feature: "SQL table constraints are not supported; declare UNIQUE columns or add indexes explicitly".into() });
765                }
766                let name = self.expect_ident("column name")?;
767                let ty = self.sql_type()?;
768                let mut required = false;
769                let mut unique = false;
770                let mut auto = false;
771                let mut default: Option<String> = None;
772                loop {
773                    if self.eat_kw("not") {
774                        self.expect_kw("null")?;
775                        required = true;
776                    } else if self.eat_kw("unique") {
777                        unique = true;
778                    } else if self.eat_kw("autoincrement") || self.eat_kw("auto_increment") {
779                        auto = true;
780                    } else if self.eat_kw("default") {
781                        default = Some(self.default_literal()?);
782                    } else if self.eat_kw("null") {
783                    } else {
784                        break;
785                    }
786                }
787                let mut mods = Vec::new();
788                if required {
789                    mods.push("required");
790                }
791                if unique {
792                    mods.push("unique");
793                }
794                if auto {
795                    mods.push("auto");
796                }
797                let prefix = if mods.is_empty() {
798                    String::new()
799                } else {
800                    format!("{} ", mods.join(" "))
801                };
802                let suffix = match default {
803                    Some(lit) => format!(" default {lit}"),
804                    None => String::new(),
805                };
806                fields.push(format!("{prefix}{name}: {ty}{suffix}"));
807                let _ = self.eat_sym(',');
808            }
809            return Ok(format!("type {table} {{ {} }}", fields.join(", ")));
810        }
811        let unique = self.eat_kw("unique");
812        self.expect_kw("index")?;
813        let _idx = self.expect_ident("index name")?;
814        self.expect_kw("on")?;
815        let table = self.expect_ident("table name")?;
816        self.expect_sym('(')?;
817        let col = self.expect_ident("column name")?;
818        self.expect_sym(')')?;
819        Ok(if unique {
820            format!("alter {table} add unique .{col}")
821        } else {
822            format!("alter {table} add index .{col}")
823        })
824    }
825
826    fn drop_stmt(&mut self) -> Result<String, ParseError> {
827        self.expect_kw("drop")?;
828        if self.eat_kw("table") {
829            let table = self.expect_ident("table name")?;
830            Ok(format!("drop {table}"))
831        } else if self.eat_kw("view") {
832            let view = self.expect_ident("view name")?;
833            Ok(format!("drop view {view}"))
834        } else {
835            Err(ParseError::UnexpectedToken {
836                expected: "TABLE or VIEW".into(),
837                got: self
838                    .peek()
839                    .map(|t| t.display())
840                    .unwrap_or_else(|| "<eof>".into()),
841            })
842        }
843    }
844
845    fn alter(&mut self) -> Result<String, ParseError> {
846        self.expect_kw("alter")?;
847        self.expect_kw("table")?;
848        let table = self.expect_ident("table name")?;
849        if self.eat_kw("add") {
850            let _ = self.eat_kw("column");
851            let name = self.expect_ident("column name")?;
852            let ty = self.sql_type()?;
853            let mut required = false;
854            if self.eat_kw("not") {
855                self.expect_kw("null")?;
856                required = true;
857            }
858            let prefix = if required { "required " } else { "" };
859            Ok(format!("alter {table} add column {prefix}{name}: {ty}"))
860        } else if self.eat_kw("drop") {
861            let _ = self.eat_kw("column");
862            let name = self.expect_ident("column name")?;
863            Ok(format!("alter {table} drop column {name}"))
864        } else {
865            Err(ParseError::UnexpectedToken {
866                expected: "ADD or DROP".into(),
867                got: self
868                    .peek()
869                    .map(|t| t.display())
870                    .unwrap_or_else(|| "<eof>".into()),
871            })
872        }
873    }
874
875    fn sql_type(&mut self) -> Result<String, ParseError> {
876        let raw = self.expect_ident("type name")?;
877        // Ignore VARCHAR(255)-style length specifiers.
878        if self.eat_sym('(') {
879            while !self.eat_sym(')') {
880                if self.at_end() {
881                    return Err(ParseError::Syntax {
882                        message: "unterminated SQL type length".into(),
883                    });
884                }
885                self.bump();
886            }
887        }
888        let ty = match raw.to_ascii_lowercase().as_str() {
889            "text" | "varchar" | "char" | "string" | "str" => "str",
890            "int" | "integer" | "bigint" | "smallint" => "int",
891            "real" | "double" | "float" | "decimal" | "numeric" => "float",
892            "bool" | "boolean" => "bool",
893            "datetime" | "timestamp" => "datetime",
894            "uuid" => "uuid",
895            "blob" | "bytes" => "bytes",
896            other => {
897                return Err(ParseError::Unsupported {
898                    feature: format!("unsupported SQL type `{other}`"),
899                })
900            }
901        };
902        Ok(ty.into())
903    }
904
905    fn assignment_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
906        let mut out = Vec::new();
907        loop {
908            let name = self.expect_ident("column name")?;
909            match self.bump() {
910                Some(SqlTok::Op(op)) if op == "=" => {}
911                Some(t) => {
912                    return Err(ParseError::UnexpectedToken {
913                        expected: "=".into(),
914                        got: t.display(),
915                    })
916                }
917                None => {
918                    return Err(ParseError::UnexpectedToken {
919                        expected: "=".into(),
920                        got: "<eof>".into(),
921                    })
922                }
923            }
924            let v = self.expr_until(stop)?;
925            out.push(format!("{name} := {v}"));
926            if !self.eat_sym(',') {
927                break;
928            }
929        }
930        Ok(out)
931    }
932
933    fn field_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
934        let mut fields = Vec::new();
935        loop {
936            fields.push(self.field_ref()?);
937            if !self.eat_sym(',') || self.next_is_stop(stop) {
938                break;
939            }
940        }
941        Ok(fields)
942    }
943
944    fn order_list_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
945        let mut parts = Vec::new();
946        loop {
947            let mut p = self.field_ref()?;
948            if self.eat_kw("desc") {
949                p.push_str(" desc");
950            } else if self.eat_kw("asc") {
951                p.push_str(" asc");
952            }
953            parts.push(p);
954            if !self.eat_sym(',') || self.next_is_stop(stop) {
955                break;
956            }
957        }
958        Ok(parts.join(", "))
959    }
960
961    fn field_ref(&mut self) -> Result<String, ParseError> {
962        let first = self.expect_ident("column name")?;
963        if self.eat_sym('.') {
964            let second = self.expect_ident("qualified column name")?;
965            Ok(format!("{first}.{second}"))
966        } else {
967            Ok(format!(".{first}"))
968        }
969    }
970
971    fn expr_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
972        self.expr_bp(0, stop)
973    }
974
975    fn expr_bp(&mut self, min_bp: u8, stop: &[&str]) -> Result<String, ParseError> {
976        // Guard the recursive descent against stack overflow. Error paths below
977        // abort the whole parse, so only the success path needs to restore the
978        // counter (done right before the final `Ok`).
979        self.depth += 1;
980        if self.depth > MAX_SQL_NESTING_DEPTH {
981            return Err(ParseError::NestingDepthExceeded {
982                max: MAX_SQL_NESTING_DEPTH,
983            });
984        }
985        let mut lhs = if self.eat_kw("not") {
986            // Standard SQL: `NOT` binds looser than comparison, so `NOT x = 1`
987            // is `NOT (x = 1)`. Parse the comparison (min_bp 5 admits `=`/`<`/…
988            // but stops before AND/OR) and parenthesize it so the canonical
989            // PowQL re-parse is unambiguous regardless of PowQL's own NOT
990            // precedence.
991            format!("not ({})", self.expr_bp(5, stop)?)
992        } else if self.eat_kw("exists") {
993            if self.eat_sym('(') {
994                if self.is_kw("select") {
995                    return Err(ParseError::Unsupported {
996                        feature:
997                            "SQL EXISTS subqueries are not supported yet; use PowQL EXISTS for now"
998                                .into(),
999                    });
1000                }
1001                return Err(ParseError::Syntax {
1002                    message: "expected subquery after EXISTS".into(),
1003                });
1004            }
1005            return Err(ParseError::Syntax {
1006                message: "expected EXISTS (...)".into(),
1007            });
1008        } else if self.eat_sym('(') {
1009            if self.is_kw("select") {
1010                return Err(ParseError::Unsupported {
1011                    feature:
1012                        "SQL scalar subqueries are not supported yet; use PowQL subqueries for now"
1013                            .into(),
1014                });
1015            }
1016            let inner = self.expr_bp(0, stop)?;
1017            self.expect_sym(')')?;
1018            format!("({inner})")
1019        } else {
1020            self.primary_expr()?
1021        };
1022
1023        loop {
1024            if self.next_is_stop(stop)
1025                || self.at_end()
1026                || matches!(self.peek(), Some(SqlTok::Symbol(')' | ',')))
1027            {
1028                break;
1029            }
1030            if self.eat_kw("is") {
1031                let not = self.eat_kw("not");
1032                self.expect_kw("null")?;
1033                lhs = if not {
1034                    format!("{lhs} != null")
1035                } else {
1036                    format!("{lhs} = null")
1037                };
1038                continue;
1039            }
1040            if self.eat_kw("not") {
1041                if self.eat_kw("in") {
1042                    return Err(ParseError::Unsupported {
1043                        feature:
1044                            "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1045                                .into(),
1046                    });
1047                }
1048                if self.eat_kw("like") {
1049                    let rhs = self.expr_bp(6, stop)?;
1050                    lhs = format!("{lhs} not like {rhs}");
1051                    continue;
1052                }
1053                if self.eat_kw("between") {
1054                    return Err(ParseError::Unsupported {
1055                        feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1056                    });
1057                }
1058                return Err(ParseError::UnexpectedToken {
1059                    expected: "IN, LIKE, or BETWEEN after NOT".into(),
1060                    got: self
1061                        .peek()
1062                        .map(|t| t.display())
1063                        .unwrap_or_else(|| "<eof>".into()),
1064                });
1065            }
1066            if self.eat_kw("in") {
1067                return Err(ParseError::Unsupported {
1068                    feature: "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1069                        .into(),
1070                });
1071            }
1072            if self.eat_kw("between") {
1073                return Err(ParseError::Unsupported {
1074                    feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1075                });
1076            }
1077            if self.eat_kw("like") {
1078                let (l_bp, r_bp) = (5, 6);
1079                if l_bp < min_bp {
1080                    self.pos -= 1;
1081                    break;
1082                }
1083                let rhs = self.expr_bp(r_bp, stop)?;
1084                lhs = format!("{lhs} like {rhs}");
1085                continue;
1086            }
1087
1088            let op = if self.eat_kw("or") {
1089                "or".to_string()
1090            } else if self.eat_kw("and") {
1091                "and".to_string()
1092            } else if let Some(SqlTok::Op(op)) = self.peek().cloned() {
1093                self.pos += 1;
1094                op
1095            } else if self.eat_sym('*') {
1096                "*".into()
1097            } else {
1098                break;
1099            };
1100            let (l_bp, r_bp) = infix_bp(&op).ok_or_else(|| ParseError::Syntax {
1101                message: format!("unsupported SQL operator `{op}`"),
1102            })?;
1103            if l_bp < min_bp {
1104                self.pos -= 1;
1105                break;
1106            }
1107            let rhs = self.expr_bp(r_bp, stop)?;
1108            lhs = format!("{lhs} {op} {rhs}");
1109        }
1110        self.depth -= 1;
1111        Ok(lhs)
1112    }
1113
1114    fn primary_expr(&mut self) -> Result<String, ParseError> {
1115        match self.bump() {
1116            Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("null") => Ok("null".into()),
1117            Some(SqlTok::Word(w))
1118                if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
1119            {
1120                Ok(w.to_ascii_lowercase())
1121            }
1122            Some(SqlTok::Word(w)) => {
1123                if self.eat_sym('(') {
1124                    let func = w.to_ascii_lowercase();
1125                    if func == "count" && self.eat_sym('*') {
1126                        self.expect_sym(')')?;
1127                        return Ok("count(*)".into());
1128                    }
1129                    let mut args = Vec::new();
1130                    while !self.eat_sym(')') {
1131                        args.push(self.expr_bp(0, &[])?);
1132                        let _ = self.eat_sym(',');
1133                    }
1134                    return Ok(format!("{}({})", func, args.join(", ")));
1135                }
1136                if self.eat_sym('.') {
1137                    let f = self.expect_ident("qualified column name")?;
1138                    Ok(format!("{w}.{f}"))
1139                } else {
1140                    Ok(format!(".{w}"))
1141                }
1142            }
1143            Some(SqlTok::Number(n)) => Ok(n),
1144            Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
1145            Some(SqlTok::Param(p)) => Ok(format!("${p}")),
1146            Some(SqlTok::Symbol('*')) => Ok("*".into()),
1147            Some(t) => Err(ParseError::Syntax {
1148                message: format!("unexpected SQL token in expression: {}", t.display()),
1149            }),
1150            None => Err(ParseError::UnexpectedToken {
1151                expected: "expression".into(),
1152                got: "<eof>".into(),
1153            }),
1154        }
1155    }
1156
1157    fn next_is_stop(&self, stop: &[&str]) -> bool {
1158        matches!(self.peek(), Some(SqlTok::Word(w)) if stop.iter().any(|kw| w.eq_ignore_ascii_case(kw)))
1159    }
1160}
1161
1162fn infix_bp(op: &str) -> Option<(u8, u8)> {
1163    Some(match op.to_ascii_lowercase().as_str() {
1164        "or" => (1, 2),
1165        "and" => (3, 4),
1166        "=" | "!=" | "<" | ">" | "<=" | ">=" => (5, 6),
1167        "+" | "-" => (7, 8),
1168        "*" | "/" => (9, 10),
1169        _ => return None,
1170    })
1171}
1172
1173fn quote_powql_string(s: &str) -> String {
1174    format!(
1175        "\"{}\"",
1176        s.replace('\\', "\\\\")
1177            .replace('"', "\\\"")
1178            .replace('\n', "\\n")
1179            .replace('\t', "\\t")
1180    )
1181}
1182
1183fn is_clause_kw(w: &str) -> bool {
1184    matches!(
1185        w.to_ascii_lowercase().as_str(),
1186        "where"
1187            | "group"
1188            | "having"
1189            | "order"
1190            | "limit"
1191            | "offset"
1192            | "join"
1193            | "inner"
1194            | "left"
1195            | "right"
1196            | "cross"
1197            | "on"
1198            | "values"
1199            | "set"
1200    )
1201}
1202fn is_join_modifier(w: &str) -> bool {
1203    matches!(
1204        w.to_ascii_lowercase().as_str(),
1205        "join" | "inner" | "left" | "right" | "cross" | "outer"
1206    )
1207}
1208fn is_reserved_identifier(w: &str) -> bool {
1209    matches!(
1210        w.to_ascii_lowercase().as_str(),
1211        "select"
1212            | "from"
1213            | "where"
1214            | "insert"
1215            | "into"
1216            | "values"
1217            | "update"
1218            | "set"
1219            | "delete"
1220            | "create"
1221            | "table"
1222            | "drop"
1223            | "alter"
1224    )
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229    use super::*;
1230
1231    #[test]
1232    fn select_lowers_to_powql_ast() {
1233        let sql = parse_sql_with_canonical(
1234            "SELECT name, age FROM User WHERE age > 25 ORDER BY age DESC LIMIT 10",
1235        )
1236        .unwrap();
1237        assert_eq!(
1238            sql.canonical_powql,
1239            "User filter .age > 25 order .age desc limit 10 { .name, .age }"
1240        );
1241        assert_eq!(
1242            sql.statement,
1243            parser::parse("User filter .age > 25 order .age desc limit 10 { .name, .age }")
1244                .unwrap()
1245        );
1246    }
1247
1248    #[test]
1249    fn insert_update_delete_and_ddl_lower_to_existing_ast() {
1250        assert!(matches!(
1251            parse_sql("CREATE TABLE User (id INTEGER NOT NULL UNIQUE, name TEXT)").unwrap(),
1252            Statement::CreateType(_)
1253        ));
1254        assert!(matches!(
1255            parse_sql("INSERT INTO User (id, name) VALUES (1, 'Ada')").unwrap(),
1256            Statement::Insert(_)
1257        ));
1258        assert!(matches!(
1259            parse_sql("UPDATE User SET name = 'Grace' WHERE id = 1").unwrap(),
1260            Statement::UpdateQuery(_)
1261        ));
1262        assert!(matches!(
1263            parse_sql("DELETE FROM User WHERE id = 1").unwrap(),
1264            Statement::DeleteQuery(_)
1265        ));
1266    }
1267
1268    #[test]
1269    fn unsupported_sql_gets_explicit_error() {
1270        let err = parse_sql("SELECT name FROM User WHERE id IN (SELECT user_id FROM Orders)")
1271            .unwrap_err();
1272        assert!(err.to_string().contains("SQL IN"));
1273    }
1274}