1use crate::ast::{AggregateMode, Expr, QueryExpr, 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 qual_ctx: QualCtx::None,
28 };
29 let canonical_powql = p.statement()?;
30 if !p.at_end() {
31 return Err(ParseError::Syntax {
32 message: format!(
33 "unexpected trailing SQL token: {}",
34 p.peek()
35 .map(|t| t.display())
36 .unwrap_or_else(|| "<eof>".into())
37 ),
38 });
39 }
40 let mut statement = parser::parse(&canonical_powql)?;
41 mark_sql_statement_raw(&mut statement);
42 Ok(ParsedSql {
43 statement,
44 canonical_powql,
45 })
46}
47
48pub fn sql_is_effectively_blank(input: &str) -> bool {
73 match lex_sql(input) {
74 Ok(toks) => toks.is_empty(),
75 Err(_) => {
79 let stripped = input
80 .lines()
81 .map(|line| line.split('#').next().unwrap_or(""))
82 .collect::<Vec<_>>()
83 .join("\n");
84 matches!(lex_sql(&stripped), Ok(toks) if toks.is_empty())
85 }
86 }
87}
88
89pub fn split_statements_sql(input: &str) -> Vec<&str> {
103 let mut out = Vec::new();
104 let mut start = 0usize;
105 let bytes = input.as_bytes();
106 let mut i = 0usize;
107
108 while i < bytes.len() {
109 match bytes[i] {
110 b'-' if bytes.get(i + 1) == Some(&b'-') => {
111 i += 2;
112 while i < bytes.len() && bytes[i] != b'\n' {
113 i += 1;
114 }
115 }
116 b'/' if bytes.get(i + 1) == Some(&b'*') => {
117 i += 2;
118 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
119 i += 1;
120 }
121 i = (i + 2).min(bytes.len());
123 }
124 q @ (b'\'' | b'"') => {
125 i += 1;
126 while i < bytes.len() {
127 if bytes[i] == b'\\' && i + 1 < bytes.len() {
128 i += 2;
129 continue;
130 }
131 if bytes[i] == q {
132 if q == b'\'' && bytes.get(i + 1) == Some(&b'\'') {
134 i += 2;
135 continue;
136 }
137 i += 1;
138 break;
139 }
140 i += 1;
141 }
142 }
143 b';' => {
144 let seg = input[start..i].trim();
145 if !seg.is_empty() {
146 out.push(seg);
147 }
148 start = i + 1;
149 i += 1;
150 }
151 _ => i += 1,
152 }
153 }
154
155 let seg = input[start..].trim();
156 if !seg.is_empty() {
157 out.push(seg);
158 }
159 out
160}
161
162fn mark_sql_statement_raw(statement: &mut Statement) {
163 match statement {
164 Statement::Query(query) => mark_sql_query_raw(query),
165 Statement::Union(union) => {
166 mark_sql_statement_raw(&mut union.left);
167 mark_sql_statement_raw(&mut union.right);
168 }
169 Statement::Explain(inner) => mark_sql_statement_raw(inner),
170 Statement::CreateView(view) => mark_sql_query_raw(&mut view.query),
178 _ => {}
179 }
180}
181
182fn mark_sql_query_raw(query: &mut QueryExpr) {
183 if let Some(aggregate) = &mut query.aggregation {
184 aggregate.mode = AggregateMode::Raw;
185 if let Some(argument) = &mut aggregate.argument {
186 mark_sql_expr_raw(argument);
187 }
188 }
189 for join in &mut query.joins {
190 if let Some(on) = &mut join.on {
191 mark_sql_expr_raw(on);
192 }
193 }
194 if let Some(filter) = &mut query.filter {
195 mark_sql_expr_raw(filter);
196 }
197 if let Some(order) = &mut query.order {
198 for key in &mut order.keys {
199 mark_sql_expr_raw(&mut key.expr);
200 }
201 }
202 if let Some(projection) = &mut query.projection {
203 for field in projection {
204 mark_sql_expr_raw(&mut field.expr);
205 }
206 }
207 if let Some(group) = &mut query.group_by {
208 for key in &mut group.keys {
209 mark_sql_expr_raw(&mut key.expr);
210 }
211 if let Some(having) = &mut group.having {
212 mark_sql_expr_raw(having);
213 }
214 }
215}
216
217fn mark_sql_expr_raw(expr: &mut Expr) {
218 match expr {
219 Expr::FunctionCall(_, argument, mode) => {
220 *mode = AggregateMode::Raw;
221 mark_sql_expr_raw(argument);
222 }
223 Expr::Window {
224 args,
225 mode,
226 partition_by,
227 order_by,
228 ..
229 } => {
230 *mode = AggregateMode::Raw;
231 for expr in args.iter_mut().chain(partition_by.iter_mut()) {
232 mark_sql_expr_raw(expr);
233 }
234 for key in order_by {
235 mark_sql_expr_raw(&mut key.expr);
236 }
237 }
238 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
239 mark_sql_expr_raw(left);
240 mark_sql_expr_raw(right);
241 }
242 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
243 mark_sql_expr_raw(inner);
244 }
245 Expr::ScalarFunc(_, args) => {
246 for expr in args {
247 mark_sql_expr_raw(expr);
248 }
249 }
250 Expr::InList { expr, list, .. } => {
251 mark_sql_expr_raw(expr);
252 for item in list {
253 mark_sql_expr_raw(item);
254 }
255 }
256 Expr::InSubquery { expr, subquery, .. } => {
257 mark_sql_expr_raw(expr);
258 mark_sql_query_raw(subquery);
259 }
260 Expr::ExistsSubquery { subquery, .. } => mark_sql_query_raw(subquery),
261 Expr::Case { whens, else_expr } => {
262 for (condition, result) in whens {
263 mark_sql_expr_raw(condition);
264 mark_sql_expr_raw(result);
265 }
266 if let Some(expr) = else_expr {
267 mark_sql_expr_raw(expr);
268 }
269 }
270 _ => {}
271 }
272}
273
274pub(crate) fn statement_has_aggregate(statement: &Statement) -> bool {
275 match statement {
276 Statement::Query(query) => query_has_aggregate(query),
277 Statement::Union(union) => {
278 statement_has_aggregate(&union.left) || statement_has_aggregate(&union.right)
279 }
280 Statement::Explain(inner) => statement_has_aggregate(inner),
281 Statement::CreateView(view) => query_has_aggregate(&view.query),
282 _ => false,
283 }
284}
285
286fn query_has_aggregate(query: &QueryExpr) -> bool {
287 query.aggregation.is_some()
288 || query
289 .joins
290 .iter()
291 .filter_map(|join| join.on.as_ref())
292 .any(expr_has_aggregate)
293 || query.filter.as_ref().is_some_and(expr_has_aggregate)
294 || query
295 .order
296 .as_ref()
297 .is_some_and(|order| order.keys.iter().any(|key| expr_has_aggregate(&key.expr)))
298 || query.projection.as_ref().is_some_and(|projection| {
299 projection
300 .iter()
301 .any(|field| expr_has_aggregate(&field.expr))
302 })
303 || query.group_by.as_ref().is_some_and(|group| {
304 group.keys.iter().any(|key| expr_has_aggregate(&key.expr))
305 || group.having.as_ref().is_some_and(expr_has_aggregate)
306 })
307}
308
309fn expr_has_aggregate(expr: &Expr) -> bool {
310 match expr {
311 Expr::FunctionCall(..) | Expr::Window { .. } => true,
312 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
313 expr_has_aggregate(left) || expr_has_aggregate(right)
314 }
315 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
316 expr_has_aggregate(inner)
317 }
318 Expr::ScalarFunc(_, args) => args.iter().any(expr_has_aggregate),
319 Expr::InList { expr, list, .. } => {
320 expr_has_aggregate(expr) || list.iter().any(expr_has_aggregate)
321 }
322 Expr::InSubquery { expr, subquery, .. } => {
323 expr_has_aggregate(expr) || query_has_aggregate(subquery)
324 }
325 Expr::ExistsSubquery { subquery, .. } => query_has_aggregate(subquery),
326 Expr::Case { whens, else_expr } => {
327 whens.iter().any(|(condition, result)| {
328 expr_has_aggregate(condition) || expr_has_aggregate(result)
329 }) || else_expr.as_deref().is_some_and(expr_has_aggregate)
330 }
331 _ => false,
332 }
333}
334
335#[derive(Debug, Clone, PartialEq)]
336enum SqlTok {
337 Word(String),
338 Number(String),
339 String(String),
340 Symbol(char),
341 Op(String),
342 Param(String),
343}
344
345impl SqlTok {
346 fn display(&self) -> String {
347 match self {
348 SqlTok::Word(s) => s.clone(),
349 SqlTok::Number(s) => s.clone(),
350 SqlTok::String(s) => format!("'{s}'"),
351 SqlTok::Symbol(c) => c.to_string(),
352 SqlTok::Op(s) => s.clone(),
353 SqlTok::Param(s) => format!("${s}"),
354 }
355 }
356}
357
358fn lex_sql(input: &str) -> Result<Vec<SqlTok>, ParseError> {
359 let mut out = Vec::new();
360 let chars: Vec<char> = input.chars().collect();
361 let mut i = 0usize;
362 while i < chars.len() {
363 let c = chars[i];
364 if c.is_whitespace() {
365 i += 1;
366 continue;
367 }
368 if c == '-' && chars.get(i + 1) == Some(&'-') {
369 i += 2;
370 while i < chars.len() && chars[i] != '\n' {
371 i += 1;
372 }
373 continue;
374 }
375 if c == '/' && chars.get(i + 1) == Some(&'*') {
376 i += 2;
377 while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {
378 i += 1;
379 }
380 if i + 1 >= chars.len() {
381 return Err(ParseError::Lex {
382 message: "unterminated block comment".into(),
383 position: i,
384 });
385 }
386 i += 2;
387 continue;
388 }
389 if c == '\'' || c == '"' {
390 let quote = c;
391 i += 1;
392 let mut s = String::new();
393 while i < chars.len() {
394 if chars[i] == quote {
395 if quote == '\'' && chars.get(i + 1) == Some(&'\'') {
396 s.push('\'');
397 i += 2;
398 continue;
399 }
400 i += 1;
401 break;
402 }
403 if chars[i] == '\\' && i + 1 < chars.len() {
404 let next = chars[i + 1];
405 match next {
406 'n' => s.push('\n'),
407 't' => s.push('\t'),
408 other => s.push(other),
409 }
410 i += 2;
411 } else {
412 s.push(chars[i]);
413 i += 1;
414 }
415 }
416 if i > chars.len() || chars.get(i.saturating_sub(1)) != Some("e) {
417 return Err(ParseError::Lex {
418 message: if quote == '"' {
419 "unterminated quoted identifier".into()
420 } else {
421 "unterminated string".into()
422 },
423 position: i,
424 });
425 }
426 if quote == '"' {
427 if s.is_empty() {
441 return Err(ParseError::Syntax {
442 message: "empty quoted identifier".into(),
443 });
444 }
445 if s.contains('`') {
446 return Err(ParseError::Unsupported {
447 feature: format!(
448 "quoted identifier `{s}` contains a backtick, which PowQL uses to \
449 quote identifiers and cannot escape"
450 ),
451 });
452 }
453 out.push(SqlTok::Word(format!("`{s}`")));
454 continue;
455 }
456 out.push(SqlTok::String(s));
457 continue;
458 }
459 if c == '$' {
460 i += 1;
461 let start = i;
462 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
463 i += 1;
464 }
465 out.push(SqlTok::Param(chars[start..i].iter().collect()));
466 continue;
467 }
468 if c == '-' && chars.get(i + 1) == Some(&'>') {
470 if chars.get(i + 2) == Some(&'>') {
471 out.push(SqlTok::Op("->>".into()));
472 i += 3;
473 } else {
474 out.push(SqlTok::Op("->".into()));
475 i += 2;
476 }
477 continue;
478 }
479 if c.is_ascii_digit() || (c == '-' && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit()))
480 {
481 let start = i;
482 i += 1;
483 while i < chars.len() && chars[i].is_ascii_digit() {
484 i += 1;
485 }
486 if i < chars.len()
487 && chars[i] == '.'
488 && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit())
489 {
490 i += 1;
491 while i < chars.len() && chars[i].is_ascii_digit() {
492 i += 1;
493 }
494 }
495 out.push(SqlTok::Number(chars[start..i].iter().collect()));
496 continue;
497 }
498 if c.is_alphabetic() || c == '_' {
499 let start = i;
500 i += 1;
501 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
502 i += 1;
503 }
504 out.push(SqlTok::Word(chars[start..i].iter().collect()));
505 continue;
506 }
507 if matches!(c, '(' | ')' | ',' | '*' | '.') {
508 out.push(SqlTok::Symbol(c));
509 i += 1;
510 continue;
511 }
512 if matches!(c, '=' | '<' | '>' | '!') {
513 let mut op = String::new();
514 op.push(c);
515 if matches!(chars.get(i + 1), Some('=') | Some('>')) {
516 op.push(chars[i + 1]);
517 i += 2;
518 } else {
519 i += 1;
520 }
521 if op == "<>" {
522 op = "!=".into();
523 }
524 out.push(SqlTok::Op(op));
525 continue;
526 }
527 if matches!(c, '+' | '-' | '/') {
528 out.push(SqlTok::Op(c.to_string()));
529 i += 1;
530 continue;
531 }
532 return Err(ParseError::Lex {
533 message: format!("unexpected SQL character `{c}`"),
534 position: i,
535 });
536 }
537 Ok(out)
538}
539
540const MAX_SQL_NESTING_DEPTH: usize = 64;
554
555struct SqlParser {
556 toks: Vec<SqlTok>,
557 pos: usize,
558 depth: usize,
559 qual_ctx: QualCtx,
563}
564
565#[derive(Clone, PartialEq)]
572enum QualCtx {
573 None,
575 Single { visible_name: String },
579 Joined,
581}
582
583struct Projection {
585 text: String,
588 agg: Option<AggCall>,
592}
593
594struct AggCall {
596 func: String,
598 arg: AggArg,
599}
600
601enum AggArg {
602 Star,
604 Field(String),
606}
607
608impl AggCall {
609 fn canonical(&self) -> String {
611 match &self.arg {
612 AggArg::Star => format!("{}(*)", self.func),
613 AggArg::Field(f) => format!("{}({f})", self.func),
614 }
615 }
616}
617
618fn build_ungrouped_aggregate(agg: &AggCall, inner: &str) -> Result<String, ParseError> {
624 match agg.func.as_str() {
625 "count" if matches!(agg.arg, AggArg::Star) => Ok(format!("count({inner})")),
626 "count" | "sum" | "avg" | "min" | "max" => match &agg.arg {
627 AggArg::Field(f) => Ok(format!("{}({inner} {{ {f} }})", agg.func)),
628 AggArg::Star => Err(ParseError::Unsupported {
629 feature: format!("{0}(*) is not valid; {0}() needs a column", agg.func),
630 }),
631 },
632 other => Err(ParseError::Syntax {
634 message: format!("unknown aggregate function `{other}`"),
635 }),
636 }
637}
638
639impl SqlParser {
640 fn at_end(&self) -> bool {
641 self.pos >= self.toks.len()
642 }
643 fn peek(&self) -> Option<&SqlTok> {
644 self.toks.get(self.pos)
645 }
646 fn bump(&mut self) -> Option<SqlTok> {
647 let t = self.toks.get(self.pos).cloned();
648 if t.is_some() {
649 self.pos += 1;
650 }
651 t
652 }
653 fn is_kw(&self, kw: &str) -> bool {
654 matches!(self.peek(), Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case(kw))
655 }
656 fn eat_kw(&mut self, kw: &str) -> bool {
657 if self.is_kw(kw) {
658 self.pos += 1;
659 true
660 } else {
661 false
662 }
663 }
664 fn expect_kw(&mut self, kw: &str) -> Result<(), ParseError> {
665 if self.eat_kw(kw) {
666 Ok(())
667 } else {
668 Err(ParseError::UnexpectedToken {
669 expected: kw.into(),
670 got: self
671 .peek()
672 .map(|t| t.display())
673 .unwrap_or_else(|| "<eof>".into()),
674 })
675 }
676 }
677 fn eat_sym(&mut self, c: char) -> bool {
678 if matches!(self.peek(), Some(SqlTok::Symbol(got)) if *got == c) {
679 self.pos += 1;
680 true
681 } else {
682 false
683 }
684 }
685 fn expect_sym(&mut self, c: char) -> Result<(), ParseError> {
686 if self.eat_sym(c) {
687 Ok(())
688 } else {
689 Err(ParseError::UnexpectedToken {
690 expected: c.to_string(),
691 got: self
692 .peek()
693 .map(|t| t.display())
694 .unwrap_or_else(|| "<eof>".into()),
695 })
696 }
697 }
698 fn expect_ident(&mut self, what: &str) -> Result<String, ParseError> {
699 match self.bump() {
700 Some(SqlTok::Word(w)) if !is_reserved_identifier(&w) => Ok(w),
701 Some(SqlTok::Word(w)) => Err(ParseError::Syntax {
702 message: format!("expected {what}, got reserved word `{w}`"),
703 }),
704 Some(t) => Err(ParseError::UnexpectedToken {
705 expected: what.into(),
706 got: t.display(),
707 }),
708 None => Err(ParseError::UnexpectedToken {
709 expected: what.into(),
710 got: "<eof>".into(),
711 }),
712 }
713 }
714
715 fn statement(&mut self) -> Result<String, ParseError> {
716 if self.is_kw("select") {
717 self.select()
718 } else if self.is_kw("insert") {
719 self.insert()
720 } else if self.is_kw("update") {
721 self.update()
722 } else if self.is_kw("delete") {
723 self.delete()
724 } else if self.is_kw("create") {
725 self.create()
726 } else if self.is_kw("drop") {
727 self.drop_stmt()
728 } else if self.is_kw("alter") {
729 self.alter()
730 } else if self.eat_kw("begin") {
731 let _ = self.eat_kw("transaction");
732 Ok("begin".into())
733 } else if self.eat_kw("commit") {
734 Ok("commit".into())
735 } else if self.eat_kw("rollback") {
736 Ok("rollback".into())
737 } else {
738 Err(ParseError::UnexpectedToken {
739 expected: "SQL statement".into(),
740 got: self
741 .peek()
742 .map(|t| t.display())
743 .unwrap_or_else(|| "<eof>".into()),
744 })
745 }
746 }
747
748 fn scan_select_qual_ctx(&self) -> QualCtx {
754 let mut i = self.pos;
755 let mut depth = 0usize;
756 loop {
757 match self.toks.get(i) {
758 None => return QualCtx::None,
759 Some(SqlTok::Symbol('(')) => depth += 1,
760 Some(SqlTok::Symbol(')')) => depth = depth.saturating_sub(1),
761 Some(SqlTok::Word(w)) if depth == 0 && w.eq_ignore_ascii_case("from") => break,
762 Some(_) => {}
763 }
764 i += 1;
765 }
766 let Some(SqlTok::Word(table)) = self.toks.get(i + 1) else {
767 return QualCtx::None;
769 };
770 let mut visible = table.clone();
771 let mut j = i + 2;
772 match self.toks.get(j) {
775 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("as") => {
776 if let Some(SqlTok::Word(a)) = self.toks.get(j + 1) {
777 visible = a.clone();
778 j += 2;
779 }
780 }
781 Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w) => {
782 visible = w.clone();
783 j += 1;
784 }
785 _ => {}
786 }
787 match self.toks.get(j) {
788 Some(SqlTok::Word(w)) if is_join_modifier(w) => QualCtx::Joined,
789 _ => QualCtx::Single {
790 visible_name: visible,
791 },
792 }
793 }
794
795 fn select(&mut self) -> Result<String, ParseError> {
796 self.expect_kw("select")?;
797 self.qual_ctx = self.scan_select_qual_ctx();
798 let distinct = self.eat_kw("distinct");
799 let projection = self.projection_list()?;
800 self.expect_kw("from")?;
801 let source = self.table_ref()?;
802 let mut joins = Vec::new();
803 while self.starts_join() {
804 joins.push(self.join_clause()?);
805 }
806 let filter = if self.eat_kw("where") {
807 Some(self.expr_until(&["group", "having", "order", "limit", "offset"])?)
808 } else {
809 None
810 };
811 let group = if self.eat_kw("group") {
812 self.expect_kw("by")?;
813 Some(self.expression_list_until(&["having", "order", "limit", "offset"])?)
814 } else {
815 None
816 };
817 let having = if self.eat_kw("having") {
818 Some(self.expr_until(&["order", "limit", "offset"])?)
819 } else {
820 None
821 };
822 let order = if self.eat_kw("order") {
823 self.expect_kw("by")?;
824 Some(self.order_list_until(&["limit", "offset"])?)
825 } else {
826 None
827 };
828 let limit = if self.eat_kw("limit") {
829 Some(self.expr_until(&["offset"])?)
830 } else {
831 None
832 };
833 let offset = if self.eat_kw("offset") {
834 Some(self.expr_until(&[])?)
835 } else {
836 None
837 };
838
839 let has_group = group.is_some();
840
841 let mut out = source;
842 for j in joins {
843 out.push(' ');
844 out.push_str(&j);
845 }
846 if distinct {
847 out.push_str(" distinct");
848 }
849 if let Some(f) = filter {
850 out.push_str(" filter ");
851 out.push_str(&f);
852 }
853 if let Some(keys) = group {
854 out.push_str(" group ");
855 out.push_str(&keys.join(", "));
856 if let Some(h) = having {
857 out.push_str(" having ");
858 out.push_str(&h);
859 }
860 } else if having.is_some() {
861 return Err(ParseError::Syntax {
862 message: "HAVING requires GROUP BY".into(),
863 });
864 }
865 if let Some(o) = order {
866 out.push_str(" order ");
867 out.push_str(&o);
868 }
869 if let Some(l) = limit {
870 out.push_str(" limit ");
871 out.push_str(&l);
872 }
873 if let Some(o) = offset {
874 out.push_str(" offset ");
875 out.push_str(&o);
876 }
877 if let Some(items) = projection {
878 if !has_group && items.iter().any(|p| p.agg.is_some()) {
883 if distinct {
884 return Err(ParseError::Unsupported {
885 feature: "aggregates with DISTINCT and no GROUP BY are not supported by the SQL frontend".into(),
886 });
887 }
888 if items.len() != 1 {
889 return Err(ParseError::Unsupported {
890 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(),
891 });
892 }
893 let agg = items.into_iter().next().unwrap().agg.unwrap();
895 return build_ungrouped_aggregate(&agg, &out);
896 }
897 out.push_str(" { ");
898 out.push_str(
899 &items
900 .iter()
901 .map(|p| p.text.as_str())
902 .collect::<Vec<_>>()
903 .join(", "),
904 );
905 out.push_str(" }");
906 }
907 Ok(out)
908 }
909
910 fn projection_list(&mut self) -> Result<Option<Vec<Projection>>, ParseError> {
911 if self.eat_sym('*') {
912 return Ok(None);
913 }
914 let mut fields = Vec::new();
915 loop {
916 let (expr, agg) = match self.try_aggregate()? {
921 Some(a) => (a.canonical(), Some(a)),
922 None => (self.expr_until(&["from", "as"])?, None),
923 };
924 let text = if self.eat_kw("as") {
925 let alias = self.expect_ident("projection alias")?;
926 format!("{alias}: {expr}")
927 } else {
928 expr
929 };
930 fields.push(Projection { text, agg });
931 if !self.eat_sym(',') {
932 break;
933 }
934 }
935 Ok(Some(fields))
936 }
937
938 fn try_aggregate(&mut self) -> Result<Option<AggCall>, ParseError> {
943 let Some(SqlTok::Word(w)) = self.peek().cloned() else {
944 return Ok(None);
945 };
946 let func = w.to_ascii_lowercase();
947 if !matches!(func.as_str(), "count" | "sum" | "avg" | "min" | "max") {
948 return Ok(None);
949 }
950 let save = self.pos;
951 self.pos += 1; if !self.eat_sym('(') {
953 self.pos = save;
954 return Ok(None);
955 }
956 let arg = if func == "count" && self.eat_sym('*') {
957 AggArg::Star
958 } else if func == "count" && self.is_kw("distinct") {
959 self.pos = save;
963 return Ok(None);
964 } else {
965 AggArg::Field(self.expr_bp(0, &[])?)
966 };
967 if self.eat_sym(')')
970 && (matches!(self.peek(), Some(SqlTok::Symbol(',')))
971 || self.is_kw("as")
972 || self.is_kw("from"))
973 {
974 Ok(Some(AggCall { func, arg }))
975 } else {
976 self.pos = save;
977 Ok(None)
978 }
979 }
980
981 fn table_ref(&mut self) -> Result<String, ParseError> {
982 let table = self.expect_ident("table name")?;
983 let has_alias = self.eat_kw("as")
984 || matches!(self.peek(), Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w));
985 if has_alias {
986 let alias = self.expect_ident("table alias")?;
987 Ok(format!("{table} as {alias}"))
988 } else {
989 Ok(table)
990 }
991 }
992
993 fn starts_join(&self) -> bool {
994 self.is_kw("join")
995 || self.is_kw("inner")
996 || self.is_kw("left")
997 || self.is_kw("right")
998 || self.is_kw("cross")
999 }
1000
1001 fn join_clause(&mut self) -> Result<String, ParseError> {
1002 let kind = if self.eat_kw("inner") {
1003 self.expect_kw("join")?;
1004 "inner join"
1005 } else if self.eat_kw("left") {
1006 let _ = self.eat_kw("outer");
1007 self.expect_kw("join")?;
1008 "left join"
1009 } else if self.eat_kw("right") {
1010 let _ = self.eat_kw("outer");
1011 self.expect_kw("join")?;
1012 "right join"
1013 } else if self.eat_kw("cross") {
1014 self.expect_kw("join")?;
1015 "cross join"
1016 } else {
1017 self.expect_kw("join")?;
1018 "inner join"
1019 };
1020 let table = self.table_ref()?;
1021 if kind == "cross join" {
1022 return Ok(format!("{kind} {table}"));
1023 }
1024 self.expect_kw("on")?;
1025 let on = self.expr_until(&[
1026 "join", "inner", "left", "right", "cross", "where", "group", "having", "order",
1027 "limit", "offset",
1028 ])?;
1029 Ok(format!("{kind} {table} on {on}"))
1030 }
1031
1032 fn insert(&mut self) -> Result<String, ParseError> {
1033 self.expect_kw("insert")?;
1034 self.expect_kw("into")?;
1035 let table = self.expect_ident("table name")?;
1036 self.expect_sym('(')?;
1037 let mut cols = Vec::new();
1038 loop {
1039 cols.push(self.expect_ident("column name")?);
1040 if !self.eat_sym(',') {
1041 break;
1042 }
1043 }
1044 self.expect_sym(')')?;
1045 self.expect_kw("values")?;
1046 let mut rows = Vec::new();
1047 loop {
1048 self.expect_sym('(')?;
1049 let mut vals = Vec::new();
1050 loop {
1051 vals.push(self.expr_until(&[])?);
1052 if !self.eat_sym(',') {
1053 break;
1054 }
1055 }
1056 self.expect_sym(')')?;
1057 if vals.len() != cols.len() {
1058 return Err(ParseError::Syntax {
1059 message: format!(
1060 "INSERT has {} column(s) but {} value(s)",
1061 cols.len(),
1062 vals.len()
1063 ),
1064 });
1065 }
1066 let assigns = cols
1067 .iter()
1068 .zip(vals)
1069 .map(|(c, v)| format!("{c} := {v}"))
1070 .collect::<Vec<_>>();
1071 rows.push(format!("{{ {} }}", assigns.join(", ")));
1072 if !self.eat_sym(',') {
1073 break;
1074 }
1075 }
1076 let mut out = format!("insert {table} {}", rows.join(", "));
1077 if self.returning_clause()? {
1078 out.push_str(" returning");
1079 }
1080 Ok(out)
1081 }
1082
1083 fn update(&mut self) -> Result<String, ParseError> {
1084 self.expect_kw("update")?;
1085 let table = self.expect_ident("table name")?;
1086 self.qual_ctx = QualCtx::Single {
1087 visible_name: table.clone(),
1088 };
1089 self.expect_kw("set")?;
1090 let assigns = self.assignment_list_until(&["where", "returning"])?;
1091 let filter = if self.eat_kw("where") {
1092 Some(self.expr_until(&["returning"])?)
1093 } else {
1094 None
1095 };
1096 let mut out = table;
1097 if let Some(f) = filter {
1098 out.push_str(" filter ");
1099 out.push_str(&f);
1100 }
1101 out.push_str(" update { ");
1102 out.push_str(&assigns.join(", "));
1103 out.push_str(" }");
1104 if self.returning_clause()? {
1105 out.push_str(" returning");
1106 }
1107 Ok(out)
1108 }
1109
1110 fn delete(&mut self) -> Result<String, ParseError> {
1111 self.expect_kw("delete")?;
1112 self.expect_kw("from")?;
1113 let table = self.expect_ident("table name")?;
1114 self.qual_ctx = QualCtx::Single {
1115 visible_name: table.clone(),
1116 };
1117 let filter = if self.eat_kw("where") {
1118 Some(self.expr_until(&["returning"])?)
1119 } else {
1120 None
1121 };
1122 let mut out = table;
1123 if let Some(f) = filter {
1124 out.push_str(" filter ");
1125 out.push_str(&f);
1126 }
1127 out.push_str(" delete");
1128 if self.returning_clause()? {
1129 out.push_str(" returning");
1130 }
1131 Ok(out)
1132 }
1133
1134 fn default_literal(&mut self) -> Result<String, ParseError> {
1138 match self.bump() {
1139 Some(SqlTok::Number(n)) => Ok(n),
1140 Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
1141 Some(SqlTok::Word(w))
1142 if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
1143 {
1144 Ok(w.to_ascii_lowercase())
1145 }
1146 other => Err(ParseError::Syntax {
1147 message: format!(
1148 "DEFAULT requires a literal value, got {}",
1149 other.map(|t| t.display()).unwrap_or_else(|| "<eof>".into())
1150 ),
1151 }),
1152 }
1153 }
1154
1155 fn returning_clause(&mut self) -> Result<bool, ParseError> {
1160 if !self.eat_kw("returning") {
1161 return Ok(false);
1162 }
1163 if !self.eat_sym('*') {
1164 return Err(ParseError::Syntax {
1165 message: "RETURNING currently supports only `RETURNING *` \
1166 (column projection is not yet supported)"
1167 .into(),
1168 });
1169 }
1170 Ok(true)
1171 }
1172
1173 fn create(&mut self) -> Result<String, ParseError> {
1174 self.expect_kw("create")?;
1175 if self.eat_kw("table") {
1176 let table = self.expect_ident("table name")?;
1177 self.expect_sym('(')?;
1178 let mut fields = Vec::new();
1179 while !self.eat_sym(')') {
1180 if self.is_kw("primary")
1186 || self.is_kw("foreign")
1187 || self.is_kw("constraint")
1188 || self.is_kw("unique")
1189 || self.is_kw("check")
1190 {
1191 return Err(ParseError::Unsupported { feature: "SQL table constraints are not supported; declare UNIQUE columns or add indexes explicitly".into() });
1192 }
1193 let name = self.expect_ident("column name")?;
1194 let ty = self.sql_type()?;
1195 let mut required = false;
1196 let mut unique = false;
1197 let mut auto = false;
1198 let mut default: Option<String> = None;
1199 loop {
1200 if self.eat_kw("not") {
1201 self.expect_kw("null")?;
1202 required = true;
1203 } else if self.eat_kw("unique") {
1204 unique = true;
1205 } else if self.eat_kw("autoincrement") || self.eat_kw("auto_increment") {
1206 auto = true;
1207 } else if self.eat_kw("default") {
1208 default = Some(self.default_literal()?);
1209 } else if self.eat_kw("null") {
1210 } else {
1211 break;
1212 }
1213 }
1214 let mut mods = Vec::new();
1215 if required {
1216 mods.push("required");
1217 }
1218 if unique {
1219 mods.push("unique");
1220 }
1221 if auto {
1222 mods.push("auto");
1223 }
1224 let prefix = if mods.is_empty() {
1225 String::new()
1226 } else {
1227 format!("{} ", mods.join(" "))
1228 };
1229 let suffix = match default {
1230 Some(lit) => format!(" default {lit}"),
1231 None => String::new(),
1232 };
1233 fields.push(format!("{prefix}{name}: {ty}{suffix}"));
1234 let _ = self.eat_sym(',');
1235 }
1236 return Ok(format!("type {table} {{ {} }}", fields.join(", ")));
1237 }
1238 let unique = self.eat_kw("unique");
1239 self.expect_kw("index")?;
1240 let _idx = self.expect_ident("index name")?;
1241 self.expect_kw("on")?;
1242 let table = self.expect_ident("table name")?;
1243 self.expect_sym('(')?;
1244 let expression_parenthesized = self.eat_sym('(');
1245 if !matches!(self.peek(), Some(SqlTok::Word(_))) {
1246 return Err(ParseError::Unsupported {
1247 feature: "SQL expression indexes are not supported; use PowQL `alter <table> add index (.<json-column>-><path>)`"
1248 .into(),
1249 });
1250 }
1251 let col = self.expect_ident("column name")?;
1252 let mut path = format!(".{col}");
1253 let mut has_json_path = false;
1254 loop {
1255 match self.peek() {
1256 Some(SqlTok::Op(operator)) if operator == "->" => {
1257 self.bump();
1258 has_json_path = true;
1259 match self.bump() {
1260 Some(SqlTok::String(key)) => {
1261 path.push_str("->");
1262 path.push_str("e_powql_string(&key));
1263 }
1264 Some(SqlTok::Number(index))
1265 if !index.starts_with('-')
1266 && !index.contains('.')
1267 && index.parse::<u32>().is_ok() =>
1268 {
1269 path.push_str("->");
1270 path.push_str(&index);
1271 }
1272 Some(segment) => {
1273 return Err(ParseError::Unsupported {
1274 feature: format!(
1275 "SQL JSON expression indexes require string keys or non-negative integer path segments after ->, got {}",
1276 segment.display()
1277 ),
1278 });
1279 }
1280 None => {
1281 return Err(ParseError::UnexpectedToken {
1282 expected: "JSON path segment after ->".into(),
1283 got: "<eof>".into(),
1284 });
1285 }
1286 }
1287 }
1288 Some(SqlTok::Op(operator)) if operator == "->>" => {
1289 return Err(ParseError::Unsupported {
1290 feature:
1291 "SQL ->> text expressions cannot be indexed; use a direct JSON -> path"
1292 .into(),
1293 });
1294 }
1295 _ => break,
1296 }
1297 }
1298 if expression_parenthesized && !has_json_path {
1299 return Err(ParseError::Unsupported {
1300 feature: "SQL expression indexes support only direct JSON -> paths; use a plain column without extra parentheses"
1301 .into(),
1302 });
1303 }
1304 if expression_parenthesized && !self.eat_sym(')') {
1305 return Err(ParseError::Unsupported {
1306 feature: "SQL expression indexes support only direct JSON -> paths".into(),
1307 });
1308 }
1309 if !self.eat_sym(')') {
1310 return Err(ParseError::Unsupported {
1311 feature: "SQL expression and multi-column indexes are not supported; use PowQL `alter <table> add index (.<json-column>-><path>)` for a JSON path"
1312 .into(),
1313 });
1314 }
1315 Ok(if unique {
1316 if has_json_path {
1317 format!("alter {table} add unique ({path})")
1318 } else {
1319 format!("alter {table} add unique .{col}")
1320 }
1321 } else {
1322 if has_json_path {
1323 format!("alter {table} add index ({path})")
1324 } else {
1325 format!("alter {table} add index .{col}")
1326 }
1327 })
1328 }
1329
1330 fn drop_stmt(&mut self) -> Result<String, ParseError> {
1331 self.expect_kw("drop")?;
1332 if self.eat_kw("table") {
1333 let table = self.expect_ident("table name")?;
1334 Ok(format!("drop {table}"))
1335 } else if self.eat_kw("view") {
1336 let view = self.expect_ident("view name")?;
1337 Ok(format!("drop view {view}"))
1338 } else {
1339 Err(ParseError::UnexpectedToken {
1340 expected: "TABLE or VIEW".into(),
1341 got: self
1342 .peek()
1343 .map(|t| t.display())
1344 .unwrap_or_else(|| "<eof>".into()),
1345 })
1346 }
1347 }
1348
1349 fn alter(&mut self) -> Result<String, ParseError> {
1350 self.expect_kw("alter")?;
1351 self.expect_kw("table")?;
1352 let table = self.expect_ident("table name")?;
1353 if self.eat_kw("add") {
1354 let _ = self.eat_kw("column");
1355 let name = self.expect_ident("column name")?;
1356 let ty = self.sql_type()?;
1357 let mut required = false;
1358 if self.eat_kw("not") {
1359 self.expect_kw("null")?;
1360 required = true;
1361 }
1362 let prefix = if required { "required " } else { "" };
1363 Ok(format!("alter {table} add column {prefix}{name}: {ty}"))
1364 } else if self.eat_kw("drop") {
1365 let _ = self.eat_kw("column");
1366 let name = self.expect_ident("column name")?;
1367 Ok(format!("alter {table} drop column {name}"))
1368 } else {
1369 Err(ParseError::UnexpectedToken {
1370 expected: "ADD or DROP".into(),
1371 got: self
1372 .peek()
1373 .map(|t| t.display())
1374 .unwrap_or_else(|| "<eof>".into()),
1375 })
1376 }
1377 }
1378
1379 fn sql_type(&mut self) -> Result<String, ParseError> {
1380 let raw = self.expect_ident("type name")?;
1381 if self.eat_sym('(') {
1383 while !self.eat_sym(')') {
1384 if self.at_end() {
1385 return Err(ParseError::Syntax {
1386 message: "unterminated SQL type length".into(),
1387 });
1388 }
1389 self.bump();
1390 }
1391 }
1392 let ty = match raw.to_ascii_lowercase().as_str() {
1393 "text" | "varchar" | "char" | "string" | "str" => "str",
1394 "int" | "integer" | "bigint" | "smallint" => "int",
1395 "real" | "double" | "float" | "decimal" | "numeric" => "float",
1396 "bool" | "boolean" => "bool",
1397 "datetime" | "timestamp" => "datetime",
1398 "uuid" => "uuid",
1399 "blob" | "bytes" | "bytea" => "bytes",
1400 other => {
1401 return Err(ParseError::Unsupported {
1402 feature: format!("unsupported SQL type `{other}`"),
1403 })
1404 }
1405 };
1406 Ok(ty.into())
1407 }
1408
1409 fn assignment_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
1410 let mut out = Vec::new();
1411 loop {
1412 let name = self.expect_ident("column name")?;
1413 match self.bump() {
1414 Some(SqlTok::Op(op)) if op == "=" => {}
1415 Some(SqlTok::Op(op)) if op == "->" || op == "->>" => {
1421 return Err(ParseError::Unsupported {
1422 feature: format!(
1423 "cannot assign to a JSON path target `{name}{op}...`: JSON path \
1424 assignment targets are not supported; write the whole JSON column \
1425 instead (path mutation such as json_set is not yet available)"
1426 ),
1427 })
1428 }
1429 Some(t) => {
1430 return Err(ParseError::UnexpectedToken {
1431 expected: "=".into(),
1432 got: t.display(),
1433 })
1434 }
1435 None => {
1436 return Err(ParseError::UnexpectedToken {
1437 expected: "=".into(),
1438 got: "<eof>".into(),
1439 })
1440 }
1441 }
1442 let v = self.expr_until(stop)?;
1443 out.push(format!("{name} := {v}"));
1444 if !self.eat_sym(',') {
1445 break;
1446 }
1447 }
1448 Ok(out)
1449 }
1450
1451 fn expression_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
1452 let mut expressions = Vec::new();
1453 loop {
1454 expressions.push(self.expr_until(stop)?);
1455 if !self.eat_sym(',') || self.next_is_stop(stop) {
1456 break;
1457 }
1458 }
1459 Ok(expressions)
1460 }
1461
1462 fn order_list_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
1463 let mut parts = Vec::new();
1464 let mut expression_stop = Vec::with_capacity(stop.len() + 2);
1465 expression_stop.extend_from_slice(stop);
1466 expression_stop.extend_from_slice(&["asc", "desc"]);
1467 loop {
1468 let mut p = self.expr_until(&expression_stop)?;
1469 if self.eat_kw("desc") {
1470 p.push_str(" desc");
1471 } else if self.eat_kw("asc") {
1472 p.push_str(" asc");
1473 }
1474 parts.push(p);
1475 if !self.eat_sym(',') || self.next_is_stop(stop) {
1476 break;
1477 }
1478 }
1479 Ok(parts.join(", "))
1480 }
1481
1482 fn expr_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
1483 self.expr_bp(0, stop)
1484 }
1485
1486 fn expr_bp(&mut self, min_bp: u8, stop: &[&str]) -> Result<String, ParseError> {
1487 self.depth += 1;
1491 if self.depth > MAX_SQL_NESTING_DEPTH {
1492 return Err(ParseError::NestingDepthExceeded {
1493 max: MAX_SQL_NESTING_DEPTH,
1494 });
1495 }
1496 let mut lhs = if self.eat_kw("not") {
1497 format!("not ({})", self.expr_bp(5, stop)?)
1503 } else if self.eat_kw("exists") {
1504 if self.eat_sym('(') {
1505 if self.is_kw("select") {
1506 return Err(ParseError::Unsupported {
1507 feature:
1508 "SQL EXISTS subqueries are not supported yet; use PowQL EXISTS for now"
1509 .into(),
1510 });
1511 }
1512 return Err(ParseError::Syntax {
1513 message: "expected subquery after EXISTS".into(),
1514 });
1515 }
1516 return Err(ParseError::Syntax {
1517 message: "expected EXISTS (...)".into(),
1518 });
1519 } else if self.eat_sym('(') {
1520 if self.is_kw("select") {
1521 return Err(ParseError::Unsupported {
1522 feature:
1523 "SQL scalar subqueries are not supported yet; use PowQL subqueries for now"
1524 .into(),
1525 });
1526 }
1527 let inner = self.expr_bp(0, stop)?;
1528 self.expect_sym(')')?;
1529 format!("({inner})")
1530 } else {
1531 self.primary_expr()?
1532 };
1533
1534 let mut chain = 0usize;
1537 loop {
1538 if self.next_is_stop(stop)
1539 || self.at_end()
1540 || matches!(self.peek(), Some(SqlTok::Symbol(')' | ',')))
1541 {
1542 break;
1543 }
1544 chain += 1;
1545 if self.depth + chain > MAX_SQL_NESTING_DEPTH {
1546 return Err(ParseError::NestingDepthExceeded {
1547 max: MAX_SQL_NESTING_DEPTH,
1548 });
1549 }
1550 if matches!(self.peek(), Some(SqlTok::Op(op)) if op == "->" || op == "->>") {
1551 let text = matches!(self.bump(), Some(SqlTok::Op(op)) if op == "->>");
1552 let segment = match self.bump() {
1553 Some(SqlTok::String(key)) => quote_powql_string(&key),
1554 Some(SqlTok::Number(index))
1555 if !index.starts_with('-') && !index.contains('.') =>
1556 {
1557 index
1558 }
1559 Some(token) => {
1560 return Err(ParseError::Syntax {
1561 message: format!(
1562 "SQL JSON arrows require a string key or non-negative integer index, got {}",
1563 token.display()
1564 ),
1565 });
1566 }
1567 None => {
1568 return Err(ParseError::UnexpectedToken {
1569 expected: "JSON object key or array index".into(),
1570 got: "<eof>".into(),
1571 });
1572 }
1573 };
1574 let path = format!("{lhs}->{segment}");
1575 lhs = if text {
1576 format!("json_text({path})")
1577 } else {
1578 path
1579 };
1580 continue;
1581 }
1582 if self.eat_kw("is") {
1583 let not = self.eat_kw("not");
1584 self.expect_kw("null")?;
1585 lhs = if not {
1586 format!("{lhs} != null")
1587 } else {
1588 format!("{lhs} = null")
1589 };
1590 continue;
1591 }
1592 if self.eat_kw("not") {
1593 if self.eat_kw("in") {
1594 return Err(ParseError::Unsupported {
1595 feature:
1596 "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1597 .into(),
1598 });
1599 }
1600 if self.eat_kw("like") {
1601 let rhs = self.expr_bp(6, stop)?;
1602 lhs = format!("{lhs} not like {rhs}");
1603 continue;
1604 }
1605 if self.eat_kw("between") {
1606 return Err(ParseError::Unsupported {
1607 feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1608 });
1609 }
1610 return Err(ParseError::UnexpectedToken {
1611 expected: "IN, LIKE, or BETWEEN after NOT".into(),
1612 got: self
1613 .peek()
1614 .map(|t| t.display())
1615 .unwrap_or_else(|| "<eof>".into()),
1616 });
1617 }
1618 if self.eat_kw("in") {
1619 return Err(ParseError::Unsupported {
1620 feature: "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1621 .into(),
1622 });
1623 }
1624 if self.eat_kw("between") {
1625 return Err(ParseError::Unsupported {
1626 feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1627 });
1628 }
1629 if self.is_kw("over") {
1635 return Err(ParseError::Unsupported {
1636 feature: "SQL window functions (OVER) are not supported yet in the SQL \
1637 frontend; PowQL has them: row_number() over (order .col)"
1638 .into(),
1639 });
1640 }
1641 if self.eat_kw("like") {
1642 let (l_bp, r_bp) = (5, 6);
1643 if l_bp < min_bp {
1644 self.pos -= 1;
1645 break;
1646 }
1647 let rhs = self.expr_bp(r_bp, stop)?;
1648 lhs = format!("{lhs} like {rhs}");
1649 continue;
1650 }
1651
1652 let op = if self.eat_kw("or") {
1653 "or".to_string()
1654 } else if self.eat_kw("and") {
1655 "and".to_string()
1656 } else if let Some(SqlTok::Op(op)) = self.peek().cloned() {
1657 self.pos += 1;
1658 op
1659 } else if self.eat_sym('*') {
1660 "*".into()
1661 } else {
1662 break;
1663 };
1664 let (l_bp, r_bp) = infix_bp(&op).ok_or_else(|| ParseError::Syntax {
1665 message: format!("unsupported SQL operator `{op}`"),
1666 })?;
1667 if l_bp < min_bp {
1668 self.pos -= 1;
1669 break;
1670 }
1671 let rhs = self.expr_bp(r_bp, stop)?;
1672 if rhs == "null" && matches!(op.as_str(), "=" | "!=" | "<>") {
1688 lhs = "false".to_string();
1689 continue;
1690 }
1691 lhs = format!("{lhs} {op} {rhs}");
1692 }
1693 self.depth -= 1;
1694 Ok(lhs)
1695 }
1696
1697 fn primary_expr(&mut self) -> Result<String, ParseError> {
1698 match self.bump() {
1699 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("null") => Ok("null".into()),
1700 Some(SqlTok::Word(w))
1701 if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
1702 {
1703 Ok(w.to_ascii_lowercase())
1704 }
1705 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("case") => {
1711 Err(ParseError::Unsupported {
1712 feature: "SQL CASE/WHEN is not supported yet in the SQL frontend; \
1713 PowQL has it: case when <cond> then <value> else <value> end"
1714 .into(),
1715 })
1716 }
1717 Some(SqlTok::Word(w)) => {
1718 if self.eat_sym('(') {
1719 let func = w.to_ascii_lowercase();
1720 if func == "count" && self.eat_sym('*') {
1721 self.expect_sym(')')?;
1722 return Ok("count(*)".into());
1723 }
1724 if func == "coalesce" {
1733 return Err(ParseError::Unsupported {
1734 feature: "SQL COALESCE is not supported yet in the SQL frontend; \
1735 PowQL spells it with the ?? operator: .a ?? .b"
1736 .into(),
1737 });
1738 }
1739 if self.is_kw("distinct") {
1740 let hint = if func == "count" {
1743 "; PowQL spells it count(distinct T { .col })"
1744 } else {
1745 ""
1746 };
1747 return Err(ParseError::Unsupported {
1748 feature: format!(
1749 "SQL {}(DISTINCT ...) is not supported yet in the SQL \
1750 frontend{hint}",
1751 func.to_ascii_uppercase()
1752 ),
1753 });
1754 }
1755 let mut args = Vec::new();
1756 while !self.eat_sym(')') {
1757 args.push(self.expr_bp(0, &[])?);
1758 if func == "cast" && self.is_kw("as") {
1762 return Err(ParseError::Unsupported {
1763 feature: "SQL CAST(x AS TYPE) is not supported yet in the SQL \
1764 frontend; PowDB spells a cast cast(x, 'int') with the \
1765 target type as a string argument"
1766 .into(),
1767 });
1768 }
1769 let _ = self.eat_sym(',');
1770 }
1771 return Ok(format!("{}({})", func, args.join(", ")));
1772 }
1773 if self.eat_sym('.') {
1774 let f = self.expect_ident("qualified column name")?;
1775 match &self.qual_ctx {
1776 QualCtx::Joined => Ok(format!("{w}.{f}")),
1777 QualCtx::Single { visible_name } => {
1778 if w.eq_ignore_ascii_case(visible_name) {
1779 Ok(format!(".{f}"))
1780 } else {
1781 Err(ParseError::Syntax {
1782 message: format!(
1783 "no such column: {w}.{f} (the only table in this \
1784 statement is `{visible_name}`)"
1785 ),
1786 })
1787 }
1788 }
1789 QualCtx::None => Err(ParseError::Syntax {
1790 message: format!(
1791 "qualified column reference `{w}.{f}` is not allowed here"
1792 ),
1793 }),
1794 }
1795 } else {
1796 Ok(format!(".{w}"))
1797 }
1798 }
1799 Some(SqlTok::Number(n)) => Ok(n),
1800 Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
1801 Some(SqlTok::Param(p)) => Ok(format!("${p}")),
1802 Some(SqlTok::Symbol('*')) => Ok("*".into()),
1803 Some(t) => Err(ParseError::Syntax {
1804 message: format!("unexpected SQL token in expression: {}", t.display()),
1805 }),
1806 None => Err(ParseError::UnexpectedToken {
1807 expected: "expression".into(),
1808 got: "<eof>".into(),
1809 }),
1810 }
1811 }
1812
1813 fn next_is_stop(&self, stop: &[&str]) -> bool {
1814 matches!(self.peek(), Some(SqlTok::Word(w)) if stop.iter().any(|kw| w.eq_ignore_ascii_case(kw)))
1815 }
1816}
1817
1818fn infix_bp(op: &str) -> Option<(u8, u8)> {
1819 Some(match op.to_ascii_lowercase().as_str() {
1820 "or" => (1, 2),
1821 "and" => (3, 4),
1822 "=" | "!=" | "<" | ">" | "<=" | ">=" => (5, 6),
1823 "+" | "-" => (7, 8),
1824 "*" | "/" => (9, 10),
1825 _ => return None,
1826 })
1827}
1828
1829fn quote_powql_string(s: &str) -> String {
1830 format!(
1831 "\"{}\"",
1832 s.replace('\\', "\\\\")
1833 .replace('"', "\\\"")
1834 .replace('\n', "\\n")
1835 .replace('\t', "\\t")
1836 )
1837}
1838
1839fn is_clause_kw(w: &str) -> bool {
1840 matches!(
1841 w.to_ascii_lowercase().as_str(),
1842 "where"
1843 | "group"
1844 | "having"
1845 | "order"
1846 | "limit"
1847 | "offset"
1848 | "join"
1849 | "inner"
1850 | "left"
1851 | "right"
1852 | "cross"
1853 | "on"
1854 | "values"
1855 | "set"
1856 )
1857}
1858fn is_join_modifier(w: &str) -> bool {
1859 matches!(
1860 w.to_ascii_lowercase().as_str(),
1861 "join" | "inner" | "left" | "right" | "cross" | "outer"
1862 )
1863}
1864fn is_reserved_identifier(w: &str) -> bool {
1865 matches!(
1866 w.to_ascii_lowercase().as_str(),
1867 "select"
1868 | "from"
1869 | "where"
1870 | "insert"
1871 | "into"
1872 | "values"
1873 | "update"
1874 | "set"
1875 | "delete"
1876 | "create"
1877 | "table"
1878 | "drop"
1879 | "alter"
1880 )
1881}
1882
1883#[cfg(test)]
1884mod tests {
1885 use super::*;
1886 use crate::ast::{AlterAction, IndexTarget};
1887
1888 #[test]
1889 fn a_comment_only_sql_segment_is_effectively_blank() {
1890 for blank in [
1894 "",
1895 " ",
1896 "\n\t \n",
1897 "-- comment only",
1898 "--comment only",
1899 "-- a\n-- b\n",
1900 "# comment only",
1901 " # comment only ",
1902 "# a\n# b",
1903 "/* block */",
1904 "/* multi\nline */\n-- and a line comment\n",
1905 "-- end of dump\n",
1906 ] {
1907 assert!(
1908 sql_is_effectively_blank(blank),
1909 "{blank:?} must be effectively blank"
1910 );
1911 }
1912
1913 for real in [
1914 "SELECT 1",
1915 "SELECT 1 -- trailing",
1917 "SELECT 1\n-- trailing",
1918 "-- leading\nSELECT 1",
1919 "# leading\nSELECT 1",
1920 "/* leading */ SELECT 1",
1921 "SELECT '-- not a comment'",
1925 "SELECT '# not a comment'",
1926 "SELECT '/* not a comment */'",
1927 "SELECT @",
1929 "SELECT 'unterminated",
1930 ] {
1931 assert!(
1932 !sql_is_effectively_blank(real),
1933 "{real:?} must not be treated as blank"
1934 );
1935 }
1936 }
1937
1938 #[test]
1939 fn sql_frontend_rejects_create_view() {
1940 let result = parse_sql("CREATE VIEW v AS SELECT id FROM Post");
1945 assert!(
1946 result.is_err(),
1947 "CREATE VIEW must be rejected by the SQL frontend, got {result:?}"
1948 );
1949 }
1950
1951 #[test]
1952 fn json_path_update_target_is_targeted_unsupported() {
1953 for stmt in [
1957 "UPDATE Doc SET data->'x' = 5",
1958 "UPDATE Doc SET data->>'x' = 5",
1959 ] {
1960 let err = parse_sql(stmt).unwrap_err();
1961 assert!(
1962 matches!(err, ParseError::Unsupported { .. }),
1963 "{stmt}: expected Unsupported, got {err:?}"
1964 );
1965 let msg = err.to_string();
1966 assert!(
1967 msg.contains("JSON path assignment targets are not supported"),
1968 "{stmt}: message must state the unsupported feature: {msg}"
1969 );
1970 assert!(
1971 msg.contains("json_set"),
1972 "{stmt}: message must point at the whole-column alternative: {msg}"
1973 );
1974 }
1975 assert!(parse_sql("UPDATE Doc SET data = '{}'").is_ok());
1977 }
1978
1979 #[test]
1980 fn json_arrows_lex_longest_token_and_lower_to_powql_paths() {
1981 assert_eq!(
1982 lex_sql("data->>'name'").unwrap(),
1983 vec![
1984 SqlTok::Word("data".into()),
1985 SqlTok::Op("->>".into()),
1986 SqlTok::String("name".into()),
1987 ]
1988 );
1989 let parsed = parse_sql_with_canonical(
1990 "SELECT data -> 'author' ->> 'name' AS name, data -> 'tags' -> 0 AS first FROM Post WHERE data ->> 'state' = 'ready'",
1991 )
1992 .unwrap();
1993 assert_eq!(
1994 parsed.canonical_powql,
1995 "Post filter json_text(.data->\"state\") = \"ready\" { name: json_text(.data->\"author\"->\"name\"), first: .data->\"tags\"->0 }"
1996 );
1997 let raw = parse_sql_with_canonical("SELECT data -> 'name' FROM Post").unwrap();
1998 let text = parse_sql_with_canonical("SELECT data ->> 'name' FROM Post").unwrap();
1999 assert_ne!(
2000 crate::canonicalize::canonicalize(&raw.canonical_powql)
2001 .unwrap()
2002 .0,
2003 crate::canonicalize::canonicalize(&text.canonical_powql)
2004 .unwrap()
2005 .0,
2006 "-> and ->> must never share a cached plan"
2007 );
2008
2009 let ordered = parse_sql_with_canonical(
2010 "SELECT id FROM Post ORDER BY data ->> 'rank' DESC, data -> 'tie' ASC",
2011 )
2012 .unwrap();
2013 assert_eq!(
2014 ordered.canonical_powql,
2015 "Post order json_text(.data->\"rank\") desc, .data->\"tie\" asc { .id }"
2016 );
2017
2018 let grouped = parse_sql_with_canonical(
2019 "SELECT data ->> 'kind' AS kind, COUNT(*) AS n FROM Post GROUP BY data ->> 'kind'",
2020 )
2021 .unwrap();
2022 assert_eq!(
2023 grouped.canonical_powql,
2024 "Post group json_text(.data->\"kind\") { kind: json_text(.data->\"kind\"), n: count(*) }"
2025 );
2026 }
2027
2028 #[test]
2029 fn json_arrows_reject_invalid_path_segments() {
2030 for sql in [
2031 "SELECT data -> other FROM Post",
2032 "SELECT data -> -1 FROM Post",
2033 "SELECT data -> 1.5 FROM Post",
2034 ] {
2035 let err = parse_sql_with_canonical(sql).unwrap_err();
2036 assert!(
2037 err.to_string()
2038 .contains("string key or non-negative integer index"),
2039 "unexpected error for `{sql}`: {err}"
2040 );
2041 }
2042 }
2043
2044 #[test]
2045 fn select_lowers_to_powql_ast() {
2046 let sql = parse_sql_with_canonical(
2047 "SELECT name, age FROM User WHERE age > 25 ORDER BY age DESC LIMIT 10",
2048 )
2049 .unwrap();
2050 assert_eq!(
2051 sql.canonical_powql,
2052 "User filter .age > 25 order .age desc limit 10 { .name, .age }"
2053 );
2054 assert_eq!(
2055 sql.statement,
2056 parser::parse("User filter .age > 25 order .age desc limit 10 { .name, .age }")
2057 .unwrap()
2058 );
2059 }
2060
2061 #[test]
2062 fn insert_update_delete_and_ddl_lower_to_existing_ast() {
2063 assert!(matches!(
2064 parse_sql("CREATE TABLE User (id INTEGER NOT NULL UNIQUE, name TEXT)").unwrap(),
2065 Statement::CreateType(_)
2066 ));
2067 assert!(matches!(
2068 parse_sql("INSERT INTO User (id, name) VALUES (1, 'Ada')").unwrap(),
2069 Statement::Insert(_)
2070 ));
2071 assert!(matches!(
2072 parse_sql("UPDATE User SET name = 'Grace' WHERE id = 1").unwrap(),
2073 Statement::UpdateQuery(_)
2074 ));
2075 assert!(matches!(
2076 parse_sql("DELETE FROM User WHERE id = 1").unwrap(),
2077 Statement::DeleteQuery(_)
2078 ));
2079 }
2080
2081 #[test]
2082 fn unsupported_sql_gets_explicit_error() {
2083 let err = parse_sql("SELECT name FROM User WHERE id IN (SELECT user_id FROM Orders)")
2084 .unwrap_err();
2085 assert!(err.to_string().contains("SQL IN"));
2086 }
2087
2088 #[test]
2089 fn sql_expression_index_has_targeted_powql_guidance() {
2090 assert!(matches!(
2091 parse_sql("CREATE INDEX post_slug ON Post (slug)").unwrap(),
2092 Statement::AlterTable(_)
2093 ));
2094 for sql in [
2095 "CREATE INDEX post_age ON Post ((data -> 'age'))",
2096 "CREATE INDEX post_first ON Post (data -> 'scores' -> 0)",
2097 "CREATE UNIQUE INDEX post_code ON Post ((data -> 'code'))",
2098 ] {
2099 let Statement::AlterTable(alter) = parse_sql(sql).unwrap() else {
2100 panic!("expected expression-index ALTER lowering for `{sql}`");
2101 };
2102 let target = match alter.action {
2103 AlterAction::AddIndex { target, .. } | AlterAction::AddUnique { target, .. } => {
2104 target
2105 }
2106 action => panic!("expected add-index action, got {action:?}"),
2107 };
2108 assert!(matches!(target, IndexTarget::JsonPath(_)), "{sql}");
2109 }
2110 let error = parse_sql("CREATE INDEX post_age ON Post (data->>'age')")
2111 .expect_err("SQL text extraction is not an indexable path")
2112 .to_string();
2113 assert!(error.contains("->>"));
2114 let error = parse_sql("CREATE INDEX post_age ON Post ((data + 1))")
2115 .expect_err("arbitrary SQL expressions remain unsupported")
2116 .to_string();
2117 assert!(error.contains("only direct JSON -> paths"));
2118 }
2119
2120 #[test]
2121 fn sql_aggregates_lower_with_raw_mode() {
2122 let Statement::Query(query) =
2123 parse_sql("SELECT dept, SUM(balance) AS total FROM Account GROUP BY dept").unwrap()
2124 else {
2125 panic!("expected query");
2126 };
2127 let projection = query.projection.expect("projection");
2128 assert!(matches!(
2129 projection[1].expr,
2130 Expr::FunctionCall(_, _, AggregateMode::Raw)
2131 ));
2132 }
2133
2134 #[test]
2135 fn ungrouped_join_aggregate_lowers_to_raw_powql_aggregate() {
2136 let lowered = parse_sql_with_canonical(
2137 "SELECT AVG(a.balance) FROM Account a JOIN Entry e ON a.id = e.account_id",
2138 )
2139 .unwrap();
2140 assert_eq!(
2141 lowered.canonical_powql,
2142 "avg(Account as a inner join Entry as e on a.id = e.account_id { a.balance })"
2143 );
2144 let Statement::Query(query) = lowered.statement else {
2145 panic!("expected query");
2146 };
2147 assert_eq!(
2148 query.aggregation.expect("aggregate").mode,
2149 AggregateMode::Raw
2150 );
2151 }
2152}
2153
2154#[cfg(test)]
2155mod split_tests {
2156 use super::split_statements_sql;
2157
2158 #[test]
2167 fn a_semicolon_inside_a_comment_is_not_a_boundary() {
2168 let segs = split_statements_sql("-- cleanup; DELETE FROM t");
2169 assert_eq!(segs, vec!["-- cleanup; DELETE FROM t"]);
2170 assert!(super::sql_is_effectively_blank(segs[0]));
2171
2172 assert_eq!(
2173 split_statements_sql("SELECT 1 FROM t;\n-- trailing; DELETE FROM t\n"),
2174 vec!["SELECT 1 FROM t", "-- trailing; DELETE FROM t"]
2175 );
2176 assert_eq!(
2177 split_statements_sql("/* drop it; DELETE FROM t */ SELECT 1 FROM t"),
2178 vec!["/* drop it; DELETE FROM t */ SELECT 1 FROM t"]
2179 );
2180 }
2181
2182 #[test]
2185 fn a_semicolon_inside_a_string_is_not_a_boundary() {
2186 assert_eq!(
2187 split_statements_sql("INSERT INTO t VALUES ('a;b')"),
2188 vec!["INSERT INTO t VALUES ('a;b')"]
2189 );
2190 assert_eq!(
2191 split_statements_sql(r#"INSERT INTO t VALUES ("a;b")"#),
2192 vec![r#"INSERT INTO t VALUES ("a;b")"#]
2193 );
2194 assert_eq!(
2195 split_statements_sql("INSERT INTO t VALUES ('it''s; here')"),
2196 vec!["INSERT INTO t VALUES ('it''s; here')"]
2197 );
2198 assert_eq!(
2199 split_statements_sql(r#"INSERT INTO t VALUES ('a\';b')"#),
2200 vec![r#"INSERT INTO t VALUES ('a\';b')"#]
2201 );
2202 }
2203
2204 #[test]
2206 fn real_boundaries_still_split() {
2207 assert_eq!(
2208 split_statements_sql("SELECT 1 FROM t; SELECT 2 FROM t;"),
2209 vec!["SELECT 1 FROM t", "SELECT 2 FROM t"]
2210 );
2211 assert_eq!(split_statements_sql(";; ;"), Vec::<&str>::new());
2212 assert_eq!(split_statements_sql(""), Vec::<&str>::new());
2213 }
2214}