1use crate::ast::{AggregateMode, Expr, QueryExpr, Statement};
9use crate::parser::{self, ParseError};
10
11#[derive(Debug, Clone)]
12pub struct ParsedSql {
13 pub statement: Statement,
14 pub canonical_powql: String,
15}
16
17pub fn parse_sql(input: &str) -> Result<Statement, ParseError> {
18 parse_sql_with_canonical(input).map(|p| p.statement)
19}
20
21pub fn parse_sql_with_canonical(input: &str) -> Result<ParsedSql, ParseError> {
22 let toks = lex_sql(input)?;
23 let mut p = SqlParser {
24 toks,
25 pos: 0,
26 depth: 0,
27 qual_ctx: QualCtx::None,
28 };
29 let canonical_powql = p.statement()?;
30 if !p.at_end() {
31 return Err(ParseError::Syntax {
32 message: format!(
33 "unexpected trailing SQL token: {}",
34 p.peek()
35 .map(|t| t.display())
36 .unwrap_or_else(|| "<eof>".into())
37 ),
38 });
39 }
40 let mut statement = parser::parse(&canonical_powql)?;
41 mark_sql_statement_raw(&mut statement);
42 Ok(ParsedSql {
43 statement,
44 canonical_powql,
45 })
46}
47
48fn mark_sql_statement_raw(statement: &mut Statement) {
49 match statement {
50 Statement::Query(query) => mark_sql_query_raw(query),
51 Statement::Union(union) => {
52 mark_sql_statement_raw(&mut union.left);
53 mark_sql_statement_raw(&mut union.right);
54 }
55 Statement::Explain(inner) => mark_sql_statement_raw(inner),
56 Statement::CreateView(view) => mark_sql_query_raw(&mut view.query),
64 _ => {}
65 }
66}
67
68fn mark_sql_query_raw(query: &mut QueryExpr) {
69 if let Some(aggregate) = &mut query.aggregation {
70 aggregate.mode = AggregateMode::Raw;
71 if let Some(argument) = &mut aggregate.argument {
72 mark_sql_expr_raw(argument);
73 }
74 }
75 for join in &mut query.joins {
76 if let Some(on) = &mut join.on {
77 mark_sql_expr_raw(on);
78 }
79 }
80 if let Some(filter) = &mut query.filter {
81 mark_sql_expr_raw(filter);
82 }
83 if let Some(order) = &mut query.order {
84 for key in &mut order.keys {
85 mark_sql_expr_raw(&mut key.expr);
86 }
87 }
88 if let Some(projection) = &mut query.projection {
89 for field in projection {
90 mark_sql_expr_raw(&mut field.expr);
91 }
92 }
93 if let Some(group) = &mut query.group_by {
94 for key in &mut group.keys {
95 mark_sql_expr_raw(&mut key.expr);
96 }
97 if let Some(having) = &mut group.having {
98 mark_sql_expr_raw(having);
99 }
100 }
101}
102
103fn mark_sql_expr_raw(expr: &mut Expr) {
104 match expr {
105 Expr::FunctionCall(_, argument, mode) => {
106 *mode = AggregateMode::Raw;
107 mark_sql_expr_raw(argument);
108 }
109 Expr::Window {
110 args,
111 mode,
112 partition_by,
113 order_by,
114 ..
115 } => {
116 *mode = AggregateMode::Raw;
117 for expr in args.iter_mut().chain(partition_by.iter_mut()) {
118 mark_sql_expr_raw(expr);
119 }
120 for key in order_by {
121 mark_sql_expr_raw(&mut key.expr);
122 }
123 }
124 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
125 mark_sql_expr_raw(left);
126 mark_sql_expr_raw(right);
127 }
128 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
129 mark_sql_expr_raw(inner);
130 }
131 Expr::ScalarFunc(_, args) => {
132 for expr in args {
133 mark_sql_expr_raw(expr);
134 }
135 }
136 Expr::InList { expr, list, .. } => {
137 mark_sql_expr_raw(expr);
138 for item in list {
139 mark_sql_expr_raw(item);
140 }
141 }
142 Expr::InSubquery { expr, subquery, .. } => {
143 mark_sql_expr_raw(expr);
144 mark_sql_query_raw(subquery);
145 }
146 Expr::ExistsSubquery { subquery, .. } => mark_sql_query_raw(subquery),
147 Expr::Case { whens, else_expr } => {
148 for (condition, result) in whens {
149 mark_sql_expr_raw(condition);
150 mark_sql_expr_raw(result);
151 }
152 if let Some(expr) = else_expr {
153 mark_sql_expr_raw(expr);
154 }
155 }
156 _ => {}
157 }
158}
159
160pub(crate) fn statement_has_aggregate(statement: &Statement) -> bool {
161 match statement {
162 Statement::Query(query) => query_has_aggregate(query),
163 Statement::Union(union) => {
164 statement_has_aggregate(&union.left) || statement_has_aggregate(&union.right)
165 }
166 Statement::Explain(inner) => statement_has_aggregate(inner),
167 Statement::CreateView(view) => query_has_aggregate(&view.query),
168 _ => false,
169 }
170}
171
172fn query_has_aggregate(query: &QueryExpr) -> bool {
173 query.aggregation.is_some()
174 || query
175 .joins
176 .iter()
177 .filter_map(|join| join.on.as_ref())
178 .any(expr_has_aggregate)
179 || query.filter.as_ref().is_some_and(expr_has_aggregate)
180 || query
181 .order
182 .as_ref()
183 .is_some_and(|order| order.keys.iter().any(|key| expr_has_aggregate(&key.expr)))
184 || query.projection.as_ref().is_some_and(|projection| {
185 projection
186 .iter()
187 .any(|field| expr_has_aggregate(&field.expr))
188 })
189 || query.group_by.as_ref().is_some_and(|group| {
190 group.keys.iter().any(|key| expr_has_aggregate(&key.expr))
191 || group.having.as_ref().is_some_and(expr_has_aggregate)
192 })
193}
194
195fn expr_has_aggregate(expr: &Expr) -> bool {
196 match expr {
197 Expr::FunctionCall(..) | Expr::Window { .. } => true,
198 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
199 expr_has_aggregate(left) || expr_has_aggregate(right)
200 }
201 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
202 expr_has_aggregate(inner)
203 }
204 Expr::ScalarFunc(_, args) => args.iter().any(expr_has_aggregate),
205 Expr::InList { expr, list, .. } => {
206 expr_has_aggregate(expr) || list.iter().any(expr_has_aggregate)
207 }
208 Expr::InSubquery { expr, subquery, .. } => {
209 expr_has_aggregate(expr) || query_has_aggregate(subquery)
210 }
211 Expr::ExistsSubquery { subquery, .. } => query_has_aggregate(subquery),
212 Expr::Case { whens, else_expr } => {
213 whens.iter().any(|(condition, result)| {
214 expr_has_aggregate(condition) || expr_has_aggregate(result)
215 }) || else_expr.as_deref().is_some_and(expr_has_aggregate)
216 }
217 _ => false,
218 }
219}
220
221#[derive(Debug, Clone, PartialEq)]
222enum SqlTok {
223 Word(String),
224 Number(String),
225 String(String),
226 Symbol(char),
227 Op(String),
228 Param(String),
229}
230
231impl SqlTok {
232 fn display(&self) -> String {
233 match self {
234 SqlTok::Word(s) => s.clone(),
235 SqlTok::Number(s) => s.clone(),
236 SqlTok::String(s) => format!("'{s}'"),
237 SqlTok::Symbol(c) => c.to_string(),
238 SqlTok::Op(s) => s.clone(),
239 SqlTok::Param(s) => format!("${s}"),
240 }
241 }
242}
243
244fn lex_sql(input: &str) -> Result<Vec<SqlTok>, ParseError> {
245 let mut out = Vec::new();
246 let chars: Vec<char> = input.chars().collect();
247 let mut i = 0usize;
248 while i < chars.len() {
249 let c = chars[i];
250 if c.is_whitespace() {
251 i += 1;
252 continue;
253 }
254 if c == '-' && chars.get(i + 1) == Some(&'-') {
255 i += 2;
256 while i < chars.len() && chars[i] != '\n' {
257 i += 1;
258 }
259 continue;
260 }
261 if c == '/' && chars.get(i + 1) == Some(&'*') {
262 i += 2;
263 while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {
264 i += 1;
265 }
266 if i + 1 >= chars.len() {
267 return Err(ParseError::Lex {
268 message: "unterminated block comment".into(),
269 position: i,
270 });
271 }
272 i += 2;
273 continue;
274 }
275 if c == '\'' || c == '"' {
276 let quote = c;
277 i += 1;
278 let mut s = String::new();
279 while i < chars.len() {
280 if chars[i] == quote {
281 if quote == '\'' && chars.get(i + 1) == Some(&'\'') {
282 s.push('\'');
283 i += 2;
284 continue;
285 }
286 i += 1;
287 break;
288 }
289 if chars[i] == '\\' && i + 1 < chars.len() {
290 let next = chars[i + 1];
291 match next {
292 'n' => s.push('\n'),
293 't' => s.push('\t'),
294 other => s.push(other),
295 }
296 i += 2;
297 } else {
298 s.push(chars[i]);
299 i += 1;
300 }
301 }
302 if i > chars.len() || chars.get(i.saturating_sub(1)) != Some("e) {
303 return Err(ParseError::Lex {
304 message: "unterminated string".into(),
305 position: i,
306 });
307 }
308 out.push(SqlTok::String(s));
309 continue;
310 }
311 if c == '$' {
312 i += 1;
313 let start = i;
314 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
315 i += 1;
316 }
317 out.push(SqlTok::Param(chars[start..i].iter().collect()));
318 continue;
319 }
320 if c == '-' && chars.get(i + 1) == Some(&'>') {
322 if chars.get(i + 2) == Some(&'>') {
323 out.push(SqlTok::Op("->>".into()));
324 i += 3;
325 } else {
326 out.push(SqlTok::Op("->".into()));
327 i += 2;
328 }
329 continue;
330 }
331 if c.is_ascii_digit() || (c == '-' && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit()))
332 {
333 let start = i;
334 i += 1;
335 while i < chars.len() && chars[i].is_ascii_digit() {
336 i += 1;
337 }
338 if i < chars.len()
339 && chars[i] == '.'
340 && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit())
341 {
342 i += 1;
343 while i < chars.len() && chars[i].is_ascii_digit() {
344 i += 1;
345 }
346 }
347 out.push(SqlTok::Number(chars[start..i].iter().collect()));
348 continue;
349 }
350 if c.is_alphabetic() || c == '_' {
351 let start = i;
352 i += 1;
353 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
354 i += 1;
355 }
356 out.push(SqlTok::Word(chars[start..i].iter().collect()));
357 continue;
358 }
359 if matches!(c, '(' | ')' | ',' | '*' | '.') {
360 out.push(SqlTok::Symbol(c));
361 i += 1;
362 continue;
363 }
364 if matches!(c, '=' | '<' | '>' | '!') {
365 let mut op = String::new();
366 op.push(c);
367 if matches!(chars.get(i + 1), Some('=') | Some('>')) {
368 op.push(chars[i + 1]);
369 i += 2;
370 } else {
371 i += 1;
372 }
373 if op == "<>" {
374 op = "!=".into();
375 }
376 out.push(SqlTok::Op(op));
377 continue;
378 }
379 if matches!(c, '+' | '-' | '/') {
380 out.push(SqlTok::Op(c.to_string()));
381 i += 1;
382 continue;
383 }
384 return Err(ParseError::Lex {
385 message: format!("unexpected SQL character `{c}`"),
386 position: i,
387 });
388 }
389 Ok(out)
390}
391
392const MAX_SQL_NESTING_DEPTH: usize = 64;
406
407struct SqlParser {
408 toks: Vec<SqlTok>,
409 pos: usize,
410 depth: usize,
411 qual_ctx: QualCtx,
415}
416
417#[derive(Clone, PartialEq)]
424enum QualCtx {
425 None,
427 Single { visible_name: String },
431 Joined,
433}
434
435struct Projection {
437 text: String,
440 agg: Option<AggCall>,
444}
445
446struct AggCall {
448 func: String,
450 arg: AggArg,
451}
452
453enum AggArg {
454 Star,
456 Field(String),
458}
459
460impl AggCall {
461 fn canonical(&self) -> String {
463 match &self.arg {
464 AggArg::Star => format!("{}(*)", self.func),
465 AggArg::Field(f) => format!("{}({f})", self.func),
466 }
467 }
468}
469
470fn build_ungrouped_aggregate(agg: &AggCall, inner: &str) -> Result<String, ParseError> {
476 match agg.func.as_str() {
477 "count" if matches!(agg.arg, AggArg::Star) => Ok(format!("count({inner})")),
478 "count" | "sum" | "avg" | "min" | "max" => match &agg.arg {
479 AggArg::Field(f) => Ok(format!("{}({inner} {{ {f} }})", agg.func)),
480 AggArg::Star => Err(ParseError::Unsupported {
481 feature: format!("{0}(*) is not valid; {0}() needs a column", agg.func),
482 }),
483 },
484 other => Err(ParseError::Syntax {
486 message: format!("unknown aggregate function `{other}`"),
487 }),
488 }
489}
490
491impl SqlParser {
492 fn at_end(&self) -> bool {
493 self.pos >= self.toks.len()
494 }
495 fn peek(&self) -> Option<&SqlTok> {
496 self.toks.get(self.pos)
497 }
498 fn bump(&mut self) -> Option<SqlTok> {
499 let t = self.toks.get(self.pos).cloned();
500 if t.is_some() {
501 self.pos += 1;
502 }
503 t
504 }
505 fn is_kw(&self, kw: &str) -> bool {
506 matches!(self.peek(), Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case(kw))
507 }
508 fn eat_kw(&mut self, kw: &str) -> bool {
509 if self.is_kw(kw) {
510 self.pos += 1;
511 true
512 } else {
513 false
514 }
515 }
516 fn expect_kw(&mut self, kw: &str) -> Result<(), ParseError> {
517 if self.eat_kw(kw) {
518 Ok(())
519 } else {
520 Err(ParseError::UnexpectedToken {
521 expected: kw.into(),
522 got: self
523 .peek()
524 .map(|t| t.display())
525 .unwrap_or_else(|| "<eof>".into()),
526 })
527 }
528 }
529 fn eat_sym(&mut self, c: char) -> bool {
530 if matches!(self.peek(), Some(SqlTok::Symbol(got)) if *got == c) {
531 self.pos += 1;
532 true
533 } else {
534 false
535 }
536 }
537 fn expect_sym(&mut self, c: char) -> Result<(), ParseError> {
538 if self.eat_sym(c) {
539 Ok(())
540 } else {
541 Err(ParseError::UnexpectedToken {
542 expected: c.to_string(),
543 got: self
544 .peek()
545 .map(|t| t.display())
546 .unwrap_or_else(|| "<eof>".into()),
547 })
548 }
549 }
550 fn expect_ident(&mut self, what: &str) -> Result<String, ParseError> {
551 match self.bump() {
552 Some(SqlTok::Word(w)) if !is_reserved_identifier(&w) => Ok(w),
553 Some(SqlTok::Word(w)) => Err(ParseError::Syntax {
554 message: format!("expected {what}, got reserved word `{w}`"),
555 }),
556 Some(t) => Err(ParseError::UnexpectedToken {
557 expected: what.into(),
558 got: t.display(),
559 }),
560 None => Err(ParseError::UnexpectedToken {
561 expected: what.into(),
562 got: "<eof>".into(),
563 }),
564 }
565 }
566
567 fn statement(&mut self) -> Result<String, ParseError> {
568 if self.is_kw("select") {
569 self.select()
570 } else if self.is_kw("insert") {
571 self.insert()
572 } else if self.is_kw("update") {
573 self.update()
574 } else if self.is_kw("delete") {
575 self.delete()
576 } else if self.is_kw("create") {
577 self.create()
578 } else if self.is_kw("drop") {
579 self.drop_stmt()
580 } else if self.is_kw("alter") {
581 self.alter()
582 } else if self.eat_kw("begin") {
583 let _ = self.eat_kw("transaction");
584 Ok("begin".into())
585 } else if self.eat_kw("commit") {
586 Ok("commit".into())
587 } else if self.eat_kw("rollback") {
588 Ok("rollback".into())
589 } else {
590 Err(ParseError::UnexpectedToken {
591 expected: "SQL statement".into(),
592 got: self
593 .peek()
594 .map(|t| t.display())
595 .unwrap_or_else(|| "<eof>".into()),
596 })
597 }
598 }
599
600 fn scan_select_qual_ctx(&self) -> QualCtx {
606 let mut i = self.pos;
607 let mut depth = 0usize;
608 loop {
609 match self.toks.get(i) {
610 None => return QualCtx::None,
611 Some(SqlTok::Symbol('(')) => depth += 1,
612 Some(SqlTok::Symbol(')')) => depth = depth.saturating_sub(1),
613 Some(SqlTok::Word(w)) if depth == 0 && w.eq_ignore_ascii_case("from") => break,
614 Some(_) => {}
615 }
616 i += 1;
617 }
618 let Some(SqlTok::Word(table)) = self.toks.get(i + 1) else {
619 return QualCtx::None;
621 };
622 let mut visible = table.clone();
623 let mut j = i + 2;
624 match self.toks.get(j) {
627 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("as") => {
628 if let Some(SqlTok::Word(a)) = self.toks.get(j + 1) {
629 visible = a.clone();
630 j += 2;
631 }
632 }
633 Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w) => {
634 visible = w.clone();
635 j += 1;
636 }
637 _ => {}
638 }
639 match self.toks.get(j) {
640 Some(SqlTok::Word(w)) if is_join_modifier(w) => QualCtx::Joined,
641 _ => QualCtx::Single {
642 visible_name: visible,
643 },
644 }
645 }
646
647 fn select(&mut self) -> Result<String, ParseError> {
648 self.expect_kw("select")?;
649 self.qual_ctx = self.scan_select_qual_ctx();
650 let distinct = self.eat_kw("distinct");
651 let projection = self.projection_list()?;
652 self.expect_kw("from")?;
653 let source = self.table_ref()?;
654 let mut joins = Vec::new();
655 while self.starts_join() {
656 joins.push(self.join_clause()?);
657 }
658 let filter = if self.eat_kw("where") {
659 Some(self.expr_until(&["group", "having", "order", "limit", "offset"])?)
660 } else {
661 None
662 };
663 let group = if self.eat_kw("group") {
664 self.expect_kw("by")?;
665 Some(self.expression_list_until(&["having", "order", "limit", "offset"])?)
666 } else {
667 None
668 };
669 let having = if self.eat_kw("having") {
670 Some(self.expr_until(&["order", "limit", "offset"])?)
671 } else {
672 None
673 };
674 let order = if self.eat_kw("order") {
675 self.expect_kw("by")?;
676 Some(self.order_list_until(&["limit", "offset"])?)
677 } else {
678 None
679 };
680 let limit = if self.eat_kw("limit") {
681 Some(self.expr_until(&["offset"])?)
682 } else {
683 None
684 };
685 let offset = if self.eat_kw("offset") {
686 Some(self.expr_until(&[])?)
687 } else {
688 None
689 };
690
691 let has_group = group.is_some();
692
693 let mut out = source;
694 for j in joins {
695 out.push(' ');
696 out.push_str(&j);
697 }
698 if distinct {
699 out.push_str(" distinct");
700 }
701 if let Some(f) = filter {
702 out.push_str(" filter ");
703 out.push_str(&f);
704 }
705 if let Some(keys) = group {
706 out.push_str(" group ");
707 out.push_str(&keys.join(", "));
708 if let Some(h) = having {
709 out.push_str(" having ");
710 out.push_str(&h);
711 }
712 } else if having.is_some() {
713 return Err(ParseError::Syntax {
714 message: "HAVING requires GROUP BY".into(),
715 });
716 }
717 if let Some(o) = order {
718 out.push_str(" order ");
719 out.push_str(&o);
720 }
721 if let Some(l) = limit {
722 out.push_str(" limit ");
723 out.push_str(&l);
724 }
725 if let Some(o) = offset {
726 out.push_str(" offset ");
727 out.push_str(&o);
728 }
729 if let Some(items) = projection {
730 if !has_group && items.iter().any(|p| p.agg.is_some()) {
735 if distinct {
736 return Err(ParseError::Unsupported {
737 feature: "aggregates with DISTINCT and no GROUP BY are not supported by the SQL frontend".into(),
738 });
739 }
740 if items.len() != 1 {
741 return Err(ParseError::Unsupported {
742 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(),
743 });
744 }
745 let agg = items.into_iter().next().unwrap().agg.unwrap();
747 return build_ungrouped_aggregate(&agg, &out);
748 }
749 out.push_str(" { ");
750 out.push_str(
751 &items
752 .iter()
753 .map(|p| p.text.as_str())
754 .collect::<Vec<_>>()
755 .join(", "),
756 );
757 out.push_str(" }");
758 }
759 Ok(out)
760 }
761
762 fn projection_list(&mut self) -> Result<Option<Vec<Projection>>, ParseError> {
763 if self.eat_sym('*') {
764 return Ok(None);
765 }
766 let mut fields = Vec::new();
767 loop {
768 let (expr, agg) = match self.try_aggregate()? {
773 Some(a) => (a.canonical(), Some(a)),
774 None => (self.expr_until(&["from", "as"])?, None),
775 };
776 let text = if self.eat_kw("as") {
777 let alias = self.expect_ident("projection alias")?;
778 format!("{alias}: {expr}")
779 } else {
780 expr
781 };
782 fields.push(Projection { text, agg });
783 if !self.eat_sym(',') {
784 break;
785 }
786 }
787 Ok(Some(fields))
788 }
789
790 fn try_aggregate(&mut self) -> Result<Option<AggCall>, ParseError> {
795 let Some(SqlTok::Word(w)) = self.peek().cloned() else {
796 return Ok(None);
797 };
798 let func = w.to_ascii_lowercase();
799 if !matches!(func.as_str(), "count" | "sum" | "avg" | "min" | "max") {
800 return Ok(None);
801 }
802 let save = self.pos;
803 self.pos += 1; if !self.eat_sym('(') {
805 self.pos = save;
806 return Ok(None);
807 }
808 let arg = if func == "count" && self.eat_sym('*') {
809 AggArg::Star
810 } else if func == "count" && self.is_kw("distinct") {
811 self.pos = save;
814 return Ok(None);
815 } else {
816 AggArg::Field(self.expr_bp(0, &[])?)
817 };
818 if self.eat_sym(')')
821 && (matches!(self.peek(), Some(SqlTok::Symbol(',')))
822 || self.is_kw("as")
823 || self.is_kw("from"))
824 {
825 Ok(Some(AggCall { func, arg }))
826 } else {
827 self.pos = save;
828 Ok(None)
829 }
830 }
831
832 fn table_ref(&mut self) -> Result<String, ParseError> {
833 let table = self.expect_ident("table name")?;
834 let has_alias = self.eat_kw("as")
835 || matches!(self.peek(), Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w));
836 if has_alias {
837 let alias = self.expect_ident("table alias")?;
838 Ok(format!("{table} as {alias}"))
839 } else {
840 Ok(table)
841 }
842 }
843
844 fn starts_join(&self) -> bool {
845 self.is_kw("join")
846 || self.is_kw("inner")
847 || self.is_kw("left")
848 || self.is_kw("right")
849 || self.is_kw("cross")
850 }
851
852 fn join_clause(&mut self) -> Result<String, ParseError> {
853 let kind = if self.eat_kw("inner") {
854 self.expect_kw("join")?;
855 "inner join"
856 } else if self.eat_kw("left") {
857 let _ = self.eat_kw("outer");
858 self.expect_kw("join")?;
859 "left join"
860 } else if self.eat_kw("right") {
861 let _ = self.eat_kw("outer");
862 self.expect_kw("join")?;
863 "right join"
864 } else if self.eat_kw("cross") {
865 self.expect_kw("join")?;
866 "cross join"
867 } else {
868 self.expect_kw("join")?;
869 "inner join"
870 };
871 let table = self.table_ref()?;
872 if kind == "cross join" {
873 return Ok(format!("{kind} {table}"));
874 }
875 self.expect_kw("on")?;
876 let on = self.expr_until(&[
877 "join", "inner", "left", "right", "cross", "where", "group", "having", "order",
878 "limit", "offset",
879 ])?;
880 Ok(format!("{kind} {table} on {on}"))
881 }
882
883 fn insert(&mut self) -> Result<String, ParseError> {
884 self.expect_kw("insert")?;
885 self.expect_kw("into")?;
886 let table = self.expect_ident("table name")?;
887 self.expect_sym('(')?;
888 let mut cols = Vec::new();
889 loop {
890 cols.push(self.expect_ident("column name")?);
891 if !self.eat_sym(',') {
892 break;
893 }
894 }
895 self.expect_sym(')')?;
896 self.expect_kw("values")?;
897 let mut rows = Vec::new();
898 loop {
899 self.expect_sym('(')?;
900 let mut vals = Vec::new();
901 loop {
902 vals.push(self.expr_until(&[])?);
903 if !self.eat_sym(',') {
904 break;
905 }
906 }
907 self.expect_sym(')')?;
908 if vals.len() != cols.len() {
909 return Err(ParseError::Syntax {
910 message: format!(
911 "INSERT has {} column(s) but {} value(s)",
912 cols.len(),
913 vals.len()
914 ),
915 });
916 }
917 let assigns = cols
918 .iter()
919 .zip(vals)
920 .map(|(c, v)| format!("{c} := {v}"))
921 .collect::<Vec<_>>();
922 rows.push(format!("{{ {} }}", assigns.join(", ")));
923 if !self.eat_sym(',') {
924 break;
925 }
926 }
927 let mut out = format!("insert {table} {}", rows.join(", "));
928 if self.returning_clause()? {
929 out.push_str(" returning");
930 }
931 Ok(out)
932 }
933
934 fn update(&mut self) -> Result<String, ParseError> {
935 self.expect_kw("update")?;
936 let table = self.expect_ident("table name")?;
937 self.qual_ctx = QualCtx::Single {
938 visible_name: table.clone(),
939 };
940 self.expect_kw("set")?;
941 let assigns = self.assignment_list_until(&["where", "returning"])?;
942 let filter = if self.eat_kw("where") {
943 Some(self.expr_until(&["returning"])?)
944 } else {
945 None
946 };
947 let mut out = table;
948 if let Some(f) = filter {
949 out.push_str(" filter ");
950 out.push_str(&f);
951 }
952 out.push_str(" update { ");
953 out.push_str(&assigns.join(", "));
954 out.push_str(" }");
955 if self.returning_clause()? {
956 out.push_str(" returning");
957 }
958 Ok(out)
959 }
960
961 fn delete(&mut self) -> Result<String, ParseError> {
962 self.expect_kw("delete")?;
963 self.expect_kw("from")?;
964 let table = self.expect_ident("table name")?;
965 self.qual_ctx = QualCtx::Single {
966 visible_name: table.clone(),
967 };
968 let filter = if self.eat_kw("where") {
969 Some(self.expr_until(&["returning"])?)
970 } else {
971 None
972 };
973 let mut out = table;
974 if let Some(f) = filter {
975 out.push_str(" filter ");
976 out.push_str(&f);
977 }
978 out.push_str(" delete");
979 if self.returning_clause()? {
980 out.push_str(" returning");
981 }
982 Ok(out)
983 }
984
985 fn default_literal(&mut self) -> Result<String, ParseError> {
989 match self.bump() {
990 Some(SqlTok::Number(n)) => Ok(n),
991 Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
992 Some(SqlTok::Word(w))
993 if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
994 {
995 Ok(w.to_ascii_lowercase())
996 }
997 other => Err(ParseError::Syntax {
998 message: format!(
999 "DEFAULT requires a literal value, got {}",
1000 other.map(|t| t.display()).unwrap_or_else(|| "<eof>".into())
1001 ),
1002 }),
1003 }
1004 }
1005
1006 fn returning_clause(&mut self) -> Result<bool, ParseError> {
1011 if !self.eat_kw("returning") {
1012 return Ok(false);
1013 }
1014 if !self.eat_sym('*') {
1015 return Err(ParseError::Syntax {
1016 message: "RETURNING currently supports only `RETURNING *` \
1017 (column projection is not yet supported)"
1018 .into(),
1019 });
1020 }
1021 Ok(true)
1022 }
1023
1024 fn create(&mut self) -> Result<String, ParseError> {
1025 self.expect_kw("create")?;
1026 if self.eat_kw("table") {
1027 let table = self.expect_ident("table name")?;
1028 self.expect_sym('(')?;
1029 let mut fields = Vec::new();
1030 while !self.eat_sym(')') {
1031 if self.is_kw("primary") || self.is_kw("foreign") || self.is_kw("constraint") {
1032 return Err(ParseError::Unsupported { feature: "SQL table constraints are not supported; declare UNIQUE columns or add indexes explicitly".into() });
1033 }
1034 let name = self.expect_ident("column name")?;
1035 let ty = self.sql_type()?;
1036 let mut required = false;
1037 let mut unique = false;
1038 let mut auto = false;
1039 let mut default: Option<String> = None;
1040 loop {
1041 if self.eat_kw("not") {
1042 self.expect_kw("null")?;
1043 required = true;
1044 } else if self.eat_kw("unique") {
1045 unique = true;
1046 } else if self.eat_kw("autoincrement") || self.eat_kw("auto_increment") {
1047 auto = true;
1048 } else if self.eat_kw("default") {
1049 default = Some(self.default_literal()?);
1050 } else if self.eat_kw("null") {
1051 } else {
1052 break;
1053 }
1054 }
1055 let mut mods = Vec::new();
1056 if required {
1057 mods.push("required");
1058 }
1059 if unique {
1060 mods.push("unique");
1061 }
1062 if auto {
1063 mods.push("auto");
1064 }
1065 let prefix = if mods.is_empty() {
1066 String::new()
1067 } else {
1068 format!("{} ", mods.join(" "))
1069 };
1070 let suffix = match default {
1071 Some(lit) => format!(" default {lit}"),
1072 None => String::new(),
1073 };
1074 fields.push(format!("{prefix}{name}: {ty}{suffix}"));
1075 let _ = self.eat_sym(',');
1076 }
1077 return Ok(format!("type {table} {{ {} }}", fields.join(", ")));
1078 }
1079 let unique = self.eat_kw("unique");
1080 self.expect_kw("index")?;
1081 let _idx = self.expect_ident("index name")?;
1082 self.expect_kw("on")?;
1083 let table = self.expect_ident("table name")?;
1084 self.expect_sym('(')?;
1085 let expression_parenthesized = self.eat_sym('(');
1086 if !matches!(self.peek(), Some(SqlTok::Word(_))) {
1087 return Err(ParseError::Unsupported {
1088 feature: "SQL expression indexes are not supported; use PowQL `alter <table> add index (.<json-column>-><path>)`"
1089 .into(),
1090 });
1091 }
1092 let col = self.expect_ident("column name")?;
1093 let mut path = format!(".{col}");
1094 let mut has_json_path = false;
1095 loop {
1096 match self.peek() {
1097 Some(SqlTok::Op(operator)) if operator == "->" => {
1098 self.bump();
1099 has_json_path = true;
1100 match self.bump() {
1101 Some(SqlTok::String(key)) => {
1102 path.push_str("->");
1103 path.push_str("e_powql_string(&key));
1104 }
1105 Some(SqlTok::Number(index))
1106 if !index.starts_with('-')
1107 && !index.contains('.')
1108 && index.parse::<u32>().is_ok() =>
1109 {
1110 path.push_str("->");
1111 path.push_str(&index);
1112 }
1113 Some(segment) => {
1114 return Err(ParseError::Unsupported {
1115 feature: format!(
1116 "SQL JSON expression indexes require string keys or non-negative integer path segments after ->, got {}",
1117 segment.display()
1118 ),
1119 });
1120 }
1121 None => {
1122 return Err(ParseError::UnexpectedToken {
1123 expected: "JSON path segment after ->".into(),
1124 got: "<eof>".into(),
1125 });
1126 }
1127 }
1128 }
1129 Some(SqlTok::Op(operator)) if operator == "->>" => {
1130 return Err(ParseError::Unsupported {
1131 feature:
1132 "SQL ->> text expressions cannot be indexed; use a direct JSON -> path"
1133 .into(),
1134 });
1135 }
1136 _ => break,
1137 }
1138 }
1139 if expression_parenthesized && !has_json_path {
1140 return Err(ParseError::Unsupported {
1141 feature: "SQL expression indexes support only direct JSON -> paths; use a plain column without extra parentheses"
1142 .into(),
1143 });
1144 }
1145 if expression_parenthesized && !self.eat_sym(')') {
1146 return Err(ParseError::Unsupported {
1147 feature: "SQL expression indexes support only direct JSON -> paths".into(),
1148 });
1149 }
1150 if !self.eat_sym(')') {
1151 return Err(ParseError::Unsupported {
1152 feature: "SQL expression and multi-column indexes are not supported; use PowQL `alter <table> add index (.<json-column>-><path>)` for a JSON path"
1153 .into(),
1154 });
1155 }
1156 Ok(if unique {
1157 if has_json_path {
1158 format!("alter {table} add unique ({path})")
1159 } else {
1160 format!("alter {table} add unique .{col}")
1161 }
1162 } else {
1163 if has_json_path {
1164 format!("alter {table} add index ({path})")
1165 } else {
1166 format!("alter {table} add index .{col}")
1167 }
1168 })
1169 }
1170
1171 fn drop_stmt(&mut self) -> Result<String, ParseError> {
1172 self.expect_kw("drop")?;
1173 if self.eat_kw("table") {
1174 let table = self.expect_ident("table name")?;
1175 Ok(format!("drop {table}"))
1176 } else if self.eat_kw("view") {
1177 let view = self.expect_ident("view name")?;
1178 Ok(format!("drop view {view}"))
1179 } else {
1180 Err(ParseError::UnexpectedToken {
1181 expected: "TABLE or VIEW".into(),
1182 got: self
1183 .peek()
1184 .map(|t| t.display())
1185 .unwrap_or_else(|| "<eof>".into()),
1186 })
1187 }
1188 }
1189
1190 fn alter(&mut self) -> Result<String, ParseError> {
1191 self.expect_kw("alter")?;
1192 self.expect_kw("table")?;
1193 let table = self.expect_ident("table name")?;
1194 if self.eat_kw("add") {
1195 let _ = self.eat_kw("column");
1196 let name = self.expect_ident("column name")?;
1197 let ty = self.sql_type()?;
1198 let mut required = false;
1199 if self.eat_kw("not") {
1200 self.expect_kw("null")?;
1201 required = true;
1202 }
1203 let prefix = if required { "required " } else { "" };
1204 Ok(format!("alter {table} add column {prefix}{name}: {ty}"))
1205 } else if self.eat_kw("drop") {
1206 let _ = self.eat_kw("column");
1207 let name = self.expect_ident("column name")?;
1208 Ok(format!("alter {table} drop column {name}"))
1209 } else {
1210 Err(ParseError::UnexpectedToken {
1211 expected: "ADD or DROP".into(),
1212 got: self
1213 .peek()
1214 .map(|t| t.display())
1215 .unwrap_or_else(|| "<eof>".into()),
1216 })
1217 }
1218 }
1219
1220 fn sql_type(&mut self) -> Result<String, ParseError> {
1221 let raw = self.expect_ident("type name")?;
1222 if self.eat_sym('(') {
1224 while !self.eat_sym(')') {
1225 if self.at_end() {
1226 return Err(ParseError::Syntax {
1227 message: "unterminated SQL type length".into(),
1228 });
1229 }
1230 self.bump();
1231 }
1232 }
1233 let ty = match raw.to_ascii_lowercase().as_str() {
1234 "text" | "varchar" | "char" | "string" | "str" => "str",
1235 "int" | "integer" | "bigint" | "smallint" => "int",
1236 "real" | "double" | "float" | "decimal" | "numeric" => "float",
1237 "bool" | "boolean" => "bool",
1238 "datetime" | "timestamp" => "datetime",
1239 "uuid" => "uuid",
1240 "blob" | "bytes" | "bytea" => "bytes",
1241 other => {
1242 return Err(ParseError::Unsupported {
1243 feature: format!("unsupported SQL type `{other}`"),
1244 })
1245 }
1246 };
1247 Ok(ty.into())
1248 }
1249
1250 fn assignment_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
1251 let mut out = Vec::new();
1252 loop {
1253 let name = self.expect_ident("column name")?;
1254 match self.bump() {
1255 Some(SqlTok::Op(op)) if op == "=" => {}
1256 Some(SqlTok::Op(op)) if op == "->" || op == "->>" => {
1262 return Err(ParseError::Unsupported {
1263 feature: format!(
1264 "cannot assign to a JSON path target `{name}{op}...`: JSON path \
1265 assignment targets are not supported; write the whole JSON column \
1266 instead (path mutation such as json_set is not yet available)"
1267 ),
1268 })
1269 }
1270 Some(t) => {
1271 return Err(ParseError::UnexpectedToken {
1272 expected: "=".into(),
1273 got: t.display(),
1274 })
1275 }
1276 None => {
1277 return Err(ParseError::UnexpectedToken {
1278 expected: "=".into(),
1279 got: "<eof>".into(),
1280 })
1281 }
1282 }
1283 let v = self.expr_until(stop)?;
1284 out.push(format!("{name} := {v}"));
1285 if !self.eat_sym(',') {
1286 break;
1287 }
1288 }
1289 Ok(out)
1290 }
1291
1292 fn expression_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
1293 let mut expressions = Vec::new();
1294 loop {
1295 expressions.push(self.expr_until(stop)?);
1296 if !self.eat_sym(',') || self.next_is_stop(stop) {
1297 break;
1298 }
1299 }
1300 Ok(expressions)
1301 }
1302
1303 fn order_list_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
1304 let mut parts = Vec::new();
1305 let mut expression_stop = Vec::with_capacity(stop.len() + 2);
1306 expression_stop.extend_from_slice(stop);
1307 expression_stop.extend_from_slice(&["asc", "desc"]);
1308 loop {
1309 let mut p = self.expr_until(&expression_stop)?;
1310 if self.eat_kw("desc") {
1311 p.push_str(" desc");
1312 } else if self.eat_kw("asc") {
1313 p.push_str(" asc");
1314 }
1315 parts.push(p);
1316 if !self.eat_sym(',') || self.next_is_stop(stop) {
1317 break;
1318 }
1319 }
1320 Ok(parts.join(", "))
1321 }
1322
1323 fn expr_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
1324 self.expr_bp(0, stop)
1325 }
1326
1327 fn expr_bp(&mut self, min_bp: u8, stop: &[&str]) -> Result<String, ParseError> {
1328 self.depth += 1;
1332 if self.depth > MAX_SQL_NESTING_DEPTH {
1333 return Err(ParseError::NestingDepthExceeded {
1334 max: MAX_SQL_NESTING_DEPTH,
1335 });
1336 }
1337 let mut lhs = if self.eat_kw("not") {
1338 format!("not ({})", self.expr_bp(5, stop)?)
1344 } else if self.eat_kw("exists") {
1345 if self.eat_sym('(') {
1346 if self.is_kw("select") {
1347 return Err(ParseError::Unsupported {
1348 feature:
1349 "SQL EXISTS subqueries are not supported yet; use PowQL EXISTS for now"
1350 .into(),
1351 });
1352 }
1353 return Err(ParseError::Syntax {
1354 message: "expected subquery after EXISTS".into(),
1355 });
1356 }
1357 return Err(ParseError::Syntax {
1358 message: "expected EXISTS (...)".into(),
1359 });
1360 } else if self.eat_sym('(') {
1361 if self.is_kw("select") {
1362 return Err(ParseError::Unsupported {
1363 feature:
1364 "SQL scalar subqueries are not supported yet; use PowQL subqueries for now"
1365 .into(),
1366 });
1367 }
1368 let inner = self.expr_bp(0, stop)?;
1369 self.expect_sym(')')?;
1370 format!("({inner})")
1371 } else {
1372 self.primary_expr()?
1373 };
1374
1375 let mut chain = 0usize;
1378 loop {
1379 if self.next_is_stop(stop)
1380 || self.at_end()
1381 || matches!(self.peek(), Some(SqlTok::Symbol(')' | ',')))
1382 {
1383 break;
1384 }
1385 chain += 1;
1386 if self.depth + chain > MAX_SQL_NESTING_DEPTH {
1387 return Err(ParseError::NestingDepthExceeded {
1388 max: MAX_SQL_NESTING_DEPTH,
1389 });
1390 }
1391 if matches!(self.peek(), Some(SqlTok::Op(op)) if op == "->" || op == "->>") {
1392 let text = matches!(self.bump(), Some(SqlTok::Op(op)) if op == "->>");
1393 let segment = match self.bump() {
1394 Some(SqlTok::String(key)) => quote_powql_string(&key),
1395 Some(SqlTok::Number(index))
1396 if !index.starts_with('-') && !index.contains('.') =>
1397 {
1398 index
1399 }
1400 Some(token) => {
1401 return Err(ParseError::Syntax {
1402 message: format!(
1403 "SQL JSON arrows require a string key or non-negative integer index, got {}",
1404 token.display()
1405 ),
1406 });
1407 }
1408 None => {
1409 return Err(ParseError::UnexpectedToken {
1410 expected: "JSON object key or array index".into(),
1411 got: "<eof>".into(),
1412 });
1413 }
1414 };
1415 let path = format!("{lhs}->{segment}");
1416 lhs = if text {
1417 format!("json_text({path})")
1418 } else {
1419 path
1420 };
1421 continue;
1422 }
1423 if self.eat_kw("is") {
1424 let not = self.eat_kw("not");
1425 self.expect_kw("null")?;
1426 lhs = if not {
1427 format!("{lhs} != null")
1428 } else {
1429 format!("{lhs} = null")
1430 };
1431 continue;
1432 }
1433 if self.eat_kw("not") {
1434 if self.eat_kw("in") {
1435 return Err(ParseError::Unsupported {
1436 feature:
1437 "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1438 .into(),
1439 });
1440 }
1441 if self.eat_kw("like") {
1442 let rhs = self.expr_bp(6, stop)?;
1443 lhs = format!("{lhs} not like {rhs}");
1444 continue;
1445 }
1446 if self.eat_kw("between") {
1447 return Err(ParseError::Unsupported {
1448 feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1449 });
1450 }
1451 return Err(ParseError::UnexpectedToken {
1452 expected: "IN, LIKE, or BETWEEN after NOT".into(),
1453 got: self
1454 .peek()
1455 .map(|t| t.display())
1456 .unwrap_or_else(|| "<eof>".into()),
1457 });
1458 }
1459 if self.eat_kw("in") {
1460 return Err(ParseError::Unsupported {
1461 feature: "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1462 .into(),
1463 });
1464 }
1465 if self.eat_kw("between") {
1466 return Err(ParseError::Unsupported {
1467 feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1468 });
1469 }
1470 if self.eat_kw("like") {
1471 let (l_bp, r_bp) = (5, 6);
1472 if l_bp < min_bp {
1473 self.pos -= 1;
1474 break;
1475 }
1476 let rhs = self.expr_bp(r_bp, stop)?;
1477 lhs = format!("{lhs} like {rhs}");
1478 continue;
1479 }
1480
1481 let op = if self.eat_kw("or") {
1482 "or".to_string()
1483 } else if self.eat_kw("and") {
1484 "and".to_string()
1485 } else if let Some(SqlTok::Op(op)) = self.peek().cloned() {
1486 self.pos += 1;
1487 op
1488 } else if self.eat_sym('*') {
1489 "*".into()
1490 } else {
1491 break;
1492 };
1493 let (l_bp, r_bp) = infix_bp(&op).ok_or_else(|| ParseError::Syntax {
1494 message: format!("unsupported SQL operator `{op}`"),
1495 })?;
1496 if l_bp < min_bp {
1497 self.pos -= 1;
1498 break;
1499 }
1500 let rhs = self.expr_bp(r_bp, stop)?;
1501 lhs = format!("{lhs} {op} {rhs}");
1502 }
1503 self.depth -= 1;
1504 Ok(lhs)
1505 }
1506
1507 fn primary_expr(&mut self) -> Result<String, ParseError> {
1508 match self.bump() {
1509 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("null") => Ok("null".into()),
1510 Some(SqlTok::Word(w))
1511 if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
1512 {
1513 Ok(w.to_ascii_lowercase())
1514 }
1515 Some(SqlTok::Word(w)) => {
1516 if self.eat_sym('(') {
1517 let func = w.to_ascii_lowercase();
1518 if func == "count" && self.eat_sym('*') {
1519 self.expect_sym(')')?;
1520 return Ok("count(*)".into());
1521 }
1522 let mut args = Vec::new();
1523 while !self.eat_sym(')') {
1524 args.push(self.expr_bp(0, &[])?);
1525 let _ = self.eat_sym(',');
1526 }
1527 return Ok(format!("{}({})", func, args.join(", ")));
1528 }
1529 if self.eat_sym('.') {
1530 let f = self.expect_ident("qualified column name")?;
1531 match &self.qual_ctx {
1532 QualCtx::Joined => Ok(format!("{w}.{f}")),
1533 QualCtx::Single { visible_name } => {
1534 if w.eq_ignore_ascii_case(visible_name) {
1535 Ok(format!(".{f}"))
1536 } else {
1537 Err(ParseError::Syntax {
1538 message: format!(
1539 "no such column: {w}.{f} (the only table in this \
1540 statement is `{visible_name}`)"
1541 ),
1542 })
1543 }
1544 }
1545 QualCtx::None => Err(ParseError::Syntax {
1546 message: format!(
1547 "qualified column reference `{w}.{f}` is not allowed here"
1548 ),
1549 }),
1550 }
1551 } else {
1552 Ok(format!(".{w}"))
1553 }
1554 }
1555 Some(SqlTok::Number(n)) => Ok(n),
1556 Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
1557 Some(SqlTok::Param(p)) => Ok(format!("${p}")),
1558 Some(SqlTok::Symbol('*')) => Ok("*".into()),
1559 Some(t) => Err(ParseError::Syntax {
1560 message: format!("unexpected SQL token in expression: {}", t.display()),
1561 }),
1562 None => Err(ParseError::UnexpectedToken {
1563 expected: "expression".into(),
1564 got: "<eof>".into(),
1565 }),
1566 }
1567 }
1568
1569 fn next_is_stop(&self, stop: &[&str]) -> bool {
1570 matches!(self.peek(), Some(SqlTok::Word(w)) if stop.iter().any(|kw| w.eq_ignore_ascii_case(kw)))
1571 }
1572}
1573
1574fn infix_bp(op: &str) -> Option<(u8, u8)> {
1575 Some(match op.to_ascii_lowercase().as_str() {
1576 "or" => (1, 2),
1577 "and" => (3, 4),
1578 "=" | "!=" | "<" | ">" | "<=" | ">=" => (5, 6),
1579 "+" | "-" => (7, 8),
1580 "*" | "/" => (9, 10),
1581 _ => return None,
1582 })
1583}
1584
1585fn quote_powql_string(s: &str) -> String {
1586 format!(
1587 "\"{}\"",
1588 s.replace('\\', "\\\\")
1589 .replace('"', "\\\"")
1590 .replace('\n', "\\n")
1591 .replace('\t', "\\t")
1592 )
1593}
1594
1595fn is_clause_kw(w: &str) -> bool {
1596 matches!(
1597 w.to_ascii_lowercase().as_str(),
1598 "where"
1599 | "group"
1600 | "having"
1601 | "order"
1602 | "limit"
1603 | "offset"
1604 | "join"
1605 | "inner"
1606 | "left"
1607 | "right"
1608 | "cross"
1609 | "on"
1610 | "values"
1611 | "set"
1612 )
1613}
1614fn is_join_modifier(w: &str) -> bool {
1615 matches!(
1616 w.to_ascii_lowercase().as_str(),
1617 "join" | "inner" | "left" | "right" | "cross" | "outer"
1618 )
1619}
1620fn is_reserved_identifier(w: &str) -> bool {
1621 matches!(
1622 w.to_ascii_lowercase().as_str(),
1623 "select"
1624 | "from"
1625 | "where"
1626 | "insert"
1627 | "into"
1628 | "values"
1629 | "update"
1630 | "set"
1631 | "delete"
1632 | "create"
1633 | "table"
1634 | "drop"
1635 | "alter"
1636 )
1637}
1638
1639#[cfg(test)]
1640mod tests {
1641 use super::*;
1642 use crate::ast::{AlterAction, IndexTarget};
1643
1644 #[test]
1645 fn sql_frontend_rejects_create_view() {
1646 let result = parse_sql("CREATE VIEW v AS SELECT id FROM Post");
1651 assert!(
1652 result.is_err(),
1653 "CREATE VIEW must be rejected by the SQL frontend, got {result:?}"
1654 );
1655 }
1656
1657 #[test]
1658 fn json_path_update_target_is_targeted_unsupported() {
1659 for stmt in [
1663 "UPDATE Doc SET data->'x' = 5",
1664 "UPDATE Doc SET data->>'x' = 5",
1665 ] {
1666 let err = parse_sql(stmt).unwrap_err();
1667 assert!(
1668 matches!(err, ParseError::Unsupported { .. }),
1669 "{stmt}: expected Unsupported, got {err:?}"
1670 );
1671 let msg = err.to_string();
1672 assert!(
1673 msg.contains("JSON path assignment targets are not supported"),
1674 "{stmt}: message must state the unsupported feature: {msg}"
1675 );
1676 assert!(
1677 msg.contains("json_set"),
1678 "{stmt}: message must point at the whole-column alternative: {msg}"
1679 );
1680 }
1681 assert!(parse_sql("UPDATE Doc SET data = '{}'").is_ok());
1683 }
1684
1685 #[test]
1686 fn json_arrows_lex_longest_token_and_lower_to_powql_paths() {
1687 assert_eq!(
1688 lex_sql("data->>'name'").unwrap(),
1689 vec![
1690 SqlTok::Word("data".into()),
1691 SqlTok::Op("->>".into()),
1692 SqlTok::String("name".into()),
1693 ]
1694 );
1695 let parsed = parse_sql_with_canonical(
1696 "SELECT data -> 'author' ->> 'name' AS name, data -> 'tags' -> 0 AS first FROM Post WHERE data ->> 'state' = 'ready'",
1697 )
1698 .unwrap();
1699 assert_eq!(
1700 parsed.canonical_powql,
1701 "Post filter json_text(.data->\"state\") = \"ready\" { name: json_text(.data->\"author\"->\"name\"), first: .data->\"tags\"->0 }"
1702 );
1703 let raw = parse_sql_with_canonical("SELECT data -> 'name' FROM Post").unwrap();
1704 let text = parse_sql_with_canonical("SELECT data ->> 'name' FROM Post").unwrap();
1705 assert_ne!(
1706 crate::canonicalize::canonicalize(&raw.canonical_powql)
1707 .unwrap()
1708 .0,
1709 crate::canonicalize::canonicalize(&text.canonical_powql)
1710 .unwrap()
1711 .0,
1712 "-> and ->> must never share a cached plan"
1713 );
1714
1715 let ordered = parse_sql_with_canonical(
1716 "SELECT id FROM Post ORDER BY data ->> 'rank' DESC, data -> 'tie' ASC",
1717 )
1718 .unwrap();
1719 assert_eq!(
1720 ordered.canonical_powql,
1721 "Post order json_text(.data->\"rank\") desc, .data->\"tie\" asc { .id }"
1722 );
1723
1724 let grouped = parse_sql_with_canonical(
1725 "SELECT data ->> 'kind' AS kind, COUNT(*) AS n FROM Post GROUP BY data ->> 'kind'",
1726 )
1727 .unwrap();
1728 assert_eq!(
1729 grouped.canonical_powql,
1730 "Post group json_text(.data->\"kind\") { kind: json_text(.data->\"kind\"), n: count(*) }"
1731 );
1732 }
1733
1734 #[test]
1735 fn json_arrows_reject_invalid_path_segments() {
1736 for sql in [
1737 "SELECT data -> other FROM Post",
1738 "SELECT data -> -1 FROM Post",
1739 "SELECT data -> 1.5 FROM Post",
1740 ] {
1741 let err = parse_sql_with_canonical(sql).unwrap_err();
1742 assert!(
1743 err.to_string()
1744 .contains("string key or non-negative integer index"),
1745 "unexpected error for `{sql}`: {err}"
1746 );
1747 }
1748 }
1749
1750 #[test]
1751 fn select_lowers_to_powql_ast() {
1752 let sql = parse_sql_with_canonical(
1753 "SELECT name, age FROM User WHERE age > 25 ORDER BY age DESC LIMIT 10",
1754 )
1755 .unwrap();
1756 assert_eq!(
1757 sql.canonical_powql,
1758 "User filter .age > 25 order .age desc limit 10 { .name, .age }"
1759 );
1760 assert_eq!(
1761 sql.statement,
1762 parser::parse("User filter .age > 25 order .age desc limit 10 { .name, .age }")
1763 .unwrap()
1764 );
1765 }
1766
1767 #[test]
1768 fn insert_update_delete_and_ddl_lower_to_existing_ast() {
1769 assert!(matches!(
1770 parse_sql("CREATE TABLE User (id INTEGER NOT NULL UNIQUE, name TEXT)").unwrap(),
1771 Statement::CreateType(_)
1772 ));
1773 assert!(matches!(
1774 parse_sql("INSERT INTO User (id, name) VALUES (1, 'Ada')").unwrap(),
1775 Statement::Insert(_)
1776 ));
1777 assert!(matches!(
1778 parse_sql("UPDATE User SET name = 'Grace' WHERE id = 1").unwrap(),
1779 Statement::UpdateQuery(_)
1780 ));
1781 assert!(matches!(
1782 parse_sql("DELETE FROM User WHERE id = 1").unwrap(),
1783 Statement::DeleteQuery(_)
1784 ));
1785 }
1786
1787 #[test]
1788 fn unsupported_sql_gets_explicit_error() {
1789 let err = parse_sql("SELECT name FROM User WHERE id IN (SELECT user_id FROM Orders)")
1790 .unwrap_err();
1791 assert!(err.to_string().contains("SQL IN"));
1792 }
1793
1794 #[test]
1795 fn sql_expression_index_has_targeted_powql_guidance() {
1796 assert!(matches!(
1797 parse_sql("CREATE INDEX post_slug ON Post (slug)").unwrap(),
1798 Statement::AlterTable(_)
1799 ));
1800 for sql in [
1801 "CREATE INDEX post_age ON Post ((data -> 'age'))",
1802 "CREATE INDEX post_first ON Post (data -> 'scores' -> 0)",
1803 "CREATE UNIQUE INDEX post_code ON Post ((data -> 'code'))",
1804 ] {
1805 let Statement::AlterTable(alter) = parse_sql(sql).unwrap() else {
1806 panic!("expected expression-index ALTER lowering for `{sql}`");
1807 };
1808 let target = match alter.action {
1809 AlterAction::AddIndex { target, .. } | AlterAction::AddUnique { target, .. } => {
1810 target
1811 }
1812 action => panic!("expected add-index action, got {action:?}"),
1813 };
1814 assert!(matches!(target, IndexTarget::JsonPath(_)), "{sql}");
1815 }
1816 let error = parse_sql("CREATE INDEX post_age ON Post (data->>'age')")
1817 .expect_err("SQL text extraction is not an indexable path")
1818 .to_string();
1819 assert!(error.contains("->>"));
1820 let error = parse_sql("CREATE INDEX post_age ON Post ((data + 1))")
1821 .expect_err("arbitrary SQL expressions remain unsupported")
1822 .to_string();
1823 assert!(error.contains("only direct JSON -> paths"));
1824 }
1825
1826 #[test]
1827 fn sql_aggregates_lower_with_raw_mode() {
1828 let Statement::Query(query) =
1829 parse_sql("SELECT dept, SUM(balance) AS total FROM Account GROUP BY dept").unwrap()
1830 else {
1831 panic!("expected query");
1832 };
1833 let projection = query.projection.expect("projection");
1834 assert!(matches!(
1835 projection[1].expr,
1836 Expr::FunctionCall(_, _, AggregateMode::Raw)
1837 ));
1838 }
1839
1840 #[test]
1841 fn ungrouped_join_aggregate_lowers_to_raw_powql_aggregate() {
1842 let lowered = parse_sql_with_canonical(
1843 "SELECT AVG(a.balance) FROM Account a JOIN Entry e ON a.id = e.account_id",
1844 )
1845 .unwrap();
1846 assert_eq!(
1847 lowered.canonical_powql,
1848 "avg(Account as a inner join Entry as e on a.id = e.account_id { a.balance })"
1849 );
1850 let Statement::Query(query) = lowered.statement else {
1851 panic!("expected query");
1852 };
1853 assert_eq!(
1854 query.aggregation.expect("aggregate").mode,
1855 AggregateMode::Raw
1856 );
1857 }
1858}