1use crate::{detect_content, detect_extension, CanonicalValue, Format, SqlDialect};
30use serde_json::Number;
31use sqlparser::ast::{
32 BinaryOperator, Expr, FunctionArg, FunctionArgExpr, FunctionArguments, GroupByExpr,
33 JoinConstraint, JoinOperator, LimitClause, ObjectNamePart, OrderByExpr, OrderByKind, Query,
34 SelectItem, SetExpr, Statement, TableFactor, TableWithJoins, Value, ValueWithSpan,
35};
36use sqlparser::dialect::GenericDialect;
37use sqlparser::parser::Parser;
38use std::cmp::Ordering;
39use std::collections::{HashMap, HashSet};
40use std::error::Error;
41use std::path::{Path, PathBuf};
42
43type Row = Vec<(String, CanonicalValue)>;
44
45pub fn run(sql: &str, base_dir: &Path) -> Result<CanonicalValue, Box<dyn Error>> {
83 let stmts = Parser::parse_sql(&GenericDialect {}, sql)?;
84 let stmt = stmts.into_iter().next().ok_or("empty SQL")?;
85 match stmt {
86 Statement::Query(q) => execute_query(*q, base_dir),
87 _ => Err("only SELECT statements are supported".into()),
88 }
89}
90
91fn execute_query(query: Query, base_dir: &Path) -> Result<CanonicalValue, Box<dyn Error>> {
92 let body = (*query.body).clone();
93 let select = match body {
94 SetExpr::Select(s) => *s,
95 _ => return Err("only simple SELECT is supported".into()),
96 };
97
98 let mut rows = load_from(&select.from, base_dir)?;
99
100 if let Some(pred) = &select.selection {
101 let mut kept = Vec::new();
102 for row in rows {
103 if matches!(eval(pred.clone(), &row), Ok(CanonicalValue::Bool(true))) {
104 kept.push(row);
105 }
106 }
107 rows = kept;
108 }
109
110 let has_agg = select.projection.iter().any(|item| match item {
111 SelectItem::UnnamedExpr(e) | SelectItem::ExprWithAlias { expr: e, .. } => {
112 contains_aggregate(e)
113 }
114 _ => false,
115 });
116 let group_by = extract_group_by(&select.group_by);
117
118 let mut out_rows = if !group_by.is_empty() || has_agg {
119 aggregate(rows, &select.projection, &group_by, &select.having)?
120 } else {
121 project(rows, &select.projection)?
122 };
123
124 if select.distinct.is_some() {
125 let mut seen: HashSet<String> = HashSet::new();
126 out_rows.retain(|r| {
127 let key = r
128 .iter()
129 .map(|(_, v)| format!("{v:?}"))
130 .collect::<Vec<_>>()
131 .join("\x1f");
132 seen.insert(key)
133 });
134 }
135
136 if let Some(order) = &query.order_by {
137 if let OrderByKind::Expressions(exprs) = &order.kind {
138 sort_rows(&mut out_rows, exprs)?;
139 }
140 }
141
142 if let Some(lc) = &query.limit_clause {
143 if let Some(limit_expr) = extract_limit_expr(lc) {
144 if let Ok(CanonicalValue::Number(n)) = eval(limit_expr, &Vec::new()) {
145 if let Some(limit) = n.as_u64() {
146 out_rows.truncate(limit as usize);
147 }
148 }
149 }
150 }
151
152 Ok(CanonicalValue::Array(
153 out_rows.into_iter().map(CanonicalValue::Object).collect(),
154 ))
155}
156
157fn extract_group_by(g: &GroupByExpr) -> Vec<Expr> {
158 match g {
159 GroupByExpr::Expressions(exprs, _) => exprs.clone(),
160 _ => Vec::new(),
161 }
162}
163
164fn load_from(from: &[TableWithJoins], base_dir: &Path) -> Result<Vec<Row>, Box<dyn Error>> {
165 if from.is_empty() {
166 return Err("FROM is required".into());
167 }
168 let mut rows = load_table_with_joins(&from[0], base_dir)?;
169 for extra in &from[1..] {
170 let more = load_table_with_joins(extra, base_dir)?;
171 let mut crossed = Vec::new();
172 for a in &rows {
173 for b in &more {
174 let mut combined = a.clone();
175 combined.extend(b.clone());
176 crossed.push(combined);
177 }
178 }
179 rows = crossed;
180 }
181 Ok(rows)
182}
183
184fn load_table_with_joins(t: &TableWithJoins, base_dir: &Path) -> Result<Vec<Row>, Box<dyn Error>> {
185 let mut rows = load_factor(&t.relation, base_dir)?;
186 for join in &t.joins {
187 let right = load_factor(&join.relation, base_dir)?;
188 rows = apply_join(rows, right, &join.join_operator)?;
189 }
190 Ok(rows)
191}
192
193fn load_factor(factor: &TableFactor, base_dir: &Path) -> Result<Vec<Row>, Box<dyn Error>> {
194 let (name, alias) = match factor {
195 TableFactor::Table { name, alias, .. } => (name, alias),
196 _ => return Err("only table references are supported in FROM".into()),
197 };
198
199 let raw = name
200 .0
201 .iter()
202 .filter_map(|p| match p {
203 ObjectNamePart::Identifier(i) => Some(i.value.clone()),
204 _ => None,
205 })
206 .collect::<Vec<_>>()
207 .join(".");
208
209 let alias_name = alias
210 .as_ref()
211 .map(|a| a.name.value.clone())
212 .unwrap_or_else(|| {
213 Path::new(&raw)
214 .file_stem()
215 .and_then(|s| s.to_str())
216 .unwrap_or(&raw)
217 .to_string()
218 });
219
220 let mut all_rows = Vec::new();
221 if is_glob(&raw) {
222 let pattern = resolve_path(&raw, base_dir).to_string_lossy().to_string();
223 let entries = glob::glob(&pattern).map_err(|e| format!("invalid glob {pattern}: {e}"))?;
224 for entry in entries {
225 let p = entry?;
226 let mut r = load_file(&p)?;
227 all_rows.append(&mut r);
228 }
229 } else {
230 all_rows = load_file(&resolve_path(&raw, base_dir))?;
231 }
232
233 Ok(all_rows
234 .into_iter()
235 .map(|row| {
236 row.into_iter()
237 .map(|(k, v)| (format!("{alias_name}.{k}"), v))
238 .collect()
239 })
240 .collect())
241}
242
243fn is_glob(s: &str) -> bool {
244 s.contains('*') || s.contains('?') || s.contains('[')
245}
246
247fn resolve_path(raw: &str, base_dir: &Path) -> PathBuf {
248 let candidate = base_dir.join(raw);
249 if candidate.exists() || is_glob(raw) || Path::new(raw).is_absolute() {
250 return candidate;
251 }
252 let parts: Vec<&str> = raw.split('.').collect();
253 if parts.len() > 2 {
254 let prefix = parts[..parts.len() - 2].join("/");
255 let suffix = parts[parts.len() - 2..].join(".");
256 let slashed = base_dir.join(format!("{prefix}/{suffix}"));
257 if slashed.exists() {
258 return slashed;
259 }
260 }
261 candidate
262}
263
264fn load_file(path: &Path) -> Result<Vec<Row>, Box<dyn Error>> {
265 let bytes = std::fs::read(path)?;
266 let fmt = match detect_extension(path) {
267 Some(f) => f,
268 None => detect_content(&bytes)?,
269 };
270 let value = decode(fmt, &bytes)?;
271 match value {
272 CanonicalValue::Array(rows) => Ok(rows
273 .into_iter()
274 .filter_map(|r| match r {
275 CanonicalValue::Object(o) => Some(o),
276 _ => None,
277 })
278 .collect()),
279 _ => Err(format!("{} did not produce a table", path.display()).into()),
280 }
281}
282
283fn decode(fmt: Format, bytes: &[u8]) -> Result<CanonicalValue, Box<dyn Error>> {
284 let text = || -> Result<&str, Box<dyn Error>> { Ok(std::str::from_utf8(bytes)?) };
285 match fmt {
286 Format::Csv => CanonicalValue::from_csv(text()?),
287 Format::Tsv => CanonicalValue::from_tsv(text()?),
288 Format::Json => CanonicalValue::from_json(text()?),
289 Format::Ndjson => CanonicalValue::from_ndjson(text()?),
290 Format::Yaml => CanonicalValue::from_yaml(text()?),
291 Format::Xml => CanonicalValue::from_xml(text()?),
292 Format::Toml => CanonicalValue::from_toml(text()?),
293 Format::Xlsx => {
294 let mut sheets = CanonicalValue::from_xlsx_workbook(bytes)?;
295 if sheets.len() == 1 {
296 return Ok(sheets.remove(0).1);
297 }
298 let mut headers: Vec<String> = Vec::new();
299 let mut raw_rows: Vec<Vec<(String, CanonicalValue)>> = Vec::new();
300 for (_name, sheet) in sheets {
301 if let CanonicalValue::Array(rows) = sheet {
302 for row in rows {
303 if let CanonicalValue::Object(obj) = row {
304 for (k, _) in &obj {
305 if !headers.contains(k) {
306 headers.push(k.clone());
307 }
308 }
309 raw_rows.push(obj);
310 }
311 }
312 }
313 }
314 let rows_out: Vec<CanonicalValue> = raw_rows
315 .into_iter()
316 .map(|obj| {
317 let row: Vec<(String, CanonicalValue)> = headers
318 .iter()
319 .map(|h| {
320 let v = obj
321 .iter()
322 .find(|(k, _)| k == h)
323 .map(|(_, v)| v.clone())
324 .unwrap_or(CanonicalValue::Null);
325 (h.clone(), v)
326 })
327 .collect();
328 CanonicalValue::Object(row)
329 })
330 .collect();
331 Ok(CanonicalValue::Array(rows_out))
332 }
333 Format::Ods => CanonicalValue::from_ods(bytes),
334 Format::Parquet => crate::from_parquet(bytes),
335 Format::Sql => {
336 let mut sheets = CanonicalValue::from_sql_workbook(text()?, SqlDialect::Generic)?;
337 if sheets.len() != 1 {
338 return Err("multi-table SQL requires a single table".into());
339 }
340 Ok(sheets.remove(0).1)
341 }
342 }
343}
344
345fn apply_join(
346 left: Vec<Row>,
347 right: Vec<Row>,
348 op: &JoinOperator,
349) -> Result<Vec<Row>, Box<dyn Error>> {
350 let (constraint, is_left) = match op {
351 JoinOperator::Inner(c) => (c, false),
352 JoinOperator::Join(c) => (c, false),
353 JoinOperator::LeftOuter(c) => (c, true),
354 other => return Err(format!("unsupported join variant: {other:?}").into()),
355 };
356 let on = match constraint {
357 JoinConstraint::On(e) => e.clone(),
358 _ => return Err("only JOIN ... ON is supported".into()),
359 };
360
361 let mut result = Vec::new();
362 for l in &left {
363 let mut matched = false;
364 for r in &right {
365 let mut combined = l.clone();
366 combined.extend(r.clone());
367 if matches!(eval(on.clone(), &combined), Ok(CanonicalValue::Bool(true))) {
368 result.push(combined);
369 matched = true;
370 }
371 }
372 if is_left && !matched {
373 result.push(l.clone());
374 }
375 }
376 Ok(result)
377}
378
379fn eval(expr: Expr, row: &Row) -> Result<CanonicalValue, Box<dyn Error>> {
380 match expr {
381 Expr::Identifier(i) => resolve_column(&i.value, row),
382 Expr::CompoundIdentifier(parts) => {
383 let name = parts
384 .iter()
385 .map(|p| p.value.clone())
386 .collect::<Vec<_>>()
387 .join(".");
388 resolve_column(&name, row)
389 }
390 Expr::Value(ValueWithSpan { value, .. }) => Ok(literal(value)),
391 Expr::BinaryOp { left, op, right } => {
392 let l = eval(*left, row)?;
393 let r = eval(*right, row)?;
394 binary_op(l, &op, r)
395 }
396 Expr::Nested(inner) => eval(*inner, row),
397 other => Err(format!("unsupported expression: {other:?}").into()),
398 }
399}
400
401fn resolve_column(name: &str, row: &Row) -> Result<CanonicalValue, Box<dyn Error>> {
402 if let Some((_, v)) = row.iter().find(|(k, _)| k == name) {
403 return Ok(v.clone());
404 }
405 let suffix = format!(".{name}");
406 if let Some((_, v)) = row.iter().find(|(k, _)| k.ends_with(&suffix)) {
407 return Ok(v.clone());
408 }
409 Err(format!("column {name} not found").into())
410}
411
412fn literal(v: Value) -> CanonicalValue {
413 match v {
414 Value::Number(n, _) => {
415 if let Ok(i) = n.parse::<i64>() {
416 CanonicalValue::Number(Number::from(i))
417 } else if let Ok(f) = n.parse::<f64>() {
418 Number::from_f64(f)
419 .map(CanonicalValue::Number)
420 .unwrap_or(CanonicalValue::Null)
421 } else {
422 CanonicalValue::String(n)
423 }
424 }
425 Value::SingleQuotedString(s) | Value::DoubleQuotedString(s) => CanonicalValue::String(s),
426 Value::Boolean(b) => CanonicalValue::Bool(b),
427 Value::Null => CanonicalValue::Null,
428 _ => CanonicalValue::Null,
429 }
430}
431
432fn binary_op(
433 l: CanonicalValue,
434 op: &BinaryOperator,
435 r: CanonicalValue,
436) -> Result<CanonicalValue, Box<dyn Error>> {
437 if matches!(l, CanonicalValue::Null) || matches!(r, CanonicalValue::Null) {
438 match op {
439 BinaryOperator::Eq
440 | BinaryOperator::NotEq
441 | BinaryOperator::Lt
442 | BinaryOperator::LtEq
443 | BinaryOperator::Gt
444 | BinaryOperator::GtEq => return Ok(CanonicalValue::Bool(false)),
445 _ => {}
446 }
447 }
448 let ord = || compare_values(&l, &r);
449 match op {
450 BinaryOperator::Eq => return Ok(CanonicalValue::Bool(ord() == Ordering::Equal)),
451 BinaryOperator::NotEq => return Ok(CanonicalValue::Bool(ord() != Ordering::Equal)),
452 BinaryOperator::Lt => return Ok(CanonicalValue::Bool(ord() == Ordering::Less)),
453 BinaryOperator::LtEq => return Ok(CanonicalValue::Bool(ord() != Ordering::Greater)),
454 BinaryOperator::Gt => return Ok(CanonicalValue::Bool(ord() == Ordering::Greater)),
455 BinaryOperator::GtEq => return Ok(CanonicalValue::Bool(ord() != Ordering::Less)),
456 BinaryOperator::And => {
457 let a = matches!(l, CanonicalValue::Bool(true));
458 let b = matches!(r, CanonicalValue::Bool(true));
459 return Ok(CanonicalValue::Bool(a && b));
460 }
461 BinaryOperator::Or => {
462 let a = matches!(l, CanonicalValue::Bool(true));
463 let b = matches!(r, CanonicalValue::Bool(true));
464 return Ok(CanonicalValue::Bool(a || b));
465 }
466 _ => {}
467 }
468 let a = to_f64(&l);
469 let b = to_f64(&r);
470 let result = match (a, b) {
471 (Some(a), Some(b)) => match op {
472 BinaryOperator::Plus => Some(a + b),
473 BinaryOperator::Minus => Some(a - b),
474 BinaryOperator::Multiply => Some(a * b),
475 BinaryOperator::Divide => Some(a / b),
476 _ => None,
477 },
478 _ => None,
479 };
480 match result {
481 Some(f) => Ok(num(f)),
482 None => Err(format!("cannot apply {op:?} to {l:?} and {r:?}").into()),
483 }
484}
485
486fn to_f64(v: &CanonicalValue) -> Option<f64> {
487 match v {
488 CanonicalValue::Number(n) => n.as_f64(),
489 CanonicalValue::String(s) => s.parse().ok(),
490 CanonicalValue::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
491 _ => None,
492 }
493}
494
495fn num(f: f64) -> CanonicalValue {
496 if f.is_finite() && f.fract() == 0.0 && f.abs() < i64::MAX as f64 {
497 CanonicalValue::Number(Number::from(f as i64))
498 } else {
499 Number::from_f64(f)
500 .map(CanonicalValue::Number)
501 .unwrap_or(CanonicalValue::Null)
502 }
503}
504
505fn project(rows: Vec<Row>, projection: &[SelectItem]) -> Result<Vec<Row>, Box<dyn Error>> {
506 let mut out = Vec::with_capacity(rows.len());
507 for row in rows {
508 let mut new_row = Vec::new();
509 for item in projection {
510 match item {
511 SelectItem::UnnamedExpr(expr) => {
512 let name = expr_name(expr);
513 new_row.push((name, eval(expr.clone(), &row)?));
514 }
515 SelectItem::ExprWithAlias { expr, alias } => {
516 new_row.push((alias.value.clone(), eval(expr.clone(), &row)?));
517 }
518 SelectItem::Wildcard(_) => {
519 for (k, v) in &row {
520 let name = k.rsplit('.').next().unwrap_or(k).to_string();
521 new_row.push((name, v.clone()));
522 }
523 }
524 SelectItem::QualifiedWildcard(name, _) => {
525 let prefix = format!("{name}.");
526 for (k, v) in &row {
527 if let Some(short) = k.strip_prefix(&prefix) {
528 new_row.push((short.to_string(), v.clone()));
529 }
530 }
531 }
532 SelectItem::ExprWithAliases { .. } => {
533 return Err("multi-alias SELECT not supported".into());
534 }
535 }
536 }
537 out.push(new_row);
538 }
539 Ok(out)
540}
541
542fn expr_name(expr: &Expr) -> String {
543 match expr {
544 Expr::Identifier(i) => i.value.clone(),
545 Expr::CompoundIdentifier(parts) => {
546 parts.last().map(|p| p.value.clone()).unwrap_or_default()
547 }
548 Expr::Function(f) => {
549 let fname = f.name.to_string().to_lowercase();
550 let arg = match &f.args {
551 FunctionArguments::List(l) => l
552 .args
553 .first()
554 .and_then(|a| match a {
555 FunctionArg::Unnamed(FunctionArgExpr::Expr(e)) => Some(expr_name(e)),
556 _ => None,
557 })
558 .unwrap_or_default(),
559 _ => String::new(),
560 };
561 format!("{fname}({arg})")
562 }
563 Expr::BinaryOp { left, op, right } => {
564 format!("{} {} {}", expr_name(left), op, expr_name(right))
565 }
566 Expr::Nested(inner) => expr_name(inner),
567 _ => "expr".to_string(),
568 }
569}
570
571fn aggregate(
572 rows: Vec<Row>,
573 projection: &[SelectItem],
574 group_by: &[Expr],
575 having: &Option<Expr>,
576) -> Result<Vec<Row>, Box<dyn Error>> {
577 let mut groups: HashMap<Vec<String>, Vec<Row>> = HashMap::new();
578 if group_by.is_empty() {
579 groups.insert(Vec::new(), rows);
580 } else {
581 for row in rows {
582 let mut key = Vec::new();
583 for g in group_by {
584 let v = eval(g.clone(), &row)?;
585 key.push(format!("{v:?}"));
586 }
587 groups.entry(key).or_default().push(row);
588 }
589 }
590
591 let mut out = Vec::new();
592 for group in groups.into_values() {
593 let mut new_row = Vec::new();
594 for item in projection {
595 match item {
596 SelectItem::UnnamedExpr(expr) => {
597 let name = expr_name(expr);
598 new_row.push((name, eval_aggregated(expr, &group)?));
599 }
600 SelectItem::ExprWithAlias { expr, alias } => {
601 new_row.push((alias.value.clone(), eval_aggregated(expr, &group)?));
602 }
603 _ => return Err("unsupported aggregate SELECT item".into()),
604 }
605 }
606 if let Some(h) = having {
607 if !matches!(eval(h.clone(), &new_row), Ok(CanonicalValue::Bool(true))) {
608 continue;
609 }
610 }
611 out.push(new_row);
612 }
613 Ok(out)
614}
615
616fn eval_aggregated(expr: &Expr, group: &[Row]) -> Result<CanonicalValue, Box<dyn Error>> {
617 if let Expr::Function(f) = expr {
618 let name = f.name.to_string().to_uppercase();
619 if matches!(name.as_str(), "SUM" | "AVG" | "MIN" | "MAX" | "COUNT") {
620 let arg = match &f.args {
621 FunctionArguments::List(l) => l.args.first().and_then(|a| match a {
622 FunctionArg::Unnamed(FunctionArgExpr::Expr(e)) => Some(e.clone()),
623 _ => None,
624 }),
625 _ => None,
626 };
627 return compute_aggregate(&name, arg, group);
628 }
629 }
630 if let Some(first) = group.first() {
631 return eval(expr.clone(), first);
632 }
633 Ok(CanonicalValue::Null)
634}
635
636fn compute_aggregate(
637 name: &str,
638 arg: Option<Expr>,
639 group: &[Row],
640) -> Result<CanonicalValue, Box<dyn Error>> {
641 let arg = match arg {
642 Some(a) => a,
643 None => {
644 return match name {
645 "COUNT" => Ok(CanonicalValue::Number(Number::from(group.len() as i64))),
646 _ => Err(format!("{name} requires an argument").into()),
647 };
648 }
649 };
650
651 let mut count = 0usize;
652 let mut sum = 0.0f64;
653 let mut best: Option<f64> = None;
654
655 for row in group {
656 if let Ok(v) = eval(arg.clone(), row) {
657 if let CanonicalValue::Null = v {
658 continue;
659 }
660 if let Some(f) = to_f64(&v) {
661 sum += f;
662 count += 1;
663 best = Some(match best {
664 None => f,
665 Some(b) => match name {
666 "MIN" => b.min(f),
667 "MAX" => b.max(f),
668 _ => b,
669 },
670 });
671 }
672 }
673 }
674
675 Ok(match name {
676 "SUM" => num(sum),
677 "AVG" => {
678 if count > 0 {
679 num(sum / count as f64)
680 } else {
681 CanonicalValue::Null
682 }
683 }
684 "COUNT" => CanonicalValue::Number(Number::from(count as i64)),
685 "MIN" | "MAX" => best.map(num).unwrap_or(CanonicalValue::Null),
686 _ => CanonicalValue::Null,
687 })
688}
689
690fn extract_limit_expr(lc: &LimitClause) -> Option<Expr> {
691 match lc {
692 LimitClause::LimitOffset { limit, .. } => limit.clone(),
693 LimitClause::OffsetCommaLimit { limit, .. } => Some(limit.clone()),
694 }
695}
696
697fn sort_rows(rows: &mut [Row], order_by: &[OrderByExpr]) -> Result<(), Box<dyn Error>> {
698 let keys: Vec<(Expr, bool)> = order_by
699 .iter()
700 .map(|o| (o.expr.clone(), o.options.asc.unwrap_or(true)))
701 .collect();
702 rows.sort_by(|a, b| {
703 for (expr, asc) in &keys {
704 let av = eval(expr.clone(), a).unwrap_or(CanonicalValue::Null);
705 let bv = eval(expr.clone(), b).unwrap_or(CanonicalValue::Null);
706 let ord = compare_values(&av, &bv);
707 let ord = if *asc { ord } else { ord.reverse() };
708 if ord != Ordering::Equal {
709 return ord;
710 }
711 }
712 Ordering::Equal
713 });
714 Ok(())
715}
716
717fn compare_values(a: &CanonicalValue, b: &CanonicalValue) -> Ordering {
718 match (a, b) {
719 (CanonicalValue::Null, CanonicalValue::Null) => Ordering::Equal,
720 (CanonicalValue::Null, _) => Ordering::Less,
721 (_, CanonicalValue::Null) => Ordering::Greater,
722 _ => {
723 if let (Some(x), Some(y)) = (to_f64(a), to_f64(b)) {
724 return x.partial_cmp(&y).unwrap_or(Ordering::Equal);
725 }
726 scalar_string(a).cmp(&scalar_string(b))
727 }
728 }
729}
730
731fn scalar_string(v: &CanonicalValue) -> String {
732 match v {
733 CanonicalValue::Null => String::new(),
734 CanonicalValue::Bool(b) => b.to_string(),
735 CanonicalValue::Number(n) => n.to_string(),
736 CanonicalValue::String(s) => s.clone(),
737 CanonicalValue::Bytes(b) => b.iter().map(|x| format!("{x:02x}")).collect(),
738 _ => String::new(),
739 }
740}
741
742fn contains_aggregate(expr: &Expr) -> bool {
743 match expr {
744 Expr::Function(f) => {
745 let name = f.name.to_string().to_uppercase();
746 matches!(name.as_str(), "SUM" | "AVG" | "MIN" | "MAX" | "COUNT")
747 }
748 Expr::BinaryOp { left, right, .. } => contains_aggregate(left) || contains_aggregate(right),
749 Expr::Nested(inner) => contains_aggregate(inner),
750 _ => false,
751 }
752}