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: if quote == '"' {
305 "unterminated quoted identifier".into()
306 } else {
307 "unterminated string".into()
308 },
309 position: i,
310 });
311 }
312 if quote == '"' {
313 if s.is_empty() {
327 return Err(ParseError::Syntax {
328 message: "empty quoted identifier".into(),
329 });
330 }
331 if s.contains('`') {
332 return Err(ParseError::Unsupported {
333 feature: format!(
334 "quoted identifier `{s}` contains a backtick, which PowQL uses to \
335 quote identifiers and cannot escape"
336 ),
337 });
338 }
339 out.push(SqlTok::Word(format!("`{s}`")));
340 continue;
341 }
342 out.push(SqlTok::String(s));
343 continue;
344 }
345 if c == '$' {
346 i += 1;
347 let start = i;
348 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
349 i += 1;
350 }
351 out.push(SqlTok::Param(chars[start..i].iter().collect()));
352 continue;
353 }
354 if c == '-' && chars.get(i + 1) == Some(&'>') {
356 if chars.get(i + 2) == Some(&'>') {
357 out.push(SqlTok::Op("->>".into()));
358 i += 3;
359 } else {
360 out.push(SqlTok::Op("->".into()));
361 i += 2;
362 }
363 continue;
364 }
365 if c.is_ascii_digit() || (c == '-' && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit()))
366 {
367 let start = i;
368 i += 1;
369 while i < chars.len() && chars[i].is_ascii_digit() {
370 i += 1;
371 }
372 if i < chars.len()
373 && chars[i] == '.'
374 && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit())
375 {
376 i += 1;
377 while i < chars.len() && chars[i].is_ascii_digit() {
378 i += 1;
379 }
380 }
381 out.push(SqlTok::Number(chars[start..i].iter().collect()));
382 continue;
383 }
384 if c.is_alphabetic() || c == '_' {
385 let start = i;
386 i += 1;
387 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
388 i += 1;
389 }
390 out.push(SqlTok::Word(chars[start..i].iter().collect()));
391 continue;
392 }
393 if matches!(c, '(' | ')' | ',' | '*' | '.') {
394 out.push(SqlTok::Symbol(c));
395 i += 1;
396 continue;
397 }
398 if matches!(c, '=' | '<' | '>' | '!') {
399 let mut op = String::new();
400 op.push(c);
401 if matches!(chars.get(i + 1), Some('=') | Some('>')) {
402 op.push(chars[i + 1]);
403 i += 2;
404 } else {
405 i += 1;
406 }
407 if op == "<>" {
408 op = "!=".into();
409 }
410 out.push(SqlTok::Op(op));
411 continue;
412 }
413 if matches!(c, '+' | '-' | '/') {
414 out.push(SqlTok::Op(c.to_string()));
415 i += 1;
416 continue;
417 }
418 return Err(ParseError::Lex {
419 message: format!("unexpected SQL character `{c}`"),
420 position: i,
421 });
422 }
423 Ok(out)
424}
425
426const MAX_SQL_NESTING_DEPTH: usize = 64;
440
441struct SqlParser {
442 toks: Vec<SqlTok>,
443 pos: usize,
444 depth: usize,
445 qual_ctx: QualCtx,
449}
450
451#[derive(Clone, PartialEq)]
458enum QualCtx {
459 None,
461 Single { visible_name: String },
465 Joined,
467}
468
469struct Projection {
471 text: String,
474 agg: Option<AggCall>,
478}
479
480struct AggCall {
482 func: String,
484 arg: AggArg,
485}
486
487enum AggArg {
488 Star,
490 Field(String),
492}
493
494impl AggCall {
495 fn canonical(&self) -> String {
497 match &self.arg {
498 AggArg::Star => format!("{}(*)", self.func),
499 AggArg::Field(f) => format!("{}({f})", self.func),
500 }
501 }
502}
503
504fn build_ungrouped_aggregate(agg: &AggCall, inner: &str) -> Result<String, ParseError> {
510 match agg.func.as_str() {
511 "count" if matches!(agg.arg, AggArg::Star) => Ok(format!("count({inner})")),
512 "count" | "sum" | "avg" | "min" | "max" => match &agg.arg {
513 AggArg::Field(f) => Ok(format!("{}({inner} {{ {f} }})", agg.func)),
514 AggArg::Star => Err(ParseError::Unsupported {
515 feature: format!("{0}(*) is not valid; {0}() needs a column", agg.func),
516 }),
517 },
518 other => Err(ParseError::Syntax {
520 message: format!("unknown aggregate function `{other}`"),
521 }),
522 }
523}
524
525impl SqlParser {
526 fn at_end(&self) -> bool {
527 self.pos >= self.toks.len()
528 }
529 fn peek(&self) -> Option<&SqlTok> {
530 self.toks.get(self.pos)
531 }
532 fn bump(&mut self) -> Option<SqlTok> {
533 let t = self.toks.get(self.pos).cloned();
534 if t.is_some() {
535 self.pos += 1;
536 }
537 t
538 }
539 fn is_kw(&self, kw: &str) -> bool {
540 matches!(self.peek(), Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case(kw))
541 }
542 fn eat_kw(&mut self, kw: &str) -> bool {
543 if self.is_kw(kw) {
544 self.pos += 1;
545 true
546 } else {
547 false
548 }
549 }
550 fn expect_kw(&mut self, kw: &str) -> Result<(), ParseError> {
551 if self.eat_kw(kw) {
552 Ok(())
553 } else {
554 Err(ParseError::UnexpectedToken {
555 expected: kw.into(),
556 got: self
557 .peek()
558 .map(|t| t.display())
559 .unwrap_or_else(|| "<eof>".into()),
560 })
561 }
562 }
563 fn eat_sym(&mut self, c: char) -> bool {
564 if matches!(self.peek(), Some(SqlTok::Symbol(got)) if *got == c) {
565 self.pos += 1;
566 true
567 } else {
568 false
569 }
570 }
571 fn expect_sym(&mut self, c: char) -> Result<(), ParseError> {
572 if self.eat_sym(c) {
573 Ok(())
574 } else {
575 Err(ParseError::UnexpectedToken {
576 expected: c.to_string(),
577 got: self
578 .peek()
579 .map(|t| t.display())
580 .unwrap_or_else(|| "<eof>".into()),
581 })
582 }
583 }
584 fn expect_ident(&mut self, what: &str) -> Result<String, ParseError> {
585 match self.bump() {
586 Some(SqlTok::Word(w)) if !is_reserved_identifier(&w) => Ok(w),
587 Some(SqlTok::Word(w)) => Err(ParseError::Syntax {
588 message: format!("expected {what}, got reserved word `{w}`"),
589 }),
590 Some(t) => Err(ParseError::UnexpectedToken {
591 expected: what.into(),
592 got: t.display(),
593 }),
594 None => Err(ParseError::UnexpectedToken {
595 expected: what.into(),
596 got: "<eof>".into(),
597 }),
598 }
599 }
600
601 fn statement(&mut self) -> Result<String, ParseError> {
602 if self.is_kw("select") {
603 self.select()
604 } else if self.is_kw("insert") {
605 self.insert()
606 } else if self.is_kw("update") {
607 self.update()
608 } else if self.is_kw("delete") {
609 self.delete()
610 } else if self.is_kw("create") {
611 self.create()
612 } else if self.is_kw("drop") {
613 self.drop_stmt()
614 } else if self.is_kw("alter") {
615 self.alter()
616 } else if self.eat_kw("begin") {
617 let _ = self.eat_kw("transaction");
618 Ok("begin".into())
619 } else if self.eat_kw("commit") {
620 Ok("commit".into())
621 } else if self.eat_kw("rollback") {
622 Ok("rollback".into())
623 } else {
624 Err(ParseError::UnexpectedToken {
625 expected: "SQL statement".into(),
626 got: self
627 .peek()
628 .map(|t| t.display())
629 .unwrap_or_else(|| "<eof>".into()),
630 })
631 }
632 }
633
634 fn scan_select_qual_ctx(&self) -> QualCtx {
640 let mut i = self.pos;
641 let mut depth = 0usize;
642 loop {
643 match self.toks.get(i) {
644 None => return QualCtx::None,
645 Some(SqlTok::Symbol('(')) => depth += 1,
646 Some(SqlTok::Symbol(')')) => depth = depth.saturating_sub(1),
647 Some(SqlTok::Word(w)) if depth == 0 && w.eq_ignore_ascii_case("from") => break,
648 Some(_) => {}
649 }
650 i += 1;
651 }
652 let Some(SqlTok::Word(table)) = self.toks.get(i + 1) else {
653 return QualCtx::None;
655 };
656 let mut visible = table.clone();
657 let mut j = i + 2;
658 match self.toks.get(j) {
661 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("as") => {
662 if let Some(SqlTok::Word(a)) = self.toks.get(j + 1) {
663 visible = a.clone();
664 j += 2;
665 }
666 }
667 Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w) => {
668 visible = w.clone();
669 j += 1;
670 }
671 _ => {}
672 }
673 match self.toks.get(j) {
674 Some(SqlTok::Word(w)) if is_join_modifier(w) => QualCtx::Joined,
675 _ => QualCtx::Single {
676 visible_name: visible,
677 },
678 }
679 }
680
681 fn select(&mut self) -> Result<String, ParseError> {
682 self.expect_kw("select")?;
683 self.qual_ctx = self.scan_select_qual_ctx();
684 let distinct = self.eat_kw("distinct");
685 let projection = self.projection_list()?;
686 self.expect_kw("from")?;
687 let source = self.table_ref()?;
688 let mut joins = Vec::new();
689 while self.starts_join() {
690 joins.push(self.join_clause()?);
691 }
692 let filter = if self.eat_kw("where") {
693 Some(self.expr_until(&["group", "having", "order", "limit", "offset"])?)
694 } else {
695 None
696 };
697 let group = if self.eat_kw("group") {
698 self.expect_kw("by")?;
699 Some(self.expression_list_until(&["having", "order", "limit", "offset"])?)
700 } else {
701 None
702 };
703 let having = if self.eat_kw("having") {
704 Some(self.expr_until(&["order", "limit", "offset"])?)
705 } else {
706 None
707 };
708 let order = if self.eat_kw("order") {
709 self.expect_kw("by")?;
710 Some(self.order_list_until(&["limit", "offset"])?)
711 } else {
712 None
713 };
714 let limit = if self.eat_kw("limit") {
715 Some(self.expr_until(&["offset"])?)
716 } else {
717 None
718 };
719 let offset = if self.eat_kw("offset") {
720 Some(self.expr_until(&[])?)
721 } else {
722 None
723 };
724
725 let has_group = group.is_some();
726
727 let mut out = source;
728 for j in joins {
729 out.push(' ');
730 out.push_str(&j);
731 }
732 if distinct {
733 out.push_str(" distinct");
734 }
735 if let Some(f) = filter {
736 out.push_str(" filter ");
737 out.push_str(&f);
738 }
739 if let Some(keys) = group {
740 out.push_str(" group ");
741 out.push_str(&keys.join(", "));
742 if let Some(h) = having {
743 out.push_str(" having ");
744 out.push_str(&h);
745 }
746 } else if having.is_some() {
747 return Err(ParseError::Syntax {
748 message: "HAVING requires GROUP BY".into(),
749 });
750 }
751 if let Some(o) = order {
752 out.push_str(" order ");
753 out.push_str(&o);
754 }
755 if let Some(l) = limit {
756 out.push_str(" limit ");
757 out.push_str(&l);
758 }
759 if let Some(o) = offset {
760 out.push_str(" offset ");
761 out.push_str(&o);
762 }
763 if let Some(items) = projection {
764 if !has_group && items.iter().any(|p| p.agg.is_some()) {
769 if distinct {
770 return Err(ParseError::Unsupported {
771 feature: "aggregates with DISTINCT and no GROUP BY are not supported by the SQL frontend".into(),
772 });
773 }
774 if items.len() != 1 {
775 return Err(ParseError::Unsupported {
776 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(),
777 });
778 }
779 let agg = items.into_iter().next().unwrap().agg.unwrap();
781 return build_ungrouped_aggregate(&agg, &out);
782 }
783 out.push_str(" { ");
784 out.push_str(
785 &items
786 .iter()
787 .map(|p| p.text.as_str())
788 .collect::<Vec<_>>()
789 .join(", "),
790 );
791 out.push_str(" }");
792 }
793 Ok(out)
794 }
795
796 fn projection_list(&mut self) -> Result<Option<Vec<Projection>>, ParseError> {
797 if self.eat_sym('*') {
798 return Ok(None);
799 }
800 let mut fields = Vec::new();
801 loop {
802 let (expr, agg) = match self.try_aggregate()? {
807 Some(a) => (a.canonical(), Some(a)),
808 None => (self.expr_until(&["from", "as"])?, None),
809 };
810 let text = if self.eat_kw("as") {
811 let alias = self.expect_ident("projection alias")?;
812 format!("{alias}: {expr}")
813 } else {
814 expr
815 };
816 fields.push(Projection { text, agg });
817 if !self.eat_sym(',') {
818 break;
819 }
820 }
821 Ok(Some(fields))
822 }
823
824 fn try_aggregate(&mut self) -> Result<Option<AggCall>, ParseError> {
829 let Some(SqlTok::Word(w)) = self.peek().cloned() else {
830 return Ok(None);
831 };
832 let func = w.to_ascii_lowercase();
833 if !matches!(func.as_str(), "count" | "sum" | "avg" | "min" | "max") {
834 return Ok(None);
835 }
836 let save = self.pos;
837 self.pos += 1; if !self.eat_sym('(') {
839 self.pos = save;
840 return Ok(None);
841 }
842 let arg = if func == "count" && self.eat_sym('*') {
843 AggArg::Star
844 } else if func == "count" && self.is_kw("distinct") {
845 self.pos = save;
848 return Ok(None);
849 } else {
850 AggArg::Field(self.expr_bp(0, &[])?)
851 };
852 if self.eat_sym(')')
855 && (matches!(self.peek(), Some(SqlTok::Symbol(',')))
856 || self.is_kw("as")
857 || self.is_kw("from"))
858 {
859 Ok(Some(AggCall { func, arg }))
860 } else {
861 self.pos = save;
862 Ok(None)
863 }
864 }
865
866 fn table_ref(&mut self) -> Result<String, ParseError> {
867 let table = self.expect_ident("table name")?;
868 let has_alias = self.eat_kw("as")
869 || matches!(self.peek(), Some(SqlTok::Word(w)) if !is_clause_kw(w) && !is_join_modifier(w));
870 if has_alias {
871 let alias = self.expect_ident("table alias")?;
872 Ok(format!("{table} as {alias}"))
873 } else {
874 Ok(table)
875 }
876 }
877
878 fn starts_join(&self) -> bool {
879 self.is_kw("join")
880 || self.is_kw("inner")
881 || self.is_kw("left")
882 || self.is_kw("right")
883 || self.is_kw("cross")
884 }
885
886 fn join_clause(&mut self) -> Result<String, ParseError> {
887 let kind = if self.eat_kw("inner") {
888 self.expect_kw("join")?;
889 "inner join"
890 } else if self.eat_kw("left") {
891 let _ = self.eat_kw("outer");
892 self.expect_kw("join")?;
893 "left join"
894 } else if self.eat_kw("right") {
895 let _ = self.eat_kw("outer");
896 self.expect_kw("join")?;
897 "right join"
898 } else if self.eat_kw("cross") {
899 self.expect_kw("join")?;
900 "cross join"
901 } else {
902 self.expect_kw("join")?;
903 "inner join"
904 };
905 let table = self.table_ref()?;
906 if kind == "cross join" {
907 return Ok(format!("{kind} {table}"));
908 }
909 self.expect_kw("on")?;
910 let on = self.expr_until(&[
911 "join", "inner", "left", "right", "cross", "where", "group", "having", "order",
912 "limit", "offset",
913 ])?;
914 Ok(format!("{kind} {table} on {on}"))
915 }
916
917 fn insert(&mut self) -> Result<String, ParseError> {
918 self.expect_kw("insert")?;
919 self.expect_kw("into")?;
920 let table = self.expect_ident("table name")?;
921 self.expect_sym('(')?;
922 let mut cols = Vec::new();
923 loop {
924 cols.push(self.expect_ident("column name")?);
925 if !self.eat_sym(',') {
926 break;
927 }
928 }
929 self.expect_sym(')')?;
930 self.expect_kw("values")?;
931 let mut rows = Vec::new();
932 loop {
933 self.expect_sym('(')?;
934 let mut vals = Vec::new();
935 loop {
936 vals.push(self.expr_until(&[])?);
937 if !self.eat_sym(',') {
938 break;
939 }
940 }
941 self.expect_sym(')')?;
942 if vals.len() != cols.len() {
943 return Err(ParseError::Syntax {
944 message: format!(
945 "INSERT has {} column(s) but {} value(s)",
946 cols.len(),
947 vals.len()
948 ),
949 });
950 }
951 let assigns = cols
952 .iter()
953 .zip(vals)
954 .map(|(c, v)| format!("{c} := {v}"))
955 .collect::<Vec<_>>();
956 rows.push(format!("{{ {} }}", assigns.join(", ")));
957 if !self.eat_sym(',') {
958 break;
959 }
960 }
961 let mut out = format!("insert {table} {}", rows.join(", "));
962 if self.returning_clause()? {
963 out.push_str(" returning");
964 }
965 Ok(out)
966 }
967
968 fn update(&mut self) -> Result<String, ParseError> {
969 self.expect_kw("update")?;
970 let table = self.expect_ident("table name")?;
971 self.qual_ctx = QualCtx::Single {
972 visible_name: table.clone(),
973 };
974 self.expect_kw("set")?;
975 let assigns = self.assignment_list_until(&["where", "returning"])?;
976 let filter = if self.eat_kw("where") {
977 Some(self.expr_until(&["returning"])?)
978 } else {
979 None
980 };
981 let mut out = table;
982 if let Some(f) = filter {
983 out.push_str(" filter ");
984 out.push_str(&f);
985 }
986 out.push_str(" update { ");
987 out.push_str(&assigns.join(", "));
988 out.push_str(" }");
989 if self.returning_clause()? {
990 out.push_str(" returning");
991 }
992 Ok(out)
993 }
994
995 fn delete(&mut self) -> Result<String, ParseError> {
996 self.expect_kw("delete")?;
997 self.expect_kw("from")?;
998 let table = self.expect_ident("table name")?;
999 self.qual_ctx = QualCtx::Single {
1000 visible_name: table.clone(),
1001 };
1002 let filter = if self.eat_kw("where") {
1003 Some(self.expr_until(&["returning"])?)
1004 } else {
1005 None
1006 };
1007 let mut out = table;
1008 if let Some(f) = filter {
1009 out.push_str(" filter ");
1010 out.push_str(&f);
1011 }
1012 out.push_str(" delete");
1013 if self.returning_clause()? {
1014 out.push_str(" returning");
1015 }
1016 Ok(out)
1017 }
1018
1019 fn default_literal(&mut self) -> Result<String, ParseError> {
1023 match self.bump() {
1024 Some(SqlTok::Number(n)) => Ok(n),
1025 Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
1026 Some(SqlTok::Word(w))
1027 if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
1028 {
1029 Ok(w.to_ascii_lowercase())
1030 }
1031 other => Err(ParseError::Syntax {
1032 message: format!(
1033 "DEFAULT requires a literal value, got {}",
1034 other.map(|t| t.display()).unwrap_or_else(|| "<eof>".into())
1035 ),
1036 }),
1037 }
1038 }
1039
1040 fn returning_clause(&mut self) -> Result<bool, ParseError> {
1045 if !self.eat_kw("returning") {
1046 return Ok(false);
1047 }
1048 if !self.eat_sym('*') {
1049 return Err(ParseError::Syntax {
1050 message: "RETURNING currently supports only `RETURNING *` \
1051 (column projection is not yet supported)"
1052 .into(),
1053 });
1054 }
1055 Ok(true)
1056 }
1057
1058 fn create(&mut self) -> Result<String, ParseError> {
1059 self.expect_kw("create")?;
1060 if self.eat_kw("table") {
1061 let table = self.expect_ident("table name")?;
1062 self.expect_sym('(')?;
1063 let mut fields = Vec::new();
1064 while !self.eat_sym(')') {
1065 if self.is_kw("primary") || self.is_kw("foreign") || self.is_kw("constraint") {
1066 return Err(ParseError::Unsupported { feature: "SQL table constraints are not supported; declare UNIQUE columns or add indexes explicitly".into() });
1067 }
1068 let name = self.expect_ident("column name")?;
1069 let ty = self.sql_type()?;
1070 let mut required = false;
1071 let mut unique = false;
1072 let mut auto = false;
1073 let mut default: Option<String> = None;
1074 loop {
1075 if self.eat_kw("not") {
1076 self.expect_kw("null")?;
1077 required = true;
1078 } else if self.eat_kw("unique") {
1079 unique = true;
1080 } else if self.eat_kw("autoincrement") || self.eat_kw("auto_increment") {
1081 auto = true;
1082 } else if self.eat_kw("default") {
1083 default = Some(self.default_literal()?);
1084 } else if self.eat_kw("null") {
1085 } else {
1086 break;
1087 }
1088 }
1089 let mut mods = Vec::new();
1090 if required {
1091 mods.push("required");
1092 }
1093 if unique {
1094 mods.push("unique");
1095 }
1096 if auto {
1097 mods.push("auto");
1098 }
1099 let prefix = if mods.is_empty() {
1100 String::new()
1101 } else {
1102 format!("{} ", mods.join(" "))
1103 };
1104 let suffix = match default {
1105 Some(lit) => format!(" default {lit}"),
1106 None => String::new(),
1107 };
1108 fields.push(format!("{prefix}{name}: {ty}{suffix}"));
1109 let _ = self.eat_sym(',');
1110 }
1111 return Ok(format!("type {table} {{ {} }}", fields.join(", ")));
1112 }
1113 let unique = self.eat_kw("unique");
1114 self.expect_kw("index")?;
1115 let _idx = self.expect_ident("index name")?;
1116 self.expect_kw("on")?;
1117 let table = self.expect_ident("table name")?;
1118 self.expect_sym('(')?;
1119 let expression_parenthesized = self.eat_sym('(');
1120 if !matches!(self.peek(), Some(SqlTok::Word(_))) {
1121 return Err(ParseError::Unsupported {
1122 feature: "SQL expression indexes are not supported; use PowQL `alter <table> add index (.<json-column>-><path>)`"
1123 .into(),
1124 });
1125 }
1126 let col = self.expect_ident("column name")?;
1127 let mut path = format!(".{col}");
1128 let mut has_json_path = false;
1129 loop {
1130 match self.peek() {
1131 Some(SqlTok::Op(operator)) if operator == "->" => {
1132 self.bump();
1133 has_json_path = true;
1134 match self.bump() {
1135 Some(SqlTok::String(key)) => {
1136 path.push_str("->");
1137 path.push_str("e_powql_string(&key));
1138 }
1139 Some(SqlTok::Number(index))
1140 if !index.starts_with('-')
1141 && !index.contains('.')
1142 && index.parse::<u32>().is_ok() =>
1143 {
1144 path.push_str("->");
1145 path.push_str(&index);
1146 }
1147 Some(segment) => {
1148 return Err(ParseError::Unsupported {
1149 feature: format!(
1150 "SQL JSON expression indexes require string keys or non-negative integer path segments after ->, got {}",
1151 segment.display()
1152 ),
1153 });
1154 }
1155 None => {
1156 return Err(ParseError::UnexpectedToken {
1157 expected: "JSON path segment after ->".into(),
1158 got: "<eof>".into(),
1159 });
1160 }
1161 }
1162 }
1163 Some(SqlTok::Op(operator)) if operator == "->>" => {
1164 return Err(ParseError::Unsupported {
1165 feature:
1166 "SQL ->> text expressions cannot be indexed; use a direct JSON -> path"
1167 .into(),
1168 });
1169 }
1170 _ => break,
1171 }
1172 }
1173 if expression_parenthesized && !has_json_path {
1174 return Err(ParseError::Unsupported {
1175 feature: "SQL expression indexes support only direct JSON -> paths; use a plain column without extra parentheses"
1176 .into(),
1177 });
1178 }
1179 if expression_parenthesized && !self.eat_sym(')') {
1180 return Err(ParseError::Unsupported {
1181 feature: "SQL expression indexes support only direct JSON -> paths".into(),
1182 });
1183 }
1184 if !self.eat_sym(')') {
1185 return Err(ParseError::Unsupported {
1186 feature: "SQL expression and multi-column indexes are not supported; use PowQL `alter <table> add index (.<json-column>-><path>)` for a JSON path"
1187 .into(),
1188 });
1189 }
1190 Ok(if unique {
1191 if has_json_path {
1192 format!("alter {table} add unique ({path})")
1193 } else {
1194 format!("alter {table} add unique .{col}")
1195 }
1196 } else {
1197 if has_json_path {
1198 format!("alter {table} add index ({path})")
1199 } else {
1200 format!("alter {table} add index .{col}")
1201 }
1202 })
1203 }
1204
1205 fn drop_stmt(&mut self) -> Result<String, ParseError> {
1206 self.expect_kw("drop")?;
1207 if self.eat_kw("table") {
1208 let table = self.expect_ident("table name")?;
1209 Ok(format!("drop {table}"))
1210 } else if self.eat_kw("view") {
1211 let view = self.expect_ident("view name")?;
1212 Ok(format!("drop view {view}"))
1213 } else {
1214 Err(ParseError::UnexpectedToken {
1215 expected: "TABLE or VIEW".into(),
1216 got: self
1217 .peek()
1218 .map(|t| t.display())
1219 .unwrap_or_else(|| "<eof>".into()),
1220 })
1221 }
1222 }
1223
1224 fn alter(&mut self) -> Result<String, ParseError> {
1225 self.expect_kw("alter")?;
1226 self.expect_kw("table")?;
1227 let table = self.expect_ident("table name")?;
1228 if self.eat_kw("add") {
1229 let _ = self.eat_kw("column");
1230 let name = self.expect_ident("column name")?;
1231 let ty = self.sql_type()?;
1232 let mut required = false;
1233 if self.eat_kw("not") {
1234 self.expect_kw("null")?;
1235 required = true;
1236 }
1237 let prefix = if required { "required " } else { "" };
1238 Ok(format!("alter {table} add column {prefix}{name}: {ty}"))
1239 } else if self.eat_kw("drop") {
1240 let _ = self.eat_kw("column");
1241 let name = self.expect_ident("column name")?;
1242 Ok(format!("alter {table} drop column {name}"))
1243 } else {
1244 Err(ParseError::UnexpectedToken {
1245 expected: "ADD or DROP".into(),
1246 got: self
1247 .peek()
1248 .map(|t| t.display())
1249 .unwrap_or_else(|| "<eof>".into()),
1250 })
1251 }
1252 }
1253
1254 fn sql_type(&mut self) -> Result<String, ParseError> {
1255 let raw = self.expect_ident("type name")?;
1256 if self.eat_sym('(') {
1258 while !self.eat_sym(')') {
1259 if self.at_end() {
1260 return Err(ParseError::Syntax {
1261 message: "unterminated SQL type length".into(),
1262 });
1263 }
1264 self.bump();
1265 }
1266 }
1267 let ty = match raw.to_ascii_lowercase().as_str() {
1268 "text" | "varchar" | "char" | "string" | "str" => "str",
1269 "int" | "integer" | "bigint" | "smallint" => "int",
1270 "real" | "double" | "float" | "decimal" | "numeric" => "float",
1271 "bool" | "boolean" => "bool",
1272 "datetime" | "timestamp" => "datetime",
1273 "uuid" => "uuid",
1274 "blob" | "bytes" | "bytea" => "bytes",
1275 other => {
1276 return Err(ParseError::Unsupported {
1277 feature: format!("unsupported SQL type `{other}`"),
1278 })
1279 }
1280 };
1281 Ok(ty.into())
1282 }
1283
1284 fn assignment_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
1285 let mut out = Vec::new();
1286 loop {
1287 let name = self.expect_ident("column name")?;
1288 match self.bump() {
1289 Some(SqlTok::Op(op)) if op == "=" => {}
1290 Some(SqlTok::Op(op)) if op == "->" || op == "->>" => {
1296 return Err(ParseError::Unsupported {
1297 feature: format!(
1298 "cannot assign to a JSON path target `{name}{op}...`: JSON path \
1299 assignment targets are not supported; write the whole JSON column \
1300 instead (path mutation such as json_set is not yet available)"
1301 ),
1302 })
1303 }
1304 Some(t) => {
1305 return Err(ParseError::UnexpectedToken {
1306 expected: "=".into(),
1307 got: t.display(),
1308 })
1309 }
1310 None => {
1311 return Err(ParseError::UnexpectedToken {
1312 expected: "=".into(),
1313 got: "<eof>".into(),
1314 })
1315 }
1316 }
1317 let v = self.expr_until(stop)?;
1318 out.push(format!("{name} := {v}"));
1319 if !self.eat_sym(',') {
1320 break;
1321 }
1322 }
1323 Ok(out)
1324 }
1325
1326 fn expression_list_until(&mut self, stop: &[&str]) -> Result<Vec<String>, ParseError> {
1327 let mut expressions = Vec::new();
1328 loop {
1329 expressions.push(self.expr_until(stop)?);
1330 if !self.eat_sym(',') || self.next_is_stop(stop) {
1331 break;
1332 }
1333 }
1334 Ok(expressions)
1335 }
1336
1337 fn order_list_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
1338 let mut parts = Vec::new();
1339 let mut expression_stop = Vec::with_capacity(stop.len() + 2);
1340 expression_stop.extend_from_slice(stop);
1341 expression_stop.extend_from_slice(&["asc", "desc"]);
1342 loop {
1343 let mut p = self.expr_until(&expression_stop)?;
1344 if self.eat_kw("desc") {
1345 p.push_str(" desc");
1346 } else if self.eat_kw("asc") {
1347 p.push_str(" asc");
1348 }
1349 parts.push(p);
1350 if !self.eat_sym(',') || self.next_is_stop(stop) {
1351 break;
1352 }
1353 }
1354 Ok(parts.join(", "))
1355 }
1356
1357 fn expr_until(&mut self, stop: &[&str]) -> Result<String, ParseError> {
1358 self.expr_bp(0, stop)
1359 }
1360
1361 fn expr_bp(&mut self, min_bp: u8, stop: &[&str]) -> Result<String, ParseError> {
1362 self.depth += 1;
1366 if self.depth > MAX_SQL_NESTING_DEPTH {
1367 return Err(ParseError::NestingDepthExceeded {
1368 max: MAX_SQL_NESTING_DEPTH,
1369 });
1370 }
1371 let mut lhs = if self.eat_kw("not") {
1372 format!("not ({})", self.expr_bp(5, stop)?)
1378 } else if self.eat_kw("exists") {
1379 if self.eat_sym('(') {
1380 if self.is_kw("select") {
1381 return Err(ParseError::Unsupported {
1382 feature:
1383 "SQL EXISTS subqueries are not supported yet; use PowQL EXISTS for now"
1384 .into(),
1385 });
1386 }
1387 return Err(ParseError::Syntax {
1388 message: "expected subquery after EXISTS".into(),
1389 });
1390 }
1391 return Err(ParseError::Syntax {
1392 message: "expected EXISTS (...)".into(),
1393 });
1394 } else if self.eat_sym('(') {
1395 if self.is_kw("select") {
1396 return Err(ParseError::Unsupported {
1397 feature:
1398 "SQL scalar subqueries are not supported yet; use PowQL subqueries for now"
1399 .into(),
1400 });
1401 }
1402 let inner = self.expr_bp(0, stop)?;
1403 self.expect_sym(')')?;
1404 format!("({inner})")
1405 } else {
1406 self.primary_expr()?
1407 };
1408
1409 let mut chain = 0usize;
1412 loop {
1413 if self.next_is_stop(stop)
1414 || self.at_end()
1415 || matches!(self.peek(), Some(SqlTok::Symbol(')' | ',')))
1416 {
1417 break;
1418 }
1419 chain += 1;
1420 if self.depth + chain > MAX_SQL_NESTING_DEPTH {
1421 return Err(ParseError::NestingDepthExceeded {
1422 max: MAX_SQL_NESTING_DEPTH,
1423 });
1424 }
1425 if matches!(self.peek(), Some(SqlTok::Op(op)) if op == "->" || op == "->>") {
1426 let text = matches!(self.bump(), Some(SqlTok::Op(op)) if op == "->>");
1427 let segment = match self.bump() {
1428 Some(SqlTok::String(key)) => quote_powql_string(&key),
1429 Some(SqlTok::Number(index))
1430 if !index.starts_with('-') && !index.contains('.') =>
1431 {
1432 index
1433 }
1434 Some(token) => {
1435 return Err(ParseError::Syntax {
1436 message: format!(
1437 "SQL JSON arrows require a string key or non-negative integer index, got {}",
1438 token.display()
1439 ),
1440 });
1441 }
1442 None => {
1443 return Err(ParseError::UnexpectedToken {
1444 expected: "JSON object key or array index".into(),
1445 got: "<eof>".into(),
1446 });
1447 }
1448 };
1449 let path = format!("{lhs}->{segment}");
1450 lhs = if text {
1451 format!("json_text({path})")
1452 } else {
1453 path
1454 };
1455 continue;
1456 }
1457 if self.eat_kw("is") {
1458 let not = self.eat_kw("not");
1459 self.expect_kw("null")?;
1460 lhs = if not {
1461 format!("{lhs} != null")
1462 } else {
1463 format!("{lhs} = null")
1464 };
1465 continue;
1466 }
1467 if self.eat_kw("not") {
1468 if self.eat_kw("in") {
1469 return Err(ParseError::Unsupported {
1470 feature:
1471 "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1472 .into(),
1473 });
1474 }
1475 if self.eat_kw("like") {
1476 let rhs = self.expr_bp(6, stop)?;
1477 lhs = format!("{lhs} not like {rhs}");
1478 continue;
1479 }
1480 if self.eat_kw("between") {
1481 return Err(ParseError::Unsupported {
1482 feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1483 });
1484 }
1485 return Err(ParseError::UnexpectedToken {
1486 expected: "IN, LIKE, or BETWEEN after NOT".into(),
1487 got: self
1488 .peek()
1489 .map(|t| t.display())
1490 .unwrap_or_else(|| "<eof>".into()),
1491 });
1492 }
1493 if self.eat_kw("in") {
1494 return Err(ParseError::Unsupported {
1495 feature: "SQL IN lists/subqueries are not supported yet in the SQL frontend"
1496 .into(),
1497 });
1498 }
1499 if self.eat_kw("between") {
1500 return Err(ParseError::Unsupported {
1501 feature: "SQL BETWEEN is not supported yet in the SQL frontend".into(),
1502 });
1503 }
1504 if self.eat_kw("like") {
1505 let (l_bp, r_bp) = (5, 6);
1506 if l_bp < min_bp {
1507 self.pos -= 1;
1508 break;
1509 }
1510 let rhs = self.expr_bp(r_bp, stop)?;
1511 lhs = format!("{lhs} like {rhs}");
1512 continue;
1513 }
1514
1515 let op = if self.eat_kw("or") {
1516 "or".to_string()
1517 } else if self.eat_kw("and") {
1518 "and".to_string()
1519 } else if let Some(SqlTok::Op(op)) = self.peek().cloned() {
1520 self.pos += 1;
1521 op
1522 } else if self.eat_sym('*') {
1523 "*".into()
1524 } else {
1525 break;
1526 };
1527 let (l_bp, r_bp) = infix_bp(&op).ok_or_else(|| ParseError::Syntax {
1528 message: format!("unsupported SQL operator `{op}`"),
1529 })?;
1530 if l_bp < min_bp {
1531 self.pos -= 1;
1532 break;
1533 }
1534 let rhs = self.expr_bp(r_bp, stop)?;
1535 if rhs == "null" && matches!(op.as_str(), "=" | "!=" | "<>") {
1551 lhs = "false".to_string();
1552 continue;
1553 }
1554 lhs = format!("{lhs} {op} {rhs}");
1555 }
1556 self.depth -= 1;
1557 Ok(lhs)
1558 }
1559
1560 fn primary_expr(&mut self) -> Result<String, ParseError> {
1561 match self.bump() {
1562 Some(SqlTok::Word(w)) if w.eq_ignore_ascii_case("null") => Ok("null".into()),
1563 Some(SqlTok::Word(w))
1564 if w.eq_ignore_ascii_case("true") || w.eq_ignore_ascii_case("false") =>
1565 {
1566 Ok(w.to_ascii_lowercase())
1567 }
1568 Some(SqlTok::Word(w)) => {
1569 if self.eat_sym('(') {
1570 let func = w.to_ascii_lowercase();
1571 if func == "count" && self.eat_sym('*') {
1572 self.expect_sym(')')?;
1573 return Ok("count(*)".into());
1574 }
1575 let mut args = Vec::new();
1576 while !self.eat_sym(')') {
1577 args.push(self.expr_bp(0, &[])?);
1578 let _ = self.eat_sym(',');
1579 }
1580 return Ok(format!("{}({})", func, args.join(", ")));
1581 }
1582 if self.eat_sym('.') {
1583 let f = self.expect_ident("qualified column name")?;
1584 match &self.qual_ctx {
1585 QualCtx::Joined => Ok(format!("{w}.{f}")),
1586 QualCtx::Single { visible_name } => {
1587 if w.eq_ignore_ascii_case(visible_name) {
1588 Ok(format!(".{f}"))
1589 } else {
1590 Err(ParseError::Syntax {
1591 message: format!(
1592 "no such column: {w}.{f} (the only table in this \
1593 statement is `{visible_name}`)"
1594 ),
1595 })
1596 }
1597 }
1598 QualCtx::None => Err(ParseError::Syntax {
1599 message: format!(
1600 "qualified column reference `{w}.{f}` is not allowed here"
1601 ),
1602 }),
1603 }
1604 } else {
1605 Ok(format!(".{w}"))
1606 }
1607 }
1608 Some(SqlTok::Number(n)) => Ok(n),
1609 Some(SqlTok::String(s)) => Ok(quote_powql_string(&s)),
1610 Some(SqlTok::Param(p)) => Ok(format!("${p}")),
1611 Some(SqlTok::Symbol('*')) => Ok("*".into()),
1612 Some(t) => Err(ParseError::Syntax {
1613 message: format!("unexpected SQL token in expression: {}", t.display()),
1614 }),
1615 None => Err(ParseError::UnexpectedToken {
1616 expected: "expression".into(),
1617 got: "<eof>".into(),
1618 }),
1619 }
1620 }
1621
1622 fn next_is_stop(&self, stop: &[&str]) -> bool {
1623 matches!(self.peek(), Some(SqlTok::Word(w)) if stop.iter().any(|kw| w.eq_ignore_ascii_case(kw)))
1624 }
1625}
1626
1627fn infix_bp(op: &str) -> Option<(u8, u8)> {
1628 Some(match op.to_ascii_lowercase().as_str() {
1629 "or" => (1, 2),
1630 "and" => (3, 4),
1631 "=" | "!=" | "<" | ">" | "<=" | ">=" => (5, 6),
1632 "+" | "-" => (7, 8),
1633 "*" | "/" => (9, 10),
1634 _ => return None,
1635 })
1636}
1637
1638fn quote_powql_string(s: &str) -> String {
1639 format!(
1640 "\"{}\"",
1641 s.replace('\\', "\\\\")
1642 .replace('"', "\\\"")
1643 .replace('\n', "\\n")
1644 .replace('\t', "\\t")
1645 )
1646}
1647
1648fn is_clause_kw(w: &str) -> bool {
1649 matches!(
1650 w.to_ascii_lowercase().as_str(),
1651 "where"
1652 | "group"
1653 | "having"
1654 | "order"
1655 | "limit"
1656 | "offset"
1657 | "join"
1658 | "inner"
1659 | "left"
1660 | "right"
1661 | "cross"
1662 | "on"
1663 | "values"
1664 | "set"
1665 )
1666}
1667fn is_join_modifier(w: &str) -> bool {
1668 matches!(
1669 w.to_ascii_lowercase().as_str(),
1670 "join" | "inner" | "left" | "right" | "cross" | "outer"
1671 )
1672}
1673fn is_reserved_identifier(w: &str) -> bool {
1674 matches!(
1675 w.to_ascii_lowercase().as_str(),
1676 "select"
1677 | "from"
1678 | "where"
1679 | "insert"
1680 | "into"
1681 | "values"
1682 | "update"
1683 | "set"
1684 | "delete"
1685 | "create"
1686 | "table"
1687 | "drop"
1688 | "alter"
1689 )
1690}
1691
1692#[cfg(test)]
1693mod tests {
1694 use super::*;
1695 use crate::ast::{AlterAction, IndexTarget};
1696
1697 #[test]
1698 fn sql_frontend_rejects_create_view() {
1699 let result = parse_sql("CREATE VIEW v AS SELECT id FROM Post");
1704 assert!(
1705 result.is_err(),
1706 "CREATE VIEW must be rejected by the SQL frontend, got {result:?}"
1707 );
1708 }
1709
1710 #[test]
1711 fn json_path_update_target_is_targeted_unsupported() {
1712 for stmt in [
1716 "UPDATE Doc SET data->'x' = 5",
1717 "UPDATE Doc SET data->>'x' = 5",
1718 ] {
1719 let err = parse_sql(stmt).unwrap_err();
1720 assert!(
1721 matches!(err, ParseError::Unsupported { .. }),
1722 "{stmt}: expected Unsupported, got {err:?}"
1723 );
1724 let msg = err.to_string();
1725 assert!(
1726 msg.contains("JSON path assignment targets are not supported"),
1727 "{stmt}: message must state the unsupported feature: {msg}"
1728 );
1729 assert!(
1730 msg.contains("json_set"),
1731 "{stmt}: message must point at the whole-column alternative: {msg}"
1732 );
1733 }
1734 assert!(parse_sql("UPDATE Doc SET data = '{}'").is_ok());
1736 }
1737
1738 #[test]
1739 fn json_arrows_lex_longest_token_and_lower_to_powql_paths() {
1740 assert_eq!(
1741 lex_sql("data->>'name'").unwrap(),
1742 vec![
1743 SqlTok::Word("data".into()),
1744 SqlTok::Op("->>".into()),
1745 SqlTok::String("name".into()),
1746 ]
1747 );
1748 let parsed = parse_sql_with_canonical(
1749 "SELECT data -> 'author' ->> 'name' AS name, data -> 'tags' -> 0 AS first FROM Post WHERE data ->> 'state' = 'ready'",
1750 )
1751 .unwrap();
1752 assert_eq!(
1753 parsed.canonical_powql,
1754 "Post filter json_text(.data->\"state\") = \"ready\" { name: json_text(.data->\"author\"->\"name\"), first: .data->\"tags\"->0 }"
1755 );
1756 let raw = parse_sql_with_canonical("SELECT data -> 'name' FROM Post").unwrap();
1757 let text = parse_sql_with_canonical("SELECT data ->> 'name' FROM Post").unwrap();
1758 assert_ne!(
1759 crate::canonicalize::canonicalize(&raw.canonical_powql)
1760 .unwrap()
1761 .0,
1762 crate::canonicalize::canonicalize(&text.canonical_powql)
1763 .unwrap()
1764 .0,
1765 "-> and ->> must never share a cached plan"
1766 );
1767
1768 let ordered = parse_sql_with_canonical(
1769 "SELECT id FROM Post ORDER BY data ->> 'rank' DESC, data -> 'tie' ASC",
1770 )
1771 .unwrap();
1772 assert_eq!(
1773 ordered.canonical_powql,
1774 "Post order json_text(.data->\"rank\") desc, .data->\"tie\" asc { .id }"
1775 );
1776
1777 let grouped = parse_sql_with_canonical(
1778 "SELECT data ->> 'kind' AS kind, COUNT(*) AS n FROM Post GROUP BY data ->> 'kind'",
1779 )
1780 .unwrap();
1781 assert_eq!(
1782 grouped.canonical_powql,
1783 "Post group json_text(.data->\"kind\") { kind: json_text(.data->\"kind\"), n: count(*) }"
1784 );
1785 }
1786
1787 #[test]
1788 fn json_arrows_reject_invalid_path_segments() {
1789 for sql in [
1790 "SELECT data -> other FROM Post",
1791 "SELECT data -> -1 FROM Post",
1792 "SELECT data -> 1.5 FROM Post",
1793 ] {
1794 let err = parse_sql_with_canonical(sql).unwrap_err();
1795 assert!(
1796 err.to_string()
1797 .contains("string key or non-negative integer index"),
1798 "unexpected error for `{sql}`: {err}"
1799 );
1800 }
1801 }
1802
1803 #[test]
1804 fn select_lowers_to_powql_ast() {
1805 let sql = parse_sql_with_canonical(
1806 "SELECT name, age FROM User WHERE age > 25 ORDER BY age DESC LIMIT 10",
1807 )
1808 .unwrap();
1809 assert_eq!(
1810 sql.canonical_powql,
1811 "User filter .age > 25 order .age desc limit 10 { .name, .age }"
1812 );
1813 assert_eq!(
1814 sql.statement,
1815 parser::parse("User filter .age > 25 order .age desc limit 10 { .name, .age }")
1816 .unwrap()
1817 );
1818 }
1819
1820 #[test]
1821 fn insert_update_delete_and_ddl_lower_to_existing_ast() {
1822 assert!(matches!(
1823 parse_sql("CREATE TABLE User (id INTEGER NOT NULL UNIQUE, name TEXT)").unwrap(),
1824 Statement::CreateType(_)
1825 ));
1826 assert!(matches!(
1827 parse_sql("INSERT INTO User (id, name) VALUES (1, 'Ada')").unwrap(),
1828 Statement::Insert(_)
1829 ));
1830 assert!(matches!(
1831 parse_sql("UPDATE User SET name = 'Grace' WHERE id = 1").unwrap(),
1832 Statement::UpdateQuery(_)
1833 ));
1834 assert!(matches!(
1835 parse_sql("DELETE FROM User WHERE id = 1").unwrap(),
1836 Statement::DeleteQuery(_)
1837 ));
1838 }
1839
1840 #[test]
1841 fn unsupported_sql_gets_explicit_error() {
1842 let err = parse_sql("SELECT name FROM User WHERE id IN (SELECT user_id FROM Orders)")
1843 .unwrap_err();
1844 assert!(err.to_string().contains("SQL IN"));
1845 }
1846
1847 #[test]
1848 fn sql_expression_index_has_targeted_powql_guidance() {
1849 assert!(matches!(
1850 parse_sql("CREATE INDEX post_slug ON Post (slug)").unwrap(),
1851 Statement::AlterTable(_)
1852 ));
1853 for sql in [
1854 "CREATE INDEX post_age ON Post ((data -> 'age'))",
1855 "CREATE INDEX post_first ON Post (data -> 'scores' -> 0)",
1856 "CREATE UNIQUE INDEX post_code ON Post ((data -> 'code'))",
1857 ] {
1858 let Statement::AlterTable(alter) = parse_sql(sql).unwrap() else {
1859 panic!("expected expression-index ALTER lowering for `{sql}`");
1860 };
1861 let target = match alter.action {
1862 AlterAction::AddIndex { target, .. } | AlterAction::AddUnique { target, .. } => {
1863 target
1864 }
1865 action => panic!("expected add-index action, got {action:?}"),
1866 };
1867 assert!(matches!(target, IndexTarget::JsonPath(_)), "{sql}");
1868 }
1869 let error = parse_sql("CREATE INDEX post_age ON Post (data->>'age')")
1870 .expect_err("SQL text extraction is not an indexable path")
1871 .to_string();
1872 assert!(error.contains("->>"));
1873 let error = parse_sql("CREATE INDEX post_age ON Post ((data + 1))")
1874 .expect_err("arbitrary SQL expressions remain unsupported")
1875 .to_string();
1876 assert!(error.contains("only direct JSON -> paths"));
1877 }
1878
1879 #[test]
1880 fn sql_aggregates_lower_with_raw_mode() {
1881 let Statement::Query(query) =
1882 parse_sql("SELECT dept, SUM(balance) AS total FROM Account GROUP BY dept").unwrap()
1883 else {
1884 panic!("expected query");
1885 };
1886 let projection = query.projection.expect("projection");
1887 assert!(matches!(
1888 projection[1].expr,
1889 Expr::FunctionCall(_, _, AggregateMode::Raw)
1890 ));
1891 }
1892
1893 #[test]
1894 fn ungrouped_join_aggregate_lowers_to_raw_powql_aggregate() {
1895 let lowered = parse_sql_with_canonical(
1896 "SELECT AVG(a.balance) FROM Account a JOIN Entry e ON a.id = e.account_id",
1897 )
1898 .unwrap();
1899 assert_eq!(
1900 lowered.canonical_powql,
1901 "avg(Account as a inner join Entry as e on a.id = e.account_id { a.balance })"
1902 );
1903 let Statement::Query(query) = lowered.statement else {
1904 panic!("expected query");
1905 };
1906 assert_eq!(
1907 query.aggregation.expect("aggregate").mode,
1908 AggregateMode::Raw
1909 );
1910 }
1911}