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 match chars[i + 1] {
433 '\'' => {
434 s.push('\'');
435 i += 2;
436 }
437 '"' => {
438 s.push('"');
439 i += 2;
440 }
441 '\\' => {
442 s.push('\\');
443 i += 2;
444 }
445 'n' => {
446 s.push('\n');
447 i += 2;
448 }
449 't' => {
450 s.push('\t');
451 i += 2;
452 }
453 'r' => {
454 s.push('\r');
455 i += 2;
456 }
457 '0' => {
458 s.push('\0');
459 i += 2;
460 }
461 'u' => {
462 let digits: String = chars
463 .get(i + 2..i + 6)
464 .map(|hex| hex.iter().collect())
465 .unwrap_or_default();
466 let scalar = (digits.len() == 4)
467 .then(|| u32::from_str_radix(&digits, 16).ok())
468 .flatten()
469 .and_then(char::from_u32);
470 match scalar {
471 Some(c) => {
472 s.push(c);
473 i += 6;
474 }
475 None => {
476 return Err(ParseError::Lex {
477 message: format!(
478 "invalid \\u escape in string literal: expected four \
479 hex digits naming a Unicode character, got \"{digits}\""
480 ),
481 position: i,
482 });
483 }
484 }
485 }
486 other => {
487 return Err(ParseError::Lex {
488 message: format!(
489 "unknown escape '\\{other}' in string literal; SQL \
490 supports \\' \\\" \\\\ \\n \\t \\r \\0 and \\uXXXX"
491 ),
492 position: i,
493 });
494 }
495 }
496 } else {
497 s.push(chars[i]);
498 i += 1;
499 }
500 }
501 if i > chars.len() || chars.get(i.saturating_sub(1)) != Some("e) {
502 return Err(ParseError::Lex {
503 message: if quote == '"' {
504 "unterminated quoted identifier".into()
505 } else {
506 "unterminated string".into()
507 },
508 position: i,
509 });
510 }
511 if quote == '"' {
512 if s.is_empty() {
526 return Err(ParseError::Syntax {
527 message: "empty quoted identifier".into(),
528 position: None,
529 });
530 }
531 if s.contains('`') {
532 return Err(ParseError::Unsupported {
533 feature: format!(
534 "quoted identifier `{s}` contains a backtick, which PowQL uses to \
535 quote identifiers and cannot escape"
536 ),
537 });
538 }
539 out.push(SqlTok::Word(format!("`{s}`")));
540 spans.push(tok_start);
541 continue;
542 }
543 out.push(SqlTok::String(s));
544 spans.push(tok_start);
545 continue;
546 }
547 if c == '$' {
548 i += 1;
549 let start = i;
550 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
551 i += 1;
552 }
553 out.push(SqlTok::Param(chars[start..i].iter().collect()));
554 spans.push(tok_start);
555 continue;
556 }
557 if c == '-' && chars.get(i + 1) == Some(&'>') {
559 if chars.get(i + 2) == Some(&'>') {
560 out.push(SqlTok::Op("->>".into()));
561 spans.push(tok_start);
562 i += 3;
563 } else {
564 out.push(SqlTok::Op("->".into()));
565 spans.push(tok_start);
566 i += 2;
567 }
568 continue;
569 }
570 if c.is_ascii_digit() || (c == '-' && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit()))
571 {
572 let start = i;
573 i += 1;
574 while i < chars.len() && chars[i].is_ascii_digit() {
575 i += 1;
576 }
577 if i < chars.len()
578 && chars[i] == '.'
579 && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit())
580 {
581 i += 1;
582 while i < chars.len() && chars[i].is_ascii_digit() {
583 i += 1;
584 }
585 }
586 if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
590 let mut look = i + 1;
591 if chars.get(look).is_some_and(|c| *c == '+' || *c == '-') {
592 look += 1;
593 }
594 if chars.get(look).is_some_and(char::is_ascii_digit) {
595 i = look;
596 while i < chars.len() && chars[i].is_ascii_digit() {
597 i += 1;
598 }
599 }
600 }
601 out.push(SqlTok::Number(chars[start..i].iter().collect()));
602 spans.push(tok_start);
603 continue;
604 }
605 if c.is_alphabetic() || c == '_' {
606 let start = i;
607 i += 1;
608 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
609 i += 1;
610 }
611 out.push(SqlTok::Word(chars[start..i].iter().collect()));
612 spans.push(tok_start);
613 continue;
614 }
615 if matches!(c, '(' | ')' | ',' | '*' | '.') {
616 out.push(SqlTok::Symbol(c));
617 spans.push(tok_start);
618 i += 1;
619 continue;
620 }
621 if matches!(c, '=' | '<' | '>' | '!') {
622 let mut op = String::new();
623 op.push(c);
624 if matches!(chars.get(i + 1), Some('=') | Some('>')) {
625 op.push(chars[i + 1]);
626 i += 2;
627 } else {
628 i += 1;
629 }
630 if op == "<>" {
631 op = "!=".into();
632 }
633 out.push(SqlTok::Op(op));
634 spans.push(tok_start);
635 continue;
636 }
637 if matches!(c, '+' | '-' | '/') {
638 out.push(SqlTok::Op(c.to_string()));
639 spans.push(tok_start);
640 i += 1;
641 continue;
642 }
643 return Err(ParseError::Lex {
644 message: format!("unexpected SQL character `{c}`"),
645 position: i,
646 });
647 }
648 Ok((out, spans))
649}
650
651const MAX_SQL_NESTING_DEPTH: usize = 64;
665
666fn attach_sql_failing_position(
672 error: ParseError,
673 parser: &SqlParser,
674 spans: &[usize],
675) -> ParseError {
676 let offset = spans
677 .get(parser.pos.min(spans.len().saturating_sub(1)))
678 .copied();
679 match error {
680 ParseError::UnexpectedToken {
681 expected,
682 got,
683 position: None,
684 } => ParseError::UnexpectedToken {
685 expected,
686 got,
687 position: offset,
688 },
689 ParseError::Syntax {
690 message,
691 position: None,
692 } => ParseError::Syntax {
693 message,
694 position: offset,
695 },
696 other => other,
697 }
698}
699
700struct SqlParser {
701 toks: Vec<SqlTok>,
702 pos: usize,
703 depth: usize,
704 qual_ctx: QualCtx,
708}
709
710#[derive(Clone, PartialEq)]
717enum QualCtx {
718 None,
720 Single { visible_name: String },
724 Joined,
726}
727
728struct Projection {
730 text: String,
733 agg: Option<AggCall>,
737}
738
739struct AggCall {
741 func: String,
743 arg: AggArg,
744}
745
746enum AggArg {
747 Star,
749 Field(String),
751}
752
753impl AggCall {
754 fn canonical(&self) -> String {
756 match &self.arg {
757 AggArg::Star => format!("{}(*)", self.func),
758 AggArg::Field(f) => format!("{}({f})", self.func),
759 }
760 }
761
762 fn sql_spelling(&self) -> String {
767 match &self.arg {
768 AggArg::Star => format!("{}(*)", self.func),
769 AggArg::Field(f) => format!("{}({})", self.func, f.strip_prefix('.').unwrap_or(f)),
770 }
771 }
772}
773
774fn build_ungrouped_aggregate(agg: &AggCall, inner: &str) -> Result<String, ParseError> {
780 match agg.func.as_str() {
781 "count" if matches!(agg.arg, AggArg::Star) => Ok(format!("count({inner})")),
782 "count" | "sum" | "avg" | "min" | "max" => match &agg.arg {
783 AggArg::Field(f) => Ok(format!("{}({inner} {{ {f} }})", agg.func)),
784 AggArg::Star => Err(ParseError::Unsupported {
785 feature: format!("{0}(*) is not valid; {0}() needs a column", agg.func),
786 }),
787 },
788 other => Err(ParseError::Syntax {
790 message: format!("unknown aggregate function `{other}`"),
791 position: None,
792 }),
793 }
794}
795
796impl SqlParser {
797 fn at_end(&self) -> bool {
798 self.pos >= self.toks.len()
799 }
800 fn peek(&self) -> Option<&SqlTok> {
801 self.toks.get(self.pos)
802 }
803 fn bump(&mut self) -> Option<SqlTok> {
804 let t = self.toks.get(self.pos).cloned();
805 if t.is_some() {
806 self.pos += 1;
807 }
808 t
809 }
810 fn is_kw(&self, kw: &str) -> bool {
811 matches!(self.peek(), Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case(kw))
812 }
813 fn eat_kw(&mut self, kw: &str) -> bool {
814 if self.is_kw(kw) {
815 self.pos += 1;
816 true
817 } else {
818 false
819 }
820 }
821 fn expect_kw(&mut self, kw: &str) -> Result<(), ParseError> {
822 if self.eat_kw(kw) {
823 Ok(())
824 } else {
825 Err(ParseError::UnexpectedToken {
826 expected: kw.into(),
827 got: self
828 .peek()
829 .map(|t| t.display())
830 .unwrap_or_else(|| "<eof>".into()),
831 position: None,
832 })
833 }
834 }
835 fn eat_sym(&mut self, c: char) -> bool {
836 if matches!(self.peek(), Some(SqlTok::Symbol(got)) if *got == c) {
837 self.pos += 1;
838 true
839 } else {
840 false
841 }
842 }
843 fn expect_sym(&mut self, c: char) -> Result<(), ParseError> {
844 if self.eat_sym(c) {
845 Ok(())
846 } else {
847 Err(ParseError::UnexpectedToken {
848 expected: c.to_string(),
849 got: self
850 .peek()
851 .map(|t| t.display())
852 .unwrap_or_else(|| "<eof>".into()),
853 position: None,
854 })
855 }
856 }
857 fn expect_ident(&mut self, what: &str) -> Result<String, ParseError> {
858 match self.bump() {
859 Some(SqlTok::Word(w)) if !is_reserved_identifier(&w) => Ok(w),
860 Some(SqlTok::Word(w)) => Err(ParseError::Syntax {
861 message: format!("expected {what}, got reserved word `{w}`"),
862 position: None,
863 }),
864 Some(t) => Err(ParseError::UnexpectedToken {
865 expected: what.into(),
866 got: t.display(),
867 position: None,
868 }),
869 None => Err(ParseError::UnexpectedToken {
870 expected: what.into(),
871 got: "<eof>".into(),
872 position: None,
873 }),
874 }
875 }
876
877 fn statement(&mut self) -> Result<String, ParseError> {
878 if self.is_kw("select") {
879 self.select()
880 } else if self.is_kw("insert") {
881 self.insert()
882 } else if self.is_kw("update") {
883 self.update()
884 } else if self.is_kw("delete") {
885 self.delete()
886 } else if self.is_kw("create") {
887 self.create()
888 } else if self.is_kw("drop") {
889 self.drop_stmt()
890 } else if self.is_kw("alter") {
891 self.alter()
892 } else if self.eat_kw("begin") {
893 let _ = self.eat_kw("transaction");
894 Ok("begin".into())
895 } else if self.eat_kw("commit") {
896 Ok("commit".into())
897 } else if self.eat_kw("rollback") {
898 Ok("rollback".into())
899 } else {
900 Err(ParseError::UnexpectedToken {
901 expected: "SQL statement".into(),
902 got: self
903 .peek()
904 .map(|t| t.display())
905 .unwrap_or_else(|| "<eof>".into()),
906 position: None,
907 })
908 }
909 }
910
911 fn scan_select_qual_ctx(&self) -> QualCtx {
917 let mut i = self.pos;
918 let mut depth = 0usize;
919 loop {
920 match self.toks.get(i) {
921 None => return QualCtx::None,
922 Some(SqlTok::Symbol('(')) => depth += 1,
923 Some(SqlTok::Symbol(')')) => depth = depth.saturating_sub(1),
924 Some(SqlTok::Word(w)) if depth == 0 && w.eq_ignore_ascii_case("from") => break,
925 Some(_) => {}
926 }
927 i += 1;
928 }
929 let Some(SqlTok::Word(table)) = self.toks.get(i + 1) else {
930 return QualCtx::None;
932 };
933 let mut visible = table.clone();
934 let mut j = i + 2;
935 match self.toks.get(j) {
938 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("as") => {
939 if let Some(SqlTok::Word(a)) = self.toks.get(j + 1) {
940 visible = a.clone();
941 j += 2;
942 }
943 }
944 Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w) => {
945 visible = w.clone();
946 j += 1;
947 }
948 _ => {}
949 }
950 match self.toks.get(j) {
951 Some(SqlTok::Word(w)) if is_join_modifier(w) => QualCtx::Joined,
952 _ => QualCtx::Single {
953 visible_name: visible,
954 },
955 }
956 }
957
958 fn select(&mut self) -> Result<String, ParseError> {
959 self.expect_kw("select")?;
960 self.qual_ctx = self.scan_select_qual_ctx();
961 let distinct = self.eat_kw("distinct");
962 let projection = self.projection_list()?;
963 self.expect_kw("from")?;
964 let source = self.table_ref()?;
965 let mut joins = Vec::new();
966 while self.starts_join() {
967 joins.push(self.join_clause()?);
968 }
969 let filter = if self.eat_kw("where") {
970 Some(self.expr_until(&["group", "having", "order", "limit", "offset"])?)
971 } else {
972 None
973 };
974 let group = if self.eat_kw("group") {
975 self.expect_kw("by")?;
976 Some(self.expression_list_until(&["having", "order", "limit", "offset"])?)
977 } else {
978 None
979 };
980 let having = if self.eat_kw("having") {
981 Some(self.expr_until(&["order", "limit", "offset"])?)
982 } else {
983 None
984 };
985 let order = if self.eat_kw("order") {
986 self.expect_kw("by")?;
987 Some(self.order_list_until(&["limit", "offset"])?)
988 } else {
989 None
990 };
991 let limit = if self.eat_kw("limit") {
992 Some(self.expr_until(&["offset"])?)
993 } else {
994 None
995 };
996 let offset = if self.eat_kw("offset") {
997 Some(self.expr_until(&[])?)
998 } else {
999 None
1000 };
1001
1002 let has_group = group.is_some();
1003
1004 let mut out = source;
1005 for j in joins {
1006 out.push(' ');
1007 out.push_str(&j);
1008 }
1009 if distinct {
1010 out.push_str(" distinct");
1011 }
1012 if let Some(f) = filter {
1013 out.push_str(" filter ");
1014 out.push_str(&f);
1015 }
1016 if let Some(keys) = group {
1017 out.push_str(" group ");
1018 out.push_str(&keys.join(", "));
1019 if let Some(h) = having {
1020 out.push_str(" having ");
1021 out.push_str(&h);
1022 }
1023 } else if having.is_some() {
1024 return Err(ParseError::Syntax {
1025 message: "HAVING requires GROUP BY".into(),
1026 position: None,
1027 });
1028 }
1029 if let Some(o) = order {
1030 out.push_str(" order ");
1031 out.push_str(&o);
1032 }
1033 if let Some(l) = limit {
1034 out.push_str(" limit ");
1035 out.push_str(&l);
1036 }
1037 if let Some(o) = offset {
1038 out.push_str(" offset ");
1039 out.push_str(&o);
1040 }
1041 if let Some(items) = projection {
1042 if !has_group && items.iter().any(|p| p.agg.is_some()) {
1047 if distinct {
1048 return Err(ParseError::Unsupported {
1049 feature: "aggregates with DISTINCT and no GROUP BY are not supported by the SQL frontend".into(),
1050 });
1051 }
1052 if items.len() != 1 {
1053 return Err(ParseError::Unsupported {
1054 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(),
1055 });
1056 }
1057 let agg = items.into_iter().next().unwrap().agg.unwrap();
1059 return build_ungrouped_aggregate(&agg, &out);
1060 }
1061 out.push_str(" { ");
1062 out.push_str(
1063 &items
1064 .iter()
1065 .map(|p| p.text.as_str())
1066 .collect::<Vec<_>>()
1067 .join(", "),
1068 );
1069 out.push_str(" }");
1070 }
1071 Ok(out)
1072 }
1073
1074 fn projection_list(&mut self) -> Result<Option<Vec<Projection>>, ParseError> {
1075 if self.eat_sym('*') {
1076 return Ok(None);
1077 }
1078 let mut fields = Vec::new();
1079 loop {
1080 let (expr, agg) = match self.try_aggregate()? {
1085 Some(a) => (a.canonical(), Some(a)),
1086 None => (self.expr_until(&["from", "as"])?, None),
1087 };
1088 let text = if self.eat_kw("as") {
1089 let alias = self.expect_ident("projection alias")?;
1090 format!("{alias}: {expr}")
1091 } else if let Some(call) = &agg {
1092 format!("`{}`: {expr}", call.sql_spelling())
1095 } else {
1096 expr
1097 };
1098 fields.push(Projection { text, agg });
1099 if !self.eat_sym(',') {
1100 break;
1101 }
1102 }
1103 Ok(Some(fields))
1104 }
1105
1106 fn try_aggregate(&mut self) -> Result<Option<AggCall>, ParseError> {
1111 let Some(SqlTok::Word(w)) = self.peek().cloned() else {
1112 return Ok(None);
1113 };
1114 let func = w.to_ascii_lowercase();
1115 if !matches!(func.as_str(), "count" | "sum" | "avg" | "min" | "max") {
1116 return Ok(None);
1117 }
1118 let save = self.pos;
1119 self.pos += 1; if !self.eat_sym('(') {
1121 self.pos = save;
1122 return Ok(None);
1123 }
1124 let arg = if func == "count" && self.eat_sym('*') {
1125 AggArg::Star
1126 } else if func == "count" && self.is_kw("distinct") {
1127 self.pos = save;
1131 return Ok(None);
1132 } else {
1133 AggArg::Field(self.expr_bp(0, &[])?)
1134 };
1135 if self.eat_sym(')')
1138 && (matches!(self.peek(), Some(SqlTok::Symbol(',')))
1139 || self.is_kw("as")
1140 || self.is_kw("from"))
1141 {
1142 Ok(Some(AggCall { func, arg }))
1143 } else {
1144 self.pos = save;
1145 Ok(None)
1146 }
1147 }
1148
1149 fn table_ref(&mut self) -> Result<String, ParseError> {
1150 let table = self.expect_ident("table name")?;
1151 let has_alias = self.eat_kw("as")
1152 || matches!(self.peek(), Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w));
1153 if has_alias {
1154 let alias = self.expect_ident("table alias")?;
1155 Ok(format!("{table} as {alias}"))
1156 } else {
1157 Ok(table)
1158 }
1159 }
1160
1161 fn starts_join(&self) -> bool {
1162 self.is_kw("join")
1163 || self.is_kw("inner")
1164 || self.is_kw("left")
1165 || self.is_kw("right")
1166 || self.is_kw("cross")
1167 }
1168
1169 fn join_clause(&mut self) -> Result<String, ParseError> {
1170 let kind = if self.eat_kw("inner") {
1171 self.expect_kw("join")?;
1172 "inner join"
1173 } else if self.eat_kw("left") {
1174 let _ = self.eat_kw("outer");
1175 self.expect_kw("join")?;
1176 "left join"
1177 } else if self.eat_kw("right") {
1178 let _ = self.eat_kw("outer");
1179 self.expect_kw("join")?;
1180 "right join"
1181 } else if self.eat_kw("cross") {
1182 self.expect_kw("join")?;
1183 "cross join"
1184 } else {
1185 self.expect_kw("join")?;
1186 "inner join"
1187 };
1188 let table = self.table_ref()?;
1189 if kind == "cross join" {
1190 return Ok(format!("{kind} {table}"));
1191 }
1192 self.expect_kw("on")?;
1193 let on = self.expr_until(&[
1194 "join", "inner", "left", "right", "cross", "where", "group", "having", "order",
1195 "limit", "offset",
1196 ])?;
1197 Ok(format!("{kind} {table} on {on}"))
1198 }
1199
1200 fn insert(&mut self) -> Result<String, ParseError> {
1201 self.expect_kw("insert")?;
1202 self.expect_kw("into")?;
1203 let table = self.expect_ident("table name")?;
1204 self.expect_sym('(')?;
1205 let mut cols = Vec::new();
1206 loop {
1207 cols.push(self.expect_ident("column name")?);
1208 if !self.eat_sym(',') {
1209 break;
1210 }
1211 }
1212 self.expect_sym(')')?;
1213 self.expect_kw("values")?;
1214 let mut rows = Vec::new();
1215 loop {
1216 self.expect_sym('(')?;
1217 let mut vals = Vec::new();
1218 loop {
1219 vals.push(self.expr_until(&[])?);
1220 if !self.eat_sym(',') {
1221 break;
1222 }
1223 }
1224 self.expect_sym(')')?;
1225 if vals.len() != cols.len() {
1226 return Err(ParseError::Syntax {
1227 message: format!(
1228 "INSERT has {} column(s) but {} value(s)",
1229 cols.len(),
1230 vals.len()
1231 ),
1232 position: None,
1233 });
1234 }
1235 let assigns = cols
1236 .iter()
1237 .zip(vals)
1238 .map(|(c, v)| format!("{c} := {v}"))
1239 .collect::<Vec<_>>();
1240 rows.push(format!("{{ {} }}", assigns.join(", ")));
1241 if !self.eat_sym(',') {
1242 break;
1243 }
1244 }
1245 let mut out = format!("insert {table} {}", rows.join(", "));
1246 if self.returning_clause()? {
1247 out.push_str(" returning");
1248 }
1249 Ok(out)
1250 }
1251
1252 fn update(&mut self) -> Result<String, ParseError> {
1253 self.expect_kw("update")?;
1254 let table = self.expect_ident("table name")?;
1255 self.qual_ctx = QualCtx::Single {
1256 visible_name: table.clone(),
1257 };
1258 self.expect_kw("set")?;
1259 let assigns = self.assignment_list_until(&["where", "returning"])?;
1260 let filter = if self.eat_kw("where") {
1261 Some(self.expr_until(&["returning"])?)
1262 } else {
1263 None
1264 };
1265 let mut out = table;
1266 if let Some(f) = filter {
1267 out.push_str(" filter ");
1268 out.push_str(&f);
1269 }
1270 out.push_str(" update { ");
1271 out.push_str(&assigns.join(", "));
1272 out.push_str(" }");
1273 if self.returning_clause()? {
1274 out.push_str(" returning");
1275 }
1276 Ok(out)
1277 }
1278
1279 fn delete(&mut self) -> Result<String, ParseError> {
1280 self.expect_kw("delete")?;
1281 self.expect_kw("from")?;
1282 let table = self.expect_ident("table name")?;
1283 self.qual_ctx = QualCtx::Single {
1284 visible_name: table.clone(),
1285 };
1286 let filter = if self.eat_kw("where") {
1287 Some(self.expr_until(&["returning"])?)
1288 } else {
1289 None
1290 };
1291 let mut out = table;
1292 if let Some(f) = filter {
1293 out.push_str(" filter ");
1294 out.push_str(&f);
1295 }
1296 out.push_str(" delete");
1297 if self.returning_clause()? {
1298 out.push_str(" returning");
1299 }
1300 Ok(out)
1301 }
1302
1303 fn default_literal(&mut self) -> Result<String, ParseError> {
1307 match self.bump() {
1308 Some(SqlTok::Number(n)) => Ok(n),
1309 Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
1310 Some(SqlTok::Word(w))
1311 if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
1312 {
1313 Ok(w.to_ascii_lowercase())
1314 }
1315 other => Err(ParseError::Syntax {
1316 message: format!(
1317 "DEFAULT requires a literal value, got {}",
1318 other.map(|t| t.display()).unwrap_or_else(|| "<eof>".into())
1319 ),
1320 position: None,
1321 }),
1322 }
1323 }
1324
1325 fn returning_clause(&mut self) -> Result<bool, ParseError> {
1330 if !self.eat_kw("returning") {
1331 return Ok(false);
1332 }
1333 if !self.eat_sym('*') {
1334 return Err(ParseError::Syntax {
1335 message: "RETURNING currently supports only `RETURNING *` \
1336 (column projection is not yet supported)"
1337 .into(),
1338 position: None,
1339 });
1340 }
1341 Ok(true)
1342 }
1343
1344 fn create(&mut self) -> Result<String, ParseError> {
1345 self.expect_kw("create")?;
1346 if self.eat_kw("table") {
1347 let table = self.expect_ident("table name")?;
1348 self.expect_sym('(')?;
1349 let mut fields = Vec::new();
1350 while !self.eat_sym(')') {
1351 if self.is_kw("primary") {
1357 return Err(ParseError::Unsupported {
1358 feature: "PRIMARY KEY is supported only on the column it keys; \
1359 write it after the column's type, as `id int PRIMARY KEY`"
1360 .into(),
1361 });
1362 }
1363 if self.is_kw("foreign")
1364 || self.is_kw("constraint")
1365 || self.is_kw("unique")
1366 || self.is_kw("check")
1367 {
1368 return Err(ParseError::Unsupported { feature: "SQL table constraints are not supported; declare UNIQUE columns or add indexes explicitly".into() });
1369 }
1370 let name = self.expect_ident("column name")?;
1371 let ty = self.sql_type()?;
1372 let mut required = false;
1373 let mut unique = false;
1374 let mut auto = false;
1375 let mut default: Option<String> = None;
1376 loop {
1377 if self.eat_kw("not") {
1378 self.expect_kw("null")?;
1379 required = true;
1380 } else if self.eat_kw("unique") {
1381 unique = true;
1382 } else if self.eat_kw("primary") {
1383 self.expect_kw("key")?;
1388 required = true;
1389 unique = true;
1390 } else if self.eat_kw("autoincrement") || self.eat_kw("auto_increment") {
1391 auto = true;
1392 } else if self.eat_kw("default") {
1393 default = Some(self.default_literal()?);
1394 } else if self.eat_kw("null") {
1395 } else {
1396 break;
1397 }
1398 }
1399 let mut mods = Vec::new();
1400 if required {
1401 mods.push("required");
1402 }
1403 if unique {
1404 mods.push("unique");
1405 }
1406 if auto {
1407 mods.push("auto");
1408 }
1409 let prefix = if mods.is_empty() {
1410 String::new()
1411 } else {
1412 format!("{} ", mods.join(" "))
1413 };
1414 let suffix = match default {
1415 Some(lit) => format!(" default {lit}"),
1416 None => String::new(),
1417 };
1418 fields.push(format!("{prefix}{name}: {ty}{suffix}"));
1419 let _ = self.eat_sym(',');
1420 }
1421 return Ok(format!("type {table} {{ {} }}", fields.join(", ")));
1422 }
1423 let unique = self.eat_kw("unique");
1424 self.expect_kw("index")?;
1425 let _idx = self.expect_ident("index name")?;
1426 self.expect_kw("on")?;
1427 let table = self.expect_ident("table name")?;
1428 self.expect_sym('(')?;
1429 let expression_parenthesized = self.eat_sym('(');
1430 if !matches!(self.peek(), Some(SqlTok::Word(_))) {
1431 return Err(ParseError::Unsupported {
1432 feature: "SQL expression indexes are not supported; use PowQL `alter <table> add index (.<json-column>-><path>)`"
1433 .into(),
1434 });
1435 }
1436 let col = self.expect_ident("column name")?;
1437 let mut path = format!(".{col}");
1438 let mut has_json_path = false;
1439 loop {
1440 match self.peek() {
1441 Some(SqlTok::Op(operator)) if operator == "->" => {
1442 self.bump();
1443 has_json_path = true;
1444 match self.bump() {
1445 Some(SqlTok::String(key)) => {
1446 path.push_str("->");
1447 path.push_str("e_powql_string(&key));
1448 }
1449 Some(SqlTok::Number(index))
1450 if !index.starts_with('-')
1451 && !index.contains('.')
1452 && index.parse::<u32>().is_ok() =>
1453 {
1454 path.push_str("->");
1455 path.push_str(&index);
1456 }
1457 Some(segment) => {
1458 return Err(ParseError::Unsupported {
1459 feature: format!(
1460 "SQL JSON expression indexes require string keys or non-negative integer path segments after ->, got {}",
1461 segment.display()
1462 ),
1463 });
1464 }
1465 None => {
1466 return Err(ParseError::UnexpectedToken {
1467 expected: "JSON path segment after ->".into(),
1468 got: "<eof>".into(),
1469 position: None,
1470 });
1471 }
1472 }
1473 }
1474 Some(SqlTok::Op(operator)) if operator == "->>" => {
1475 return Err(ParseError::Unsupported {
1476 feature:
1477 "SQL ->> text expressions cannot be indexed; use a direct JSON -> path"
1478 .into(),
1479 });
1480 }
1481 _ => break,
1482 }
1483 }
1484 if expression_parenthesized && !has_json_path {
1485 return Err(ParseError::Unsupported {
1486 feature: "SQL expression indexes support only direct JSON -> paths; use a plain column without extra parentheses"
1487 .into(),
1488 });
1489 }
1490 if expression_parenthesized && !self.eat_sym(')') {
1491 return Err(ParseError::Unsupported {
1492 feature: "SQL expression indexes support only direct JSON -> paths".into(),
1493 });
1494 }
1495 if !self.eat_sym(')') {
1496 return Err(ParseError::Unsupported {
1497 feature: "SQL expression and multi-column indexes are not supported; use PowQL `alter <table> add index (.<json-column>-><path>)` for a JSON path"
1498 .into(),
1499 });
1500 }
1501 Ok(if unique {
1502 if has_json_path {
1503 format!("alter {table} add unique ({path})")
1504 } else {
1505 format!("alter {table} add unique .{col}")
1506 }
1507 } else {
1508 if has_json_path {
1509 format!("alter {table} add index ({path})")
1510 } else {
1511 format!("alter {table} add index .{col}")
1512 }
1513 })
1514 }
1515
1516 fn drop_stmt(&mut self) -> Result<String, ParseError> {
1517 self.expect_kw("drop")?;
1518 if self.eat_kw("table") {
1519 let table = self.expect_ident("table name")?;
1520 Ok(format!("drop {table}"))
1521 } else if self.eat_kw("view") {
1522 let view = self.expect_ident("view name")?;
1523 Ok(format!("drop view {view}"))
1524 } else {
1525 Err(ParseError::UnexpectedToken {
1526 expected: "TABLE or VIEW".into(),
1527 got: self
1528 .peek()
1529 .map(|t| t.display())
1530 .unwrap_or_else(|| "<eof>".into()),
1531 position: None,
1532 })
1533 }
1534 }
1535
1536 fn alter(&mut self) -> Result<String, ParseError> {
1537 self.expect_kw("alter")?;
1538 self.expect_kw("table")?;
1539 let table = self.expect_ident("table name")?;
1540 if self.eat_kw("add") {
1541 let _ = self.eat_kw("column");
1542 let name = self.expect_ident("column name")?;
1543 let ty = self.sql_type()?;
1544 let mut required = false;
1545 if self.eat_kw("not") {
1546 self.expect_kw("null")?;
1547 required = true;
1548 }
1549 let prefix = if required { "required " } else { "" };
1550 Ok(format!("alter {table} add column {prefix}{name}: {ty}"))
1551 } else if self.eat_kw("drop") {
1552 let _ = self.eat_kw("column");
1553 let name = self.expect_ident("column name")?;
1554 Ok(format!("alter {table} drop column {name}"))
1555 } else {
1556 Err(ParseError::UnexpectedToken {
1557 expected: "ADD or DROP".into(),
1558 got: self
1559 .peek()
1560 .map(|t| t.display())
1561 .unwrap_or_else(|| "<eof>".into()),
1562 position: None,
1563 })
1564 }
1565 }
1566
1567 fn sql_type(&mut self) -> Result<String, ParseError> {
1568 let raw = self.expect_ident("type name")?;
1569 if self.eat_sym('(') {
1571 while !self.eat_sym(')') {
1572 if self.at_end() {
1573 return Err(ParseError::Syntax {
1574 message: "unterminated SQL type length".into(),
1575 position: None,
1576 });
1577 }
1578 self.bump();
1579 }
1580 }
1581 let ty = match raw.to_ascii_lowercase().as_str() {
1582 "text" | "varchar" | "char" | "string" | "str" => "str",
1583 "int" | "integer" | "bigint" | "smallint" => "int",
1584 "real" | "double" | "float" | "decimal" | "numeric" => "float",
1585 "bool" | "boolean" => "bool",
1586 "datetime" | "timestamp" => "datetime",
1587 "uuid" => "uuid",
1588 "blob" | "bytes" | "bytea" => "bytes",
1589 "json" | "jsonb" => "json",
1592 other => {
1593 return Err(ParseError::Unsupported {
1594 feature: format!("unsupported SQL type `{other}`"),
1595 })
1596 }
1597 };
1598 Ok(ty.into())
1599 }
1600
1601 fn assignment_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
1602 let mut out = Vec::new();
1603 loop {
1604 let name = self.expect_ident("column name")?;
1605 match self.bump() {
1606 Some(SqlTok::Op(op)) if op == "=" => {}
1607 Some(SqlTok::Op(op)) if op == "->" || op == "->>" => {
1613 return Err(ParseError::Unsupported {
1614 feature: format!(
1615 "cannot assign to a JSON path target `{name}{op}...`: JSON path \
1616 assignment targets are not supported; write the whole JSON column \
1617 instead (path mutation such as json_set is not yet available)"
1618 ),
1619 })
1620 }
1621 Some(t) => {
1622 return Err(ParseError::UnexpectedToken {
1623 expected: "=".into(),
1624 got: t.display(),
1625 position: None,
1626 })
1627 }
1628 None => {
1629 return Err(ParseError::UnexpectedToken {
1630 expected: "=".into(),
1631 got: "<eof>".into(),
1632 position: None,
1633 })
1634 }
1635 }
1636 let v = self.expr_until(stop)?;
1637 out.push(format!("{name} := {v}"));
1638 if !self.eat_sym(',') {
1639 break;
1640 }
1641 }
1642 Ok(out)
1643 }
1644
1645 fn expression_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
1646 let mut expressions = Vec::new();
1647 loop {
1648 expressions.push(self.expr_until(stop)?);
1649 if !self.eat_sym(',') || self.next_is_stop(stop) {
1650 break;
1651 }
1652 }
1653 Ok(expressions)
1654 }
1655
1656 fn order_list_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
1657 let mut parts = Vec::new();
1658 let mut expression_stop = Vec::with_capacity(stop.len() + 2);
1659 expression_stop.extend_from_slice(stop);
1660 expression_stop.extend_from_slice(&["asc", "desc"]);
1661 loop {
1662 let mut p = self.expr_until(&expression_stop)?;
1663 if self.eat_kw("desc") {
1664 p.push_str(" desc");
1665 } else if self.eat_kw("asc") {
1666 p.push_str(" asc");
1667 }
1668 parts.push(p);
1669 if !self.eat_sym(',') || self.next_is_stop(stop) {
1670 break;
1671 }
1672 }
1673 Ok(parts.join(", "))
1674 }
1675
1676 fn expr_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
1677 self.expr_bp(0, stop)
1678 }
1679
1680 fn expr_bp(&mut self, min_bp: u8, stop: &[&str]) -> Result<String, ParseError> {
1681 self.depth += 1;
1685 if self.depth > MAX_SQL_NESTING_DEPTH {
1686 return Err(ParseError::NestingDepthExceeded {
1687 max: MAX_SQL_NESTING_DEPTH,
1688 });
1689 }
1690 let mut lhs = if self.eat_kw("not") {
1691 format!("not ({})", self.expr_bp(5, stop)?)
1697 } else if self.eat_kw("exists") {
1698 if self.eat_sym('(') {
1699 if self.is_kw("select") {
1700 return Err(ParseError::Unsupported {
1701 feature:
1702 "SQL EXISTS subqueries are not supported yet; use PowQL EXISTS for now"
1703 .into(),
1704 });
1705 }
1706 return Err(ParseError::Syntax {
1707 message: "expected subquery after EXISTS".into(),
1708 position: None,
1709 });
1710 }
1711 return Err(ParseError::Syntax {
1712 message: "expected EXISTS (...)".into(),
1713 position: None,
1714 });
1715 } else if self.eat_sym('(') {
1716 if self.is_kw("select") {
1717 return Err(ParseError::Unsupported {
1718 feature:
1719 "SQL scalar subqueries are not supported yet; use PowQL subqueries for now"
1720 .into(),
1721 });
1722 }
1723 let inner = self.expr_bp(0, stop)?;
1724 self.expect_sym(')')?;
1725 format!("({inner})")
1726 } else {
1727 self.primary_expr()?
1728 };
1729
1730 let mut chain = 0usize;
1733 loop {
1734 if self.next_is_stop(stop)
1735 || self.at_end()
1736 || matches!(self.peek(), Some(SqlTok::Symbol(')' | ',')))
1737 {
1738 break;
1739 }
1740 chain += 1;
1741 if self.depth + chain > MAX_SQL_NESTING_DEPTH {
1742 return Err(ParseError::NestingDepthExceeded {
1743 max: MAX_SQL_NESTING_DEPTH,
1744 });
1745 }
1746 if matches!(self.peek(), Some(SqlTok::Op(op)) if op == "->" || op == "->>") {
1747 let text = matches!(self.bump(), Some(SqlTok::Op(op)) if op == "->>");
1748 let segment = match self.bump() {
1749 Some(SqlTok::String(key)) => quote_powql_string(&key),
1750 Some(SqlTok::Number(index))
1751 if !index.starts_with('-') && !index.contains('.') =>
1752 {
1753 index
1754 }
1755 Some(token) => {
1756 return Err(ParseError::Syntax {
1757 message: format!(
1758 "SQL JSON arrows require a string key or non-negative integer index, got {}",
1759 token.display()
1760 ),
1761 position: None,
1762 });
1763 }
1764 None => {
1765 return Err(ParseError::UnexpectedToken {
1766 expected: "JSON object key or array index".into(),
1767 got: "<eof>".into(),
1768 position: None,
1769 });
1770 }
1771 };
1772 let path = format!("{lhs}->{segment}");
1773 lhs = if text {
1774 format!("json_text({path})")
1775 } else {
1776 path
1777 };
1778 continue;
1779 }
1780 if self.eat_kw("is") {
1781 let not = self.eat_kw("not");
1782 self.expect_kw("null")?;
1783 lhs = if not {
1784 format!("{lhs} != null")
1785 } else {
1786 format!("{lhs} = null")
1787 };
1788 continue;
1789 }
1790 if self.eat_kw("not") {
1791 if self.eat_kw("in") {
1792 return Err(ParseError::Unsupported {
1793 feature:
1794 "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1795 .into(),
1796 });
1797 }
1798 if self.eat_kw("like") {
1799 let rhs = self.expr_bp(6, stop)?;
1800 lhs = format!("{lhs} not like {rhs}");
1801 continue;
1802 }
1803 if self.eat_kw("between") {
1804 return Err(ParseError::Unsupported {
1805 feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1806 });
1807 }
1808 return Err(ParseError::UnexpectedToken {
1809 expected: "IN, LIKE, or BETWEEN after NOT".into(),
1810 got: self
1811 .peek()
1812 .map(|t| t.display())
1813 .unwrap_or_else(|| "<eof>".into()),
1814 position: None,
1815 });
1816 }
1817 if self.eat_kw("in") {
1818 return Err(ParseError::Unsupported {
1819 feature: "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1820 .into(),
1821 });
1822 }
1823 if self.eat_kw("between") {
1824 return Err(ParseError::Unsupported {
1825 feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1826 });
1827 }
1828 if self.is_kw("over") {
1834 return Err(ParseError::Unsupported {
1835 feature: "SQL window functions (OVER) are not supported yet in the SQL \
1836 frontend; PowQL has them: row_number() over (order .col)"
1837 .into(),
1838 });
1839 }
1840 if self.eat_kw("like") {
1841 let (l_bp, r_bp) = (5, 6);
1842 if l_bp < min_bp {
1843 self.pos -= 1;
1844 break;
1845 }
1846 let rhs = self.expr_bp(r_bp, stop)?;
1847 lhs = format!("{lhs} like {rhs}");
1848 continue;
1849 }
1850
1851 let op = if self.eat_kw("or") {
1852 "or".to_string()
1853 } else if self.eat_kw("and") {
1854 "and".to_string()
1855 } else if let Some(SqlTok::Op(op)) = self.peek().cloned() {
1856 self.pos += 1;
1857 op
1858 } else if self.eat_sym('*') {
1859 "*".into()
1860 } else {
1861 break;
1862 };
1863 let (l_bp, r_bp) = infix_bp(&op).ok_or_else(|| ParseError::Syntax {
1864 message: format!("unsupported SQL operator `{op}`"),
1865 position: None,
1866 })?;
1867 if l_bp < min_bp {
1868 self.pos -= 1;
1869 break;
1870 }
1871 let rhs = self.expr_bp(r_bp, stop)?;
1872 if rhs == "null" && matches!(op.as_str(), "=" | "!=" | "<>") {
1888 lhs = "false".to_string();
1889 continue;
1890 }
1891 lhs = format!("{lhs} {op} {rhs}");
1892 }
1893 self.depth -= 1;
1894 Ok(lhs)
1895 }
1896
1897 fn primary_expr(&mut self) -> Result<String, ParseError> {
1898 match self.bump() {
1899 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("null") => Ok("null".into()),
1900 Some(SqlTok::Word(w))
1901 if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
1902 {
1903 Ok(w.to_ascii_lowercase())
1904 }
1905 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("case") => {
1911 Err(ParseError::Unsupported {
1912 feature: "SQL CASE/WHEN is not supported yet in the SQL frontend; \
1913 PowQL has it: case when <cond> then <value> else <value> end"
1914 .into(),
1915 })
1916 }
1917 Some(SqlTok::Word(w)) => {
1918 if self.eat_sym('(') {
1919 let func = w.to_ascii_lowercase();
1920 if func == "count" && self.eat_sym('*') {
1921 self.expect_sym(')')?;
1922 return Ok("count(*)".into());
1923 }
1924 if func == "coalesce" {
1933 return Err(ParseError::Unsupported {
1934 feature: "SQL COALESCE is not supported yet in the SQL frontend; \
1935 PowQL spells it with the ?? operator: .a ?? .b"
1936 .into(),
1937 });
1938 }
1939 if self.is_kw("distinct") {
1940 let hint = if func == "count" {
1943 "; PowQL spells it count(distinct T { .col })"
1944 } else {
1945 ""
1946 };
1947 return Err(ParseError::Unsupported {
1948 feature: format!(
1949 "SQL {}(DISTINCT ...) is not supported yet in the SQL \
1950 frontend{hint}",
1951 func.to_ascii_uppercase()
1952 ),
1953 });
1954 }
1955 let mut args = Vec::new();
1956 while !self.eat_sym(')') {
1957 args.push(self.expr_bp(0, &[])?);
1958 if func == "cast" && self.is_kw("as") {
1962 return Err(ParseError::Unsupported {
1963 feature: "SQL CAST(x AS TYPE) is not supported yet in the SQL \
1964 frontend; PowDB spells a cast cast(x, 'int') with the \
1965 target type as a string argument"
1966 .into(),
1967 });
1968 }
1969 let _ = self.eat_sym(',');
1970 }
1971 return Ok(format!("{}({})", func, args.join(", ")));
1972 }
1973 if self.eat_sym('.') {
1974 let f = self.expect_ident("qualified column name")?;
1975 match &self.qual_ctx {
1976 QualCtx::Joined => Ok(format!("{w}.{f}")),
1977 QualCtx::Single { visible_name } => {
1978 if w.eq_ignore_ascii_case(visible_name) {
1979 Ok(format!(".{f}"))
1980 } else {
1981 Err(ParseError::Syntax {
1982 message: format!(
1983 "no such column: {w}.{f} (the only table in this \
1984 statement is `{visible_name}`)"
1985 ),
1986 position: None,
1987 })
1988 }
1989 }
1990 QualCtx::None => Err(ParseError::Syntax {
1991 message: format!(
1992 "qualified column reference `{w}.{f}` is not allowed here"
1993 ),
1994 position: None,
1995 }),
1996 }
1997 } else {
1998 Ok(format!(".{w}"))
1999 }
2000 }
2001 Some(SqlTok::Number(n)) => Ok(n),
2002 Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
2003 Some(SqlTok::Param(p)) => Ok(format!("${p}")),
2004 Some(SqlTok::Symbol('*')) => Ok("*".into()),
2005 Some(t) => Err(ParseError::Syntax {
2006 message: format!("unexpected SQL token in expression: {}", t.display()),
2007 position: None,
2008 }),
2009 None => Err(ParseError::UnexpectedToken {
2010 expected: "expression".into(),
2011 got: "<eof>".into(),
2012 position: None,
2013 }),
2014 }
2015 }
2016
2017 fn next_is_stop(&self, stop: &[&str]) -> bool {
2018 matches!(self.peek(), Some(SqlTok::Word(w)) if stop.iter().any(|kw| w.eq_ignore_ascii_case(kw)))
2019 }
2020}
2021
2022fn infix_bp(op: &str) -> Option<(u8, u8)> {
2023 Some(match op.to_ascii_lowercase().as_str() {
2024 "or" => (1, 2),
2025 "and" => (3, 4),
2026 "=" | "!=" | "<" | ">" | "<=" | ">=" => (5, 6),
2027 "+" | "-" => (7, 8),
2028 "*" | "/" => (9, 10),
2029 _ => return None,
2030 })
2031}
2032
2033fn quote_powql_string(s: &str) -> String {
2034 format!(
2035 "\"{}\"",
2036 s.replace('\\', "\\\\")
2037 .replace('"', "\\\"")
2038 .replace('\n', "\\n")
2039 .replace('\t', "\\t")
2040 )
2041}
2042
2043fn is_clause_kw(w: &str) -> bool {
2044 matches!(
2045 w.to_ascii_lowercase().as_str(),
2046 "where"
2047 | "group"
2048 | "having"
2049 | "order"
2050 | "limit"
2051 | "offset"
2052 | "join"
2053 | "inner"
2054 | "left"
2055 | "right"
2056 | "cross"
2057 | "on"
2058 | "values"
2059 | "set"
2060 )
2061}
2062fn is_join_modifier(w: &str) -> bool {
2063 matches!(
2064 w.to_ascii_lowercase().as_str(),
2065 "join" | "inner" | "left" | "right" | "cross" | "outer"
2066 )
2067}
2068fn is_reserved_identifier(w: &str) -> bool {
2069 matches!(
2070 w.to_ascii_lowercase().as_str(),
2071 "select"
2072 | "from"
2073 | "where"
2074 | "insert"
2075 | "into"
2076 | "values"
2077 | "update"
2078 | "set"
2079 | "delete"
2080 | "create"
2081 | "table"
2082 | "drop"
2083 | "alter"
2084 )
2085}
2086
2087#[cfg(test)]
2088mod tests {
2089 use super::*;
2090 use crate::ast::{AlterAction, IndexTarget};
2091
2092 #[test]
2093 fn a_comment_only_sql_segment_is_effectively_blank() {
2094 for blank in [
2098 "",
2099 " ",
2100 "\n\t \n",
2101 "-- comment only",
2102 "--comment only",
2103 "-- a\n-- b\n",
2104 "# comment only",
2105 " # comment only ",
2106 "# a\n# b",
2107 "/* block */",
2108 "/* multi\nline */\n-- and a line comment\n",
2109 "-- end of dump\n",
2110 ] {
2111 assert!(
2112 sql_is_effectively_blank(blank),
2113 "{blank:?} must be effectively blank"
2114 );
2115 }
2116
2117 for real in [
2118 "SELECT 1",
2119 "SELECT 1 -- trailing",
2121 "SELECT 1\n-- trailing",
2122 "-- leading\nSELECT 1",
2123 "# leading\nSELECT 1",
2124 "/* leading */ SELECT 1",
2125 "SELECT '-- not a comment'",
2129 "SELECT '# not a comment'",
2130 "SELECT '/* not a comment */'",
2131 "SELECT @",
2133 "SELECT 'unterminated",
2134 ] {
2135 assert!(
2136 !sql_is_effectively_blank(real),
2137 "{real:?} must not be treated as blank"
2138 );
2139 }
2140 }
2141
2142 #[test]
2143 fn sql_frontend_rejects_create_view() {
2144 let result = parse_sql("CREATE VIEW v AS SELECT id FROM Post");
2149 assert!(
2150 result.is_err(),
2151 "CREATE VIEW must be rejected by the SQL frontend, got {result:?}"
2152 );
2153 }
2154
2155 #[test]
2156 fn json_path_update_target_is_targeted_unsupported() {
2157 for stmt in [
2161 "UPDATE Doc SET data->'x' = 5",
2162 "UPDATE Doc SET data->>'x' = 5",
2163 ] {
2164 let err = parse_sql(stmt).unwrap_err();
2165 assert!(
2166 matches!(err, ParseError::Unsupported { .. }),
2167 "{stmt}: expected Unsupported, got {err:?}"
2168 );
2169 let msg = err.to_string();
2170 assert!(
2171 msg.contains("JSON path assignment targets are not supported"),
2172 "{stmt}: message must state the unsupported feature: {msg}"
2173 );
2174 assert!(
2175 msg.contains("json_set"),
2176 "{stmt}: message must point at the whole-column alternative: {msg}"
2177 );
2178 }
2179 assert!(parse_sql("UPDATE Doc SET data = '{}'").is_ok());
2181 }
2182
2183 #[test]
2184 fn json_arrows_lex_longest_token_and_lower_to_powql_paths() {
2185 assert_eq!(
2186 lex_sql("data->>'name'").unwrap(),
2187 vec![
2188 SqlTok::Word("data".into()),
2189 SqlTok::Op("->>".into()),
2190 SqlTok::String("name".into()),
2191 ]
2192 );
2193 let parsed = parse_sql_with_canonical(
2194 "SELECT data -> 'author' ->> 'name' AS name, data -> 'tags' -> 0 AS first FROM Post WHERE data ->> 'state' = 'ready'",
2195 )
2196 .unwrap();
2197 assert_eq!(
2198 parsed.canonical_powql,
2199 "Post filter json_text(.data->\"state\") = \"ready\" { name: json_text(.data->\"author\"->\"name\"), first: .data->\"tags\"->0 }"
2200 );
2201 let raw = parse_sql_with_canonical("SELECT data -> 'name' FROM Post").unwrap();
2202 let text = parse_sql_with_canonical("SELECT data ->> 'name' FROM Post").unwrap();
2203 assert_ne!(
2204 crate::canonicalize::canonicalize(&raw.canonical_powql)
2205 .unwrap()
2206 .0,
2207 crate::canonicalize::canonicalize(&text.canonical_powql)
2208 .unwrap()
2209 .0,
2210 "-> and ->> must never share a cached plan"
2211 );
2212
2213 let ordered = parse_sql_with_canonical(
2214 "SELECT id FROM Post ORDER BY data ->> 'rank' DESC, data -> 'tie' ASC",
2215 )
2216 .unwrap();
2217 assert_eq!(
2218 ordered.canonical_powql,
2219 "Post order json_text(.data->\"rank\") desc, .data->\"tie\" asc { .id }"
2220 );
2221
2222 let grouped = parse_sql_with_canonical(
2223 "SELECT data ->> 'kind' AS kind, COUNT(*) AS n FROM Post GROUP BY data ->> 'kind'",
2224 )
2225 .unwrap();
2226 assert_eq!(
2227 grouped.canonical_powql,
2228 "Post group json_text(.data->\"kind\") { kind: json_text(.data->\"kind\"), n: count(*) }"
2229 );
2230 }
2231
2232 #[test]
2233 fn json_arrows_reject_invalid_path_segments() {
2234 for sql in [
2235 "SELECT data -> other FROM Post",
2236 "SELECT data -> -1 FROM Post",
2237 "SELECT data -> 1.5 FROM Post",
2238 ] {
2239 let err = parse_sql_with_canonical(sql).unwrap_err();
2240 assert!(
2241 err.to_string()
2242 .contains("string key or non-negative integer index"),
2243 "unexpected error for `{sql}`: {err}"
2244 );
2245 }
2246 }
2247
2248 #[test]
2249 fn select_lowers_to_powql_ast() {
2250 let sql = parse_sql_with_canonical(
2251 "SELECT name, age FROM User WHERE age > 25 ORDER BY age DESC LIMIT 10",
2252 )
2253 .unwrap();
2254 assert_eq!(
2255 sql.canonical_powql,
2256 "User filter .age > 25 order .age desc limit 10 { .name, .age }"
2257 );
2258 assert_eq!(
2259 sql.statement,
2260 parser::parse("User filter .age > 25 order .age desc limit 10 { .name, .age }")
2261 .unwrap()
2262 );
2263 }
2264
2265 #[test]
2266 fn insert_update_delete_and_ddl_lower_to_existing_ast() {
2267 assert!(matches!(
2268 parse_sql("CREATE TABLE User (id INTEGER NOT NULL UNIQUE, name TEXT)").unwrap(),
2269 Statement::CreateType(_)
2270 ));
2271 assert!(matches!(
2272 parse_sql("INSERT INTO User (id, name) VALUES (1, 'Ada')").unwrap(),
2273 Statement::Insert(_)
2274 ));
2275 assert!(matches!(
2276 parse_sql("UPDATE User SET name = 'Grace' WHERE id = 1").unwrap(),
2277 Statement::UpdateQuery(_)
2278 ));
2279 assert!(matches!(
2280 parse_sql("DELETE FROM User WHERE id = 1").unwrap(),
2281 Statement::DeleteQuery(_)
2282 ));
2283 }
2284
2285 #[test]
2286 fn unsupported_sql_gets_explicit_error() {
2287 let err = parse_sql("SELECT name FROM User WHERE id IN (SELECT user_id FROM Orders)")
2288 .unwrap_err();
2289 assert!(err.to_string().contains("SQL IN"));
2290 }
2291
2292 #[test]
2293 fn sql_expression_index_has_targeted_powql_guidance() {
2294 assert!(matches!(
2295 parse_sql("CREATE INDEX post_slug ON Post (slug)").unwrap(),
2296 Statement::AlterTable(_)
2297 ));
2298 for sql in [
2299 "CREATE INDEX post_age ON Post ((data -> 'age'))",
2300 "CREATE INDEX post_first ON Post (data -> 'scores' -> 0)",
2301 "CREATE UNIQUE INDEX post_code ON Post ((data -> 'code'))",
2302 ] {
2303 let Statement::AlterTable(alter) = parse_sql(sql).unwrap() else {
2304 panic!("expected expression-index ALTER lowering for `{sql}`");
2305 };
2306 let target = match alter.action {
2307 AlterAction::AddIndex { target, .. } | AlterAction::AddUnique { target, .. } => {
2308 target
2309 }
2310 action => panic!("expected add-index action, got {action:?}"),
2311 };
2312 assert!(matches!(target, IndexTarget::JsonPath(_)), "{sql}");
2313 }
2314 let error = parse_sql("CREATE INDEX post_age ON Post (data->>'age')")
2315 .expect_err("SQL text extraction is not an indexable path")
2316 .to_string();
2317 assert!(error.contains("->>"));
2318 let error = parse_sql("CREATE INDEX post_age ON Post ((data + 1))")
2319 .expect_err("arbitrary SQL expressions remain unsupported")
2320 .to_string();
2321 assert!(error.contains("only direct JSON -> paths"));
2322 }
2323
2324 #[test]
2325 fn sql_aggregates_lower_with_raw_mode() {
2326 let Statement::Query(query) =
2327 parse_sql("SELECT dept, SUM(balance) AS total FROM Account GROUP BY dept").unwrap()
2328 else {
2329 panic!("expected query");
2330 };
2331 let projection = query.projection.expect("projection");
2332 assert!(matches!(
2333 projection[1].expr,
2334 Expr::FunctionCall(_, _, AggregateMode::Raw)
2335 ));
2336 }
2337
2338 #[test]
2339 fn ungrouped_join_aggregate_lowers_to_raw_powql_aggregate() {
2340 let lowered = parse_sql_with_canonical(
2341 "SELECT AVG(a.balance) FROM Account a JOIN Entry e ON a.id = e.account_id",
2342 )
2343 .unwrap();
2344 assert_eq!(
2345 lowered.canonical_powql,
2346 "avg(Account as a inner join Entry as e on a.id = e.account_id { a.balance })"
2347 );
2348 let Statement::Query(query) = lowered.statement else {
2349 panic!("expected query");
2350 };
2351 assert_eq!(
2352 query.aggregation.expect("aggregate").mode,
2353 AggregateMode::Raw
2354 );
2355 }
2356}
2357
2358#[cfg(test)]
2359mod split_tests {
2360 use super::split_statements_sql;
2361
2362 #[test]
2371 fn a_semicolon_inside_a_comment_is_not_a_boundary() {
2372 let segs = split_statements_sql("-- cleanup; DELETE FROM t");
2373 assert_eq!(segs, vec!["-- cleanup; DELETE FROM t"]);
2374 assert!(super::sql_is_effectively_blank(segs[0]));
2375
2376 assert_eq!(
2377 split_statements_sql("SELECT 1 FROM t;\n-- trailing; DELETE FROM t\n"),
2378 vec!["SELECT 1 FROM t", "-- trailing; DELETE FROM t"]
2379 );
2380 assert_eq!(
2381 split_statements_sql("/* drop it; DELETE FROM t */ SELECT 1 FROM t"),
2382 vec!["/* drop it; DELETE FROM t */ SELECT 1 FROM t"]
2383 );
2384 }
2385
2386 #[test]
2389 fn a_semicolon_inside_a_string_is_not_a_boundary() {
2390 assert_eq!(
2391 split_statements_sql("INSERT INTO t VALUES ('a;b')"),
2392 vec!["INSERT INTO t VALUES ('a;b')"]
2393 );
2394 assert_eq!(
2395 split_statements_sql(r#"INSERT INTO t VALUES ("a;b")"#),
2396 vec![r#"INSERT INTO t VALUES ("a;b")"#]
2397 );
2398 assert_eq!(
2399 split_statements_sql("INSERT INTO t VALUES ('it''s; here')"),
2400 vec!["INSERT INTO t VALUES ('it''s; here')"]
2401 );
2402 assert_eq!(
2403 split_statements_sql(r#"INSERT INTO t VALUES ('a\';b')"#),
2404 vec![r#"INSERT INTO t VALUES ('a\';b')"#]
2405 );
2406 }
2407
2408 #[test]
2410 fn real_boundaries_still_split() {
2411 assert_eq!(
2412 split_statements_sql("SELECT 1 FROM t; SELECT 2 FROM t;"),
2413 vec!["SELECT 1 FROM t", "SELECT 2 FROM t"]
2414 );
2415 assert_eq!(split_statements_sql(";; ;"), Vec::<&str>::new());
2416 assert_eq!(split_statements_sql(""), Vec::<&str>::new());
2417 }
2418}