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