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