Skip to main content

sheeter_engine/
query.rs

1//! SQL query engine over files.
2//!
3//! [`run`] parses a `SELECT` statement, resolves file paths from the `FROM`
4//! clause relative to a base directory, executes the query in memory, and
5//! returns the result as a [`CanonicalValue::Array`] of row objects. The
6//! supported surface is deliberately small: `SELECT` with column references,
7//! aliases, and arithmetic; `WHERE`; `GROUP BY` with `SUM`, `AVG`, `COUNT`,
8//! `MIN`, and `MAX`; `HAVING` (evaluated on projected aliases); `ORDER BY`;
9//! `LIMIT`; `DISTINCT`; `INNER JOIN` and `LEFT JOIN` with `ON`; and a
10//! `FROM` clause that accepts a single file, a quoted glob pattern, or a
11//! comma-separated list of tables (which produces a cross join).
12//!
13//! Every format the sheeter engine reads can appear in `FROM`, so a single
14//! query can mix `.xlsx`, `.csv`, `.json`, `.ndjson`, `.yaml`, `.xml`,
15//! `.toml`, `.ods`, and `.sql` files without conversion.
16//!
17//! # Examples
18//!
19//! ```
20//! use sheeter_engine::query;
21//! use std::fs;
22//!
23//! let dir = tempfile::tempdir().unwrap();
24//! fs::write(dir.path().join("users.csv"), "id,name\n1,Alice\n2,Bob\n").unwrap();
25//! let result = query::run("SELECT name FROM users.csv WHERE id = 2", dir.path()).unwrap();
26//! assert_eq!(result.to_json().unwrap(), r#"[{"name":"Bob"}]"#);
27//! ```
28
29use 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
45/// Executes a `SELECT` statement over files in `base_dir`.
46///
47/// The `FROM` clause is resolved relative to `base_dir`. Each file is
48/// decoded using the sheeter engine's format detection, so a single query
49/// can join a CSV, an XLSX sheet, and an NDJSON stream without any manual
50/// conversion step. The result is returned as a [`CanonicalValue::Array`]
51/// whose elements are row objects, ready to be serialized in any format
52/// the engine writes.
53///
54/// Column references may be qualified (`u.name`) or bare (`name`). Bare
55/// names are resolved by suffix match, so `name` finds `users.name` when
56/// only one table has a column ending in `.name`. Comparison operands are
57/// coerced to `f64` when both sides parse as numbers, and to `String`
58/// otherwise, so `id = 2` matches a CSV cell that contains the string
59/// `"2"`.
60///
61/// # Errors
62///
63/// Returns an error if the SQL does not parse, the `FROM` clause references
64/// a missing file, a glob pattern fails to expand, a referenced column does
65/// not exist in any table, or an arithmetic operation cannot coerce its
66/// operands.
67///
68/// # Examples
69///
70/// ```
71/// use sheeter_engine::query;
72/// use std::fs;
73///
74/// let dir = tempfile::tempdir().unwrap();
75/// fs::write(dir.path().join("data.csv"), "a,b\n1,x\n2,y\n3,z\n").unwrap();
76/// let result = query::run(
77///     "SELECT a FROM data.csv WHERE b != 'z' ORDER BY a DESC",
78///     dir.path(),
79/// ).unwrap();
80/// assert_eq!(result.to_json().unwrap(), r#"[{"a":"2"},{"a":"1"}]"#);
81/// ```
82pub 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}