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