Skip to main content

mdql_core/
query_engine.rs

1//! Execute parsed queries over in-memory rows.
2
3use std::cmp::Ordering;
4use std::collections::HashMap;
5
6use regex::Regex;
7
8use crate::errors::MdqlError;
9use crate::model::{Row, Value};
10use crate::query_parser::*;
11use crate::schema::Schema;
12
13pub fn execute_query(
14    query: &SelectQuery,
15    rows: &[Row],
16    _schema: &Schema,
17) -> crate::errors::Result<(Vec<Row>, Vec<String>)> {
18    if let Some(ref sub) = query.subquery {
19        let (sub_rows, _sub_cols) = execute_inner(sub, rows, None)?;
20        return execute_inner(query, &sub_rows, None);
21    }
22    execute_inner(query, rows, None)
23}
24
25#[allow(dead_code)]
26pub(crate) fn execute_query_indexed(
27    query: &SelectQuery,
28    rows: &[Row],
29    schema: &Schema,
30    index: Option<&crate::index::TableIndex>,
31    searcher: Option<&crate::search::TableSearcher>,
32) -> crate::errors::Result<(Vec<Row>, Vec<String>)> {
33    // Pre-compute FTS results for any LIKE clauses on section columns
34    let fts_results = if let (Some(ref wc), Some(searcher)) = (&query.where_clause, searcher) {
35        collect_fts_results(wc, schema, searcher)
36    } else {
37        HashMap::new()
38    };
39
40    execute_with_fts(query, rows, index, &fts_results)
41}
42
43#[allow(dead_code)]
44fn collect_fts_results(
45    clause: &WhereClause,
46    schema: &Schema,
47    searcher: &crate::search::TableSearcher,
48) -> HashMap<(String, String), std::collections::HashSet<String>> {
49    let mut results = HashMap::new();
50    collect_fts_results_inner(clause, schema, searcher, &mut results);
51    results
52}
53
54#[allow(dead_code)]
55fn collect_fts_results_inner(
56    clause: &WhereClause,
57    schema: &Schema,
58    searcher: &crate::search::TableSearcher,
59    results: &mut HashMap<(String, String), std::collections::HashSet<String>>,
60) {
61    match clause {
62        WhereClause::Comparison(cmp) => {
63            if (cmp.op == CmpOp::Like || cmp.op == CmpOp::NotLike) && schema.sections.contains_key(&cmp.column) {
64                if let Some(SqlValue::String(pattern)) = &cmp.value {
65                    // Strip SQL wildcards for Tantivy query
66                    let search_term = pattern.replace('%', " ").replace('_', " ").trim().to_string();
67                    if !search_term.is_empty() {
68                        if let Ok(paths) = searcher.search(&search_term, Some(&cmp.column)) {
69                            let key = (cmp.column.clone(), pattern.clone());
70                            results.insert(key, paths.into_iter().collect());
71                        }
72                    }
73                }
74            }
75        }
76        WhereClause::BoolOp(bop) => {
77            collect_fts_results_inner(&bop.left, schema, searcher, results);
78            collect_fts_results_inner(&bop.right, schema, searcher, results);
79        }
80    }
81}
82
83type FtsResults = HashMap<(String, String), std::collections::HashSet<String>>;
84
85fn execute_with_fts(
86    query: &SelectQuery,
87    rows: &[Row],
88    index: Option<&crate::index::TableIndex>,
89    fts: &FtsResults,
90) -> crate::errors::Result<(Vec<Row>, Vec<String>)> {
91    // Determine available columns
92    let mut all_columns: Vec<String> = Vec::new();
93    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
94    for r in rows {
95        for k in r.keys() {
96            if seen.insert(k.clone()) {
97                all_columns.push(k.clone());
98            }
99        }
100    }
101
102    // Check if query has aggregates
103    let has_aggregates = match &query.columns {
104        ColumnList::Named(exprs) => exprs.iter().any(|e| e.is_aggregate()),
105        _ => false,
106    };
107
108    // Output column names
109    let columns: Vec<String> = match &query.columns {
110        ColumnList::All => all_columns,
111        ColumnList::Named(exprs) => exprs.iter().map(|e| e.output_name()).collect(),
112    };
113
114    // Reject duplicate output names. A result row is a key->value map, so two
115    // columns with the same output name cannot both be represented — the dict
116    // would collapse them and the header/row lengths would silently disagree.
117    // Require the caller to disambiguate with AS.
118    if let ColumnList::Named(_) = &query.columns {
119        let mut seen = std::collections::HashSet::new();
120        for c in &columns {
121            if !seen.insert(c.as_str()) {
122                return Err(MdqlError::QueryExecution(format!(
123                    "duplicate output column '{}' — give each projection a unique name with AS",
124                    c
125                )));
126            }
127        }
128    }
129
130    // Filter — try index first, fall back to full scan
131    let filtered: Vec<Row> = if let Some(ref wc) = query.where_clause {
132        let candidate_paths = index.and_then(|idx| try_index_filter(wc, idx));
133        if let Some(paths) = candidate_paths {
134            rows.iter()
135                .filter(|r| {
136                    r.get("path")
137                        .and_then(|v| v.as_str())
138                        .map_or(false, |p| paths.contains(p))
139                })
140                .filter(|r| evaluate_with_fts(wc, r, fts))
141                .cloned()
142                .collect()
143        } else {
144            rows.iter()
145                .filter(|r| evaluate_with_fts(wc, r, fts))
146                .cloned()
147                .collect()
148        }
149    } else {
150        rows.to_vec()
151    };
152
153    // Aggregate if needed
154    let mut result = if has_aggregates || query.group_by.is_some() {
155        let exprs = match &query.columns {
156            ColumnList::Named(exprs) => exprs.clone(),
157            _ => return Err(MdqlError::QueryExecution(
158                "SELECT * with GROUP BY is not supported".into(),
159            )),
160        };
161        let group_keys = query.group_by.as_deref().unwrap_or(&[]);
162        aggregate_rows(&filtered, &exprs, group_keys)?
163    } else {
164        filtered
165    };
166
167    // HAVING filter — apply after aggregation
168    if let Some(ref having) = query.having {
169        result.retain(|row| evaluate(having, row));
170    }
171
172    // Window functions — compute after aggregation/HAVING, before ORDER BY
173    let has_windows = match &query.columns {
174        ColumnList::Named(exprs) => exprs.iter().any(|e| match e {
175            SelectExpr::Expr { expr, .. } => expr.contains_window(),
176            _ => false,
177        }),
178        _ => false,
179    };
180    if has_windows {
181        if let ColumnList::Named(ref exprs) = query.columns {
182            compute_windows(&mut result, exprs)?;
183        }
184    }
185
186    // DISTINCT — dedupe on the projected output values, keeping the first
187    // occurrence. Runs before ORDER BY and LIMIT per SQL semantics.
188    if query.distinct {
189        let mut seen = std::collections::HashSet::new();
190        result.retain(|row| seen.insert(distinct_key(row, &query.columns, &columns)));
191    }
192
193    // Sort — resolve ORDER BY aliases against SELECT list
194    if let Some(ref order_by) = query.order_by {
195        let resolved = resolve_order_aliases(order_by, &query.columns);
196        sort_rows(&mut result, &resolved);
197    }
198
199    // Limit
200    if let Some(limit) = query.limit {
201        result.truncate(limit as usize);
202    }
203
204    // Project — evaluate expressions and strip to requested columns
205    if !matches!(query.columns, ColumnList::All) {
206        let named_exprs = match &query.columns {
207            ColumnList::Named(exprs) => exprs,
208            _ => unreachable!(),
209        };
210
211        // Compute expression columns first, then retain only requested columns.
212        // Skip if aggregation already computed them (re-evaluating would lose
213        // columns that only existed in pre-aggregation rows, e.g. dict fields).
214        let has_expr_cols = named_exprs.iter().any(|e| matches!(e, SelectExpr::Expr { .. }));
215        let already_aggregated = has_aggregates || query.group_by.is_some();
216        if has_expr_cols && !already_aggregated {
217            for row in &mut result {
218                for expr in named_exprs {
219                    if let SelectExpr::Expr { expr: e, alias } = expr {
220                        if e.contains_window() { continue; }
221                        let name = alias.clone().unwrap_or_else(|| e.display_name());
222                        let val = evaluate_expr(e, row);
223                        row.insert(name, val);
224                    }
225                }
226            }
227        }
228
229        // Materialize dotted dict references (e.g. `params.key`, `b.params.key`)
230        // as top-level keys so they survive the retain/null-fill below. WHERE
231        // resolves these via evaluate_expr; projection must match (see
232        // resolve_column). Aggregated rows are handled in aggregate_rows.
233        if !already_aggregated {
234            for row in &mut result {
235                for expr in named_exprs {
236                    if let SelectExpr::Column(name) = expr {
237                        if name.contains('.') && !row.contains_key(name) {
238                            if let Some(v) = resolve_column(row, name) {
239                                row.insert(name.clone(), v);
240                            }
241                        }
242                    }
243                }
244            }
245        }
246
247        let col_set: std::collections::HashSet<&str> =
248            columns.iter().map(|s| s.as_str()).collect();
249        for row in &mut result {
250            row.retain(|k, _| col_set.contains(k.as_str()));
251        }
252    }
253
254    // Null-fill so every result row carries every header column as a key.
255    // A requested column may be absent from a row because it does not exist
256    // on the table at all, or because it is an optional field/section missing
257    // on that row (and SELECT * unions keys across rows). Inserting Null keeps
258    // the column header aligned with each row dict, so consumers that zip
259    // `columns` with row values stay in sync.
260    for row in &mut result {
261        for col in &columns {
262            if !row.contains_key(col) {
263                row.insert(col.clone(), Value::Null);
264            }
265        }
266    }
267
268    Ok((result, columns))
269}
270
271/// Build a dedup key for SELECT DISTINCT from a row's projected output values.
272///
273/// Mirrors how projection resolves each output column: plain columns and
274/// already-computed aggregate/window outputs read from the row; other
275/// expressions are evaluated. Values are serialized via Debug, which
276/// distinguishes variants (Null vs empty string, Int(1) vs Bool(true)).
277fn distinct_key(row: &Row, column_spec: &ColumnList, header: &[String]) -> String {
278    let mut parts: Vec<String> = Vec::new();
279    match column_spec {
280        ColumnList::All => {
281            for col in header {
282                parts.push(format!("{:?}", row.get(col).unwrap_or(&Value::Null)));
283            }
284        }
285        ColumnList::Named(exprs) => {
286            for se in exprs {
287                let val = match se {
288                    SelectExpr::Column(name) => {
289                        resolve_column(row, name).unwrap_or(Value::Null)
290                    }
291                    SelectExpr::Aggregate { .. } => {
292                        row.get(&se.output_name()).cloned().unwrap_or(Value::Null)
293                    }
294                    SelectExpr::Expr { expr, .. } => {
295                        if expr.contains_window() {
296                            row.get(&se.output_name()).cloned().unwrap_or(Value::Null)
297                        } else {
298                            evaluate_expr(expr, row)
299                        }
300                    }
301                };
302                parts.push(format!("{:?}", val));
303            }
304        }
305    }
306    parts.join("\u{1f}")
307}
308
309fn aggregate_rows(
310    rows: &[Row],
311    exprs: &[SelectExpr],
312    group_keys: &[String],
313) -> crate::errors::Result<Vec<Row>> {
314    // Group rows by group_keys
315    let mut groups: Vec<(Vec<Value>, Vec<&Row>)> = Vec::new();
316    let mut key_index: HashMap<Vec<String>, usize> = HashMap::new();
317
318    if group_keys.is_empty() {
319        // No GROUP BY — all rows are one group
320        let all_refs: Vec<&Row> = rows.iter().collect();
321        groups.push((vec![], all_refs));
322    } else {
323        for row in rows {
324            let key: Vec<String> = group_keys
325                .iter()
326                .map(|k| {
327                    resolve_column(row, k)
328                        .map(|v| v.to_display_string())
329                        .unwrap_or_default()
330                })
331                .collect();
332            let key_vals: Vec<Value> = group_keys
333                .iter()
334                .map(|k| resolve_column(row, k).unwrap_or(Value::Null))
335                .collect();
336            if let Some(&idx) = key_index.get(&key) {
337                groups[idx].1.push(row);
338            } else {
339                let idx = groups.len();
340                key_index.insert(key, idx);
341                groups.push((key_vals, vec![row]));
342            }
343        }
344    }
345
346    // Compute aggregates per group
347    let mut result = Vec::new();
348    for (key_vals, group_rows) in &groups {
349        let mut out = Row::new();
350
351        // Fill in group key values
352        for (i, k) in group_keys.iter().enumerate() {
353            out.insert(k.clone(), key_vals[i].clone());
354        }
355
356        // Compute each expression
357        for expr in exprs {
358            match expr {
359                SelectExpr::Column(name) => {
360                    // Already filled if it's a group key; otherwise take first row's
361                    // value, resolving dotted dict references (e.g. `params.key`).
362                    if !out.contains_key(name) {
363                        if let Some(first) = group_rows.first() {
364                            out.insert(
365                                name.clone(),
366                                resolve_column(first, name).unwrap_or(Value::Null),
367                            );
368                        }
369                    }
370                }
371                SelectExpr::Aggregate { func, arg, arg_expr, alias } => {
372                    let out_name = alias
373                        .clone()
374                        .unwrap_or_else(|| expr.output_name());
375                    let val = compute_aggregate(func, arg, arg_expr.as_ref(), group_rows);
376                    out.insert(out_name, val);
377                }
378                SelectExpr::Expr { expr: e, alias } => {
379                    let out_name = alias.clone().unwrap_or_else(|| e.display_name());
380                    if e.contains_aggregate() {
381                        let val = evaluate_agg_expr(e, group_rows);
382                        out.insert(out_name, val);
383                    } else if let Some(first) = group_rows.first() {
384                        let val = evaluate_expr(e, first);
385                        out.insert(out_name, val);
386                    }
387                }
388            }
389        }
390
391        result.push(out);
392    }
393
394    Ok(result)
395}
396
397/// Resolve a per-row value for an aggregate argument.
398/// If `arg_expr` is set, evaluate it; otherwise look up `arg` as a column name.
399fn resolve_agg_value<'a>(arg: &str, arg_expr: Option<&Expr>, row: &'a Row) -> Value {
400    if let Some(expr) = arg_expr {
401        evaluate_expr(expr, row)
402    } else {
403        row.get(arg).cloned().unwrap_or(Value::Null)
404    }
405}
406
407fn compute_aggregate(func: &AggFunc, arg: &str, arg_expr: Option<&Expr>, rows: &[&Row]) -> Value {
408    match func {
409        AggFunc::Count => {
410            if arg == "*" && arg_expr.is_none() {
411                Value::Int(rows.len() as i64)
412            } else {
413                let count = rows
414                    .iter()
415                    .filter(|r| {
416                        let v = resolve_agg_value(arg, arg_expr, r);
417                        !v.is_null()
418                    })
419                    .count();
420                Value::Int(count as i64)
421            }
422        }
423        AggFunc::Sum => {
424            let mut total = 0.0f64;
425            let mut has_any = false;
426            for r in rows {
427                let v = resolve_agg_value(arg, arg_expr, r);
428                match v {
429                    Value::Int(n) => { total += n as f64; has_any = true; }
430                    Value::Float(f) => { total += f; has_any = true; }
431                    _ => {}
432                }
433            }
434            if has_any { Value::Float(total) } else { Value::Null }
435        }
436        AggFunc::Avg => {
437            let mut total = 0.0f64;
438            let mut count = 0usize;
439            for r in rows {
440                let v = resolve_agg_value(arg, arg_expr, r);
441                match v {
442                    Value::Int(n) => { total += n as f64; count += 1; }
443                    Value::Float(f) => { total += f; count += 1; }
444                    _ => {}
445                }
446            }
447            if count > 0 { Value::Float(total / count as f64) } else { Value::Null }
448        }
449        AggFunc::Min => {
450            let mut min_val: Option<Value> = None;
451            for r in rows {
452                let v = resolve_agg_value(arg, arg_expr, r);
453                if v.is_null() { continue; }
454                min_val = Some(match min_val {
455                    None => v,
456                    Some(ref current) => {
457                        if v.partial_cmp(current) == Some(std::cmp::Ordering::Less) {
458                            v
459                        } else {
460                            current.clone()
461                        }
462                    }
463                });
464            }
465            min_val.unwrap_or(Value::Null)
466        }
467        AggFunc::Max => {
468            let mut max_val: Option<Value> = None;
469            for r in rows {
470                let v = resolve_agg_value(arg, arg_expr, r);
471                if v.is_null() { continue; }
472                max_val = Some(match max_val {
473                    None => v,
474                    Some(ref current) => {
475                        if v.partial_cmp(current) == Some(std::cmp::Ordering::Greater) {
476                            v
477                        } else {
478                            current.clone()
479                        }
480                    }
481                });
482            }
483            max_val.unwrap_or(Value::Null)
484        }
485    }
486}
487
488fn compute_windows(rows: &mut Vec<Row>, select_exprs: &[SelectExpr]) -> crate::errors::Result<()> {
489    for se in select_exprs {
490        if let SelectExpr::Expr { expr, alias } = se {
491            if let Expr::Window { func, args, over } = expr {
492                let col_name = alias.clone().unwrap_or_else(|| expr.display_name());
493                compute_single_window(rows, func, args, over, &col_name)?;
494            }
495        }
496    }
497    Ok(())
498}
499
500fn compute_single_window(
501    rows: &mut Vec<Row>,
502    func: &WindowFunc,
503    args: &[Expr],
504    over: &WindowSpec,
505    col_name: &str,
506) -> crate::errors::Result<()> {
507    let mut partitions: Vec<Vec<usize>> = Vec::new();
508    let mut partition_map: HashMap<Vec<String>, usize> = HashMap::new();
509
510    for (i, row) in rows.iter().enumerate() {
511        let key: Vec<String> = over.partition_by.iter()
512            .map(|col| row.get(col).map(|v| v.to_display_string()).unwrap_or_default())
513            .collect();
514        if let Some(&idx) = partition_map.get(&key) {
515            partitions[idx].push(i);
516        } else {
517            let idx = partitions.len();
518            partition_map.insert(key, idx);
519            partitions.push(vec![i]);
520        }
521    }
522
523    for partition in &mut partitions {
524        if !over.order_by.is_empty() {
525            partition.sort_by(|&a, &b| {
526                for spec in &over.order_by {
527                    let (va, vb) = if let Some(ref expr) = spec.expr {
528                        (evaluate_expr(expr, &rows[a]), evaluate_expr(expr, &rows[b]))
529                    } else {
530                        (
531                            rows[a].get(&spec.column).cloned().unwrap_or(Value::Null),
532                            rows[b].get(&spec.column).cloned().unwrap_or(Value::Null),
533                        )
534                    };
535                    let ordering = match (&va, &vb) {
536                        (Value::Null, Value::Null) => Ordering::Equal,
537                        (Value::Null, _) => Ordering::Greater,
538                        (_, Value::Null) => Ordering::Less,
539                        (a_val, b_val) => compare_model_values(a_val, b_val).unwrap_or(Ordering::Equal),
540                    };
541                    let ordering = if spec.descending { ordering.reverse() } else { ordering };
542                    if ordering != Ordering::Equal {
543                        return ordering;
544                    }
545                }
546                Ordering::Equal
547            });
548        }
549    }
550
551    let mut values: Vec<(usize, Value)> = Vec::new();
552
553    for partition in &partitions {
554        match func {
555            WindowFunc::RowNumber => {
556                for (i, &row_idx) in partition.iter().enumerate() {
557                    values.push((row_idx, Value::Int((i + 1) as i64)));
558                }
559            }
560            WindowFunc::Rank => {
561                let mut rank = 1usize;
562                for (i, &row_idx) in partition.iter().enumerate() {
563                    if i > 0 {
564                        let prev_idx = partition[i - 1];
565                        let same = over.order_by.iter().all(|spec| {
566                            let va = if let Some(ref expr) = spec.expr {
567                                evaluate_expr(expr, &rows[prev_idx])
568                            } else {
569                                rows[prev_idx].get(&spec.column).cloned().unwrap_or(Value::Null)
570                            };
571                            let vb = if let Some(ref expr) = spec.expr {
572                                evaluate_expr(expr, &rows[row_idx])
573                            } else {
574                                rows[row_idx].get(&spec.column).cloned().unwrap_or(Value::Null)
575                            };
576                            va == vb
577                        });
578                        if !same {
579                            rank = i + 1;
580                        }
581                    }
582                    values.push((row_idx, Value::Int(rank as i64)));
583                }
584            }
585            WindowFunc::DenseRank => {
586                let mut rank = 1usize;
587                for (i, &row_idx) in partition.iter().enumerate() {
588                    if i > 0 {
589                        let prev_idx = partition[i - 1];
590                        let same = over.order_by.iter().all(|spec| {
591                            let va = if let Some(ref expr) = spec.expr {
592                                evaluate_expr(expr, &rows[prev_idx])
593                            } else {
594                                rows[prev_idx].get(&spec.column).cloned().unwrap_or(Value::Null)
595                            };
596                            let vb = if let Some(ref expr) = spec.expr {
597                                evaluate_expr(expr, &rows[row_idx])
598                            } else {
599                                rows[row_idx].get(&spec.column).cloned().unwrap_or(Value::Null)
600                            };
601                            va == vb
602                        });
603                        if !same {
604                            rank += 1;
605                        }
606                    }
607                    values.push((row_idx, Value::Int(rank as i64)));
608                }
609            }
610            WindowFunc::Lag => {
611                let offset = if args.len() > 1 {
612                    if let Expr::Literal(SqlValue::Int(n)) = &args[1] { *n as usize } else { 1 }
613                } else {
614                    1
615                };
616                for (i, &row_idx) in partition.iter().enumerate() {
617                    let val = if i >= offset && !args.is_empty() {
618                        evaluate_expr(&args[0], &rows[partition[i - offset]])
619                    } else {
620                        Value::Null
621                    };
622                    values.push((row_idx, val));
623                }
624            }
625            WindowFunc::Lead => {
626                let offset = if args.len() > 1 {
627                    if let Expr::Literal(SqlValue::Int(n)) = &args[1] { *n as usize } else { 1 }
628                } else {
629                    1
630                };
631                for (i, &row_idx) in partition.iter().enumerate() {
632                    let val = if i + offset < partition.len() && !args.is_empty() {
633                        evaluate_expr(&args[0], &rows[partition[i + offset]])
634                    } else {
635                        Value::Null
636                    };
637                    values.push((row_idx, val));
638                }
639            }
640            WindowFunc::Agg(agg_func) => {
641                let partition_rows: Vec<&Row> = partition.iter().map(|&i| &rows[i]).collect();
642                let (arg_name, arg_expr_opt) = if args.is_empty() {
643                    ("*".to_string(), None)
644                } else {
645                    (args[0].display_name(), Some(&args[0]))
646                };
647                let agg_val = compute_aggregate(agg_func, &arg_name, arg_expr_opt, &partition_rows);
648                for &row_idx in partition {
649                    values.push((row_idx, agg_val.clone()));
650                }
651            }
652        }
653    }
654
655    for (row_idx, val) in values {
656        rows[row_idx].insert(col_name.to_string(), val);
657    }
658
659    Ok(())
660}
661
662fn evaluate_with_fts(clause: &WhereClause, row: &Row, fts: &FtsResults) -> bool {
663    match clause {
664        WhereClause::BoolOp(bop) => {
665            let left = evaluate_with_fts(&bop.left, row, fts);
666            match bop.op {
667                BoolOpKind::And => left && evaluate_with_fts(&bop.right, row, fts),
668                BoolOpKind::Or => left || evaluate_with_fts(&bop.right, row, fts),
669            }
670        }
671        WhereClause::Comparison(cmp) => {
672            // Check if we have FTS results for this comparison
673            if cmp.op == CmpOp::Like || cmp.op == CmpOp::NotLike {
674                if let Some(SqlValue::String(pattern)) = &cmp.value {
675                    let key = (cmp.column.clone(), pattern.clone());
676                    if let Some(matching_paths) = fts.get(&key) {
677                        let row_path = row.get("path").and_then(|v| v.as_str()).unwrap_or("");
678                        let matched = matching_paths.contains(row_path);
679                        return if cmp.op == CmpOp::Like { matched } else { !matched };
680                    }
681                }
682            }
683            evaluate_comparison(cmp, row)
684        }
685    }
686}
687
688pub use crate::query_join::execute_join_query;
689
690pub(crate) fn execute_inner(
691    query: &SelectQuery,
692    rows: &[Row],
693    index: Option<&crate::index::TableIndex>,
694) -> crate::errors::Result<(Vec<Row>, Vec<String>)> {
695    let empty_fts = HashMap::new();
696    execute_with_fts(query, rows, index, &empty_fts)
697}
698
699pub fn evaluate(clause: &WhereClause, row: &Row) -> bool {
700    match clause {
701        WhereClause::BoolOp(bop) => {
702            let left = evaluate(&bop.left, row);
703            match bop.op {
704                BoolOpKind::And => left && evaluate(&bop.right, row),
705                BoolOpKind::Or => left || evaluate(&bop.right, row),
706            }
707        }
708        WhereClause::Comparison(cmp) => evaluate_comparison(cmp, row),
709    }
710}
711
712/// Resolve a (possibly dotted) column reference against a row, descending into
713/// `Value::Dict` for paths like `params.key` or `b.params.key`. Returns `None`
714/// when neither a flat key nor any dict path matches, so callers can null-fill.
715///
716/// Single source of truth for column resolution: projection, DISTINCT, GROUP BY,
717/// and `evaluate_expr` all route through it so `SELECT a.dict.key` and
718/// `WHERE a.dict.key = ...` resolve identically.
719pub(crate) fn resolve_column(row: &Row, name: &str) -> Option<Value> {
720    if let Some(val) = row.get(name) {
721        return Some(val.clone());
722    }
723    // Try each dot split left-to-right (e.g. "b.params.key" → dict "b.params").
724    for (i, _) in name.match_indices('.') {
725        if let Some(Value::Dict(map)) = row.get(&name[..i]) {
726            return map.get(&name[i + 1..]).cloned();
727        }
728    }
729    None
730}
731
732/// Evaluate an Expr against a row, returning a Value.
733pub(crate) fn evaluate_expr(expr: &Expr, row: &Row) -> Value {
734    match expr {
735        Expr::Literal(SqlValue::Int(n)) => Value::Int(*n),
736        Expr::Literal(SqlValue::Float(f)) => Value::Float(*f),
737        Expr::Literal(SqlValue::String(s)) => Value::String(s.clone()),
738        Expr::Literal(SqlValue::Bool(b)) => Value::Bool(*b),
739        Expr::Literal(SqlValue::Null) => Value::Null,
740        Expr::Literal(SqlValue::List(_)) => Value::Null,
741        Expr::Column(name) => resolve_column(row, name).unwrap_or(Value::Null),
742        Expr::UnaryMinus(inner) => {
743            match evaluate_expr(inner, row) {
744                Value::Int(n) => Value::Int(-n),
745                Value::Float(f) => Value::Float(-f),
746                Value::Null => Value::Null,
747                _ => Value::Null, // non-numeric → NULL
748            }
749        }
750        Expr::BinaryOp { left, op, right } => {
751            let lv = evaluate_expr(left, row);
752            let rv = evaluate_expr(right, row);
753
754            // NULL propagation: any NULL operand → NULL
755            if lv.is_null() || rv.is_null() {
756                return Value::Null;
757            }
758
759            // Extract numeric values with int→float coercion
760            match (&lv, &rv) {
761                (Value::Int(a), Value::Int(b)) => {
762                    match op {
763                        ArithOp::Add => Value::Int(a.wrapping_add(*b)),
764                        ArithOp::Sub => Value::Int(a.wrapping_sub(*b)),
765                        ArithOp::Mul => Value::Int(a.wrapping_mul(*b)),
766                        ArithOp::Div => {
767                            if *b == 0 { Value::Null } else { Value::Int(a / b) }
768                        }
769                        ArithOp::Mod => {
770                            if *b == 0 { Value::Null } else { Value::Int(a % b) }
771                        }
772                    }
773                }
774                _ => {
775                    // Coerce to float
776                    let a = match &lv {
777                        Value::Int(n) => *n as f64,
778                        Value::Float(f) => *f,
779                        _ => return Value::Null,
780                    };
781                    let b = match &rv {
782                        Value::Int(n) => *n as f64,
783                        Value::Float(f) => *f,
784                        _ => return Value::Null,
785                    };
786                    match op {
787                        ArithOp::Add => Value::Float(a + b),
788                        ArithOp::Sub => Value::Float(a - b),
789                        ArithOp::Mul => Value::Float(a * b),
790                        ArithOp::Div => {
791                            if b == 0.0 { Value::Null } else { Value::Float(a / b) }
792                        }
793                        ArithOp::Mod => {
794                            if b == 0.0 { Value::Null } else { Value::Float(a % b) }
795                        }
796                    }
797                }
798            }
799        }
800        Expr::Case { whens, else_expr } => {
801            for (condition, result) in whens {
802                if evaluate(condition, row) {
803                    return evaluate_expr(result, row);
804                }
805            }
806            match else_expr {
807                Some(e) => evaluate_expr(e, row),
808                None => Value::Null,
809            }
810        }
811        Expr::CurrentDate => {
812            Value::Date(chrono::Local::now().naive_local().date())
813        }
814        Expr::CurrentTimestamp => {
815            Value::DateTime(chrono::Local::now().naive_local())
816        }
817        Expr::DateAdd { date, days } => {
818            let date_val = evaluate_expr(date, row);
819            let days_val = evaluate_expr(days, row);
820            let n = match &days_val {
821                Value::Int(n) => *n,
822                Value::Float(f) => *f as i64,
823                _ => return Value::Null,
824            };
825            let duration = chrono::Duration::days(n);
826            match date_val {
827                Value::Date(d) => {
828                    match d.checked_add_signed(duration) {
829                        Some(result) => Value::Date(result),
830                        None => Value::Null,
831                    }
832                }
833                Value::DateTime(dt) => {
834                    match dt.checked_add_signed(duration) {
835                        Some(result) => Value::DateTime(result),
836                        None => Value::Null,
837                    }
838                }
839                _ => Value::Null,
840            }
841        }
842        Expr::DateDiff { left, right } => {
843            let lv = evaluate_expr(left, row);
844            let rv = evaluate_expr(right, row);
845            let left_date = match &lv {
846                Value::Date(d) => d.and_hms_opt(0, 0, 0).unwrap(),
847                Value::DateTime(dt) => *dt,
848                _ => return Value::Null,
849            };
850            let right_date = match &rv {
851                Value::Date(d) => d.and_hms_opt(0, 0, 0).unwrap(),
852                Value::DateTime(dt) => *dt,
853                _ => return Value::Null,
854            };
855            Value::Int((left_date - right_date).num_days())
856        }
857        Expr::Aggregate { func, arg, .. } => {
858            // Post-aggregation: look up the pre-computed column name
859            let func_name = match func {
860                AggFunc::Count => "COUNT",
861                AggFunc::Sum => "SUM",
862                AggFunc::Avg => "AVG",
863                AggFunc::Min => "MIN",
864                AggFunc::Max => "MAX",
865            };
866            let col = format!("{}({})", func_name, arg);
867            row.get(&col).cloned().unwrap_or(Value::Null)
868        }
869        Expr::Subquery(_) => Value::Null,
870        Expr::Window { .. } => {
871            let display = expr.display_name();
872            row.get(&display).cloned().unwrap_or(Value::Null)
873        }
874    }
875}
876
877fn evaluate_agg_expr(expr: &Expr, group_rows: &[&Row]) -> Value {
878    match expr {
879        Expr::Aggregate { func, arg, arg_expr } => {
880            compute_aggregate(func, arg, arg_expr.as_deref(), group_rows)
881        }
882        Expr::BinaryOp { left, op, right } => {
883            let lv = evaluate_agg_expr(left, group_rows);
884            let rv = evaluate_agg_expr(right, group_rows);
885            apply_arith_op(op, &lv, &rv)
886        }
887        Expr::UnaryMinus(inner) => {
888            match evaluate_agg_expr(inner, group_rows) {
889                Value::Int(n) => Value::Int(-n),
890                Value::Float(f) => Value::Float(-f),
891                _ => Value::Null,
892            }
893        }
894        other => {
895            if let Some(first) = group_rows.first() {
896                evaluate_expr(other, first)
897            } else {
898                Value::Null
899            }
900        }
901    }
902}
903
904fn apply_arith_op(op: &ArithOp, lv: &Value, rv: &Value) -> Value {
905    if lv.is_null() || rv.is_null() {
906        return Value::Null;
907    }
908    match (lv, rv) {
909        (Value::Int(a), Value::Int(b)) => match op {
910            ArithOp::Add => Value::Int(a.wrapping_add(*b)),
911            ArithOp::Sub => Value::Int(a.wrapping_sub(*b)),
912            ArithOp::Mul => Value::Int(a.wrapping_mul(*b)),
913            ArithOp::Div => if *b == 0 { Value::Null } else { Value::Int(a / b) },
914            ArithOp::Mod => if *b == 0 { Value::Null } else { Value::Int(a % b) },
915        },
916        _ => {
917            let a = match lv {
918                Value::Int(n) => *n as f64,
919                Value::Float(f) => *f,
920                _ => return Value::Null,
921            };
922            let b = match rv {
923                Value::Int(n) => *n as f64,
924                Value::Float(f) => *f,
925                _ => return Value::Null,
926            };
927            match op {
928                ArithOp::Add => Value::Float(a + b),
929                ArithOp::Sub => Value::Float(a - b),
930                ArithOp::Mul => Value::Float(a * b),
931                ArithOp::Div => if b == 0.0 { Value::Null } else { Value::Float(a / b) },
932                ArithOp::Mod => if b == 0.0 { Value::Null } else { Value::Float(a % b) },
933            }
934        }
935    }
936}
937
938fn evaluate_comparison(cmp: &Comparison, row: &Row) -> bool {
939    // If we have expression-based comparison (new path), use it for standard ops
940    if let (Some(left_expr), Some(right_expr)) = (&cmp.left_expr, &cmp.right_expr) {
941        if matches!(cmp.op, CmpOp::Eq | CmpOp::Ne | CmpOp::Lt | CmpOp::Gt | CmpOp::Le | CmpOp::Ge) {
942            let left_val = evaluate_expr(left_expr, row);
943            let right_val = evaluate_expr(right_expr, row);
944
945            // NULL comparison: always false (except IS NULL handled below)
946            if left_val.is_null() || right_val.is_null() {
947                return false;
948            }
949
950            // Coerce for comparison: if types differ, try int→float
951            let ord = compare_model_values(&left_val, &right_val);
952
953            return match cmp.op {
954                CmpOp::Eq => ord == Some(Ordering::Equal),
955                CmpOp::Ne => ord != Some(Ordering::Equal),
956                CmpOp::Lt => ord == Some(Ordering::Less),
957                CmpOp::Gt => ord == Some(Ordering::Greater),
958                CmpOp::Le => matches!(ord, Some(Ordering::Less | Ordering::Equal)),
959                CmpOp::Ge => matches!(ord, Some(Ordering::Greater | Ordering::Equal)),
960                _ => false,
961            };
962        }
963    }
964
965    // Fall back to legacy column-based comparison for IS NULL, IN, LIKE, etc.
966    // Resolve through resolve_column so dotted dict refs (e.g. `params.key`)
967    // work here too, matching the expression path above.
968    let actual_owned = resolve_column(row, &cmp.column);
969    let actual = actual_owned.as_ref();
970
971    if cmp.op == CmpOp::IsNull {
972        return actual.map_or(true, |v| v.is_null());
973    }
974    if cmp.op == CmpOp::IsNotNull {
975        return actual.map_or(false, |v| !v.is_null());
976    }
977
978    let actual = match actual {
979        Some(v) if !v.is_null() => v,
980        _ => return false,
981    };
982
983    let expected = match &cmp.value {
984        Some(v) => v,
985        None => return false,
986    };
987
988    match cmp.op {
989        CmpOp::Eq => eq_match(actual, expected),
990        CmpOp::Ne => !eq_match(actual, expected),
991        CmpOp::Lt => compare_values(actual, expected) == Some(Ordering::Less),
992        CmpOp::Gt => compare_values(actual, expected) == Some(Ordering::Greater),
993        CmpOp::Le => matches!(compare_values(actual, expected), Some(Ordering::Less | Ordering::Equal)),
994        CmpOp::Ge => matches!(compare_values(actual, expected), Some(Ordering::Greater | Ordering::Equal)),
995        CmpOp::Like => like_match(actual, expected),
996        CmpOp::NotLike => !like_match(actual, expected),
997        CmpOp::In => {
998            if let SqlValue::List(items) = expected {
999                items.iter().any(|v| eq_match(actual, v))
1000            } else {
1001                eq_match(actual, expected)
1002            }
1003        }
1004        CmpOp::IsNull | CmpOp::IsNotNull => unreachable!(),
1005    }
1006}
1007
1008/// Compare two model::Value instances, with int↔float coercion.
1009fn compare_model_values(a: &Value, b: &Value) -> Option<Ordering> {
1010    match (a, b) {
1011        (Value::Int(x), Value::Float(y)) => (*x as f64).partial_cmp(y),
1012        (Value::Float(x), Value::Int(y)) => x.partial_cmp(&(*y as f64)),
1013        _ => a.partial_cmp(b),
1014    }
1015}
1016
1017fn coerce_sql_to_value(sql_val: &SqlValue, target: &Value) -> Value {
1018    match sql_val {
1019        SqlValue::Null => Value::Null,
1020        SqlValue::String(s) => {
1021            match target {
1022                Value::Int(_) => s.parse::<i64>().map(Value::Int).unwrap_or(Value::String(s.clone())),
1023                Value::Float(_) => s.parse::<f64>().map(Value::Float).unwrap_or(Value::String(s.clone())),
1024                Value::Date(_) => {
1025                    chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
1026                        .map(Value::Date)
1027                        .unwrap_or(Value::String(s.clone()))
1028                }
1029                Value::DateTime(_) => {
1030                    chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
1031                        .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f"))
1032                        .map(Value::DateTime)
1033                        .unwrap_or(Value::String(s.clone()))
1034                }
1035                _ => Value::String(s.clone()),
1036            }
1037        }
1038        SqlValue::Int(n) => {
1039            match target {
1040                Value::Float(_) => Value::Float(*n as f64),
1041                _ => Value::Int(*n),
1042            }
1043        }
1044        SqlValue::Float(f) => Value::Float(*f),
1045        SqlValue::Bool(b) => Value::Bool(*b),
1046        SqlValue::List(_) => Value::Null, // Lists handled separately
1047    }
1048}
1049
1050fn eq_match(actual: &Value, expected: &SqlValue) -> bool {
1051    // Special handling for lists (e.g., categories)
1052    if let Value::List(items) = actual {
1053        if let SqlValue::String(s) = expected {
1054            return items.contains(s);
1055        }
1056    }
1057
1058    let coerced = coerce_sql_to_value(expected, actual);
1059    actual == &coerced
1060}
1061
1062fn like_match(actual: &Value, pattern: &SqlValue) -> bool {
1063    let pattern_str = match pattern {
1064        SqlValue::String(s) => s,
1065        _ => return false,
1066    };
1067
1068    // Convert SQL LIKE to regex
1069    let mut regex_str = String::from("(?is)^");
1070    for ch in pattern_str.chars() {
1071        match ch {
1072            '%' => regex_str.push_str(".*"),
1073            '_' => regex_str.push('.'),
1074            c => {
1075                if regex::escape(&c.to_string()) != c.to_string() {
1076                    regex_str.push_str(&regex::escape(&c.to_string()));
1077                } else {
1078                    regex_str.push(c);
1079                }
1080            }
1081        }
1082    }
1083    regex_str.push('$');
1084
1085    let re = match Regex::new(&regex_str) {
1086        Ok(r) => r,
1087        Err(_) => return false,
1088    };
1089
1090    match actual {
1091        Value::List(items) => items.iter().any(|item| re.is_match(item)),
1092        _ => re.is_match(&actual.to_display_string()),
1093    }
1094}
1095
1096fn compare_values(actual: &Value, expected: &SqlValue) -> Option<Ordering> {
1097    let coerced = coerce_sql_to_value(expected, actual);
1098    actual.partial_cmp(&coerced)
1099}
1100
1101/// Convert a SqlValue to a Value for index lookups (without a target type for coercion).
1102fn sql_value_to_index_value(sv: &SqlValue) -> Value {
1103    match sv {
1104        SqlValue::String(s) => {
1105            // Try datetime first (more specific)
1106            if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
1107                return Value::DateTime(dt);
1108            }
1109            if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
1110                return Value::DateTime(dt);
1111            }
1112            // Try date
1113            if let Ok(d) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
1114                return Value::Date(d);
1115            }
1116            Value::String(s.clone())
1117        }
1118        SqlValue::Int(n) => Value::Int(*n),
1119        SqlValue::Float(f) => Value::Float(*f),
1120        SqlValue::Bool(b) => Value::Bool(*b),
1121        SqlValue::Null => Value::Null,
1122        SqlValue::List(_) => Value::Null,
1123    }
1124}
1125
1126/// Try to use B-tree indexes to narrow the candidate row set.
1127/// Returns Some(paths) if the entire WHERE clause could be resolved via index,
1128/// or None if a full scan is needed.
1129fn try_index_filter(
1130    clause: &WhereClause,
1131    index: &crate::index::TableIndex,
1132) -> Option<std::collections::HashSet<String>> {
1133    match clause {
1134        WhereClause::Comparison(cmp) => {
1135            if !index.has_index(&cmp.column) {
1136                return None;
1137            }
1138            match cmp.op {
1139                CmpOp::Eq => {
1140                    let val = sql_value_to_index_value(cmp.value.as_ref()?);
1141                    let paths = index.lookup_eq(&cmp.column, &val);
1142                    Some(paths.into_iter().map(|s| s.to_string()).collect())
1143                }
1144                CmpOp::Lt => {
1145                    let val = sql_value_to_index_value(cmp.value.as_ref()?);
1146                    // exclusive upper bound: use range with max < val
1147                    // lookup_range is inclusive, so we get all <= val then remove exact matches
1148                    let range_paths = index.lookup_range(&cmp.column, None, Some(&val));
1149                    let eq_paths: std::collections::HashSet<&str> = index.lookup_eq(&cmp.column, &val).into_iter().collect();
1150                    Some(range_paths.into_iter().filter(|p| !eq_paths.contains(p)).map(|s| s.to_string()).collect())
1151                }
1152                CmpOp::Gt => {
1153                    let val = sql_value_to_index_value(cmp.value.as_ref()?);
1154                    let range_paths = index.lookup_range(&cmp.column, Some(&val), None);
1155                    let eq_paths: std::collections::HashSet<&str> = index.lookup_eq(&cmp.column, &val).into_iter().collect();
1156                    Some(range_paths.into_iter().filter(|p| !eq_paths.contains(p)).map(|s| s.to_string()).collect())
1157                }
1158                CmpOp::Le => {
1159                    let val = sql_value_to_index_value(cmp.value.as_ref()?);
1160                    let paths = index.lookup_range(&cmp.column, None, Some(&val));
1161                    Some(paths.into_iter().map(|s| s.to_string()).collect())
1162                }
1163                CmpOp::Ge => {
1164                    let val = sql_value_to_index_value(cmp.value.as_ref()?);
1165                    let paths = index.lookup_range(&cmp.column, Some(&val), None);
1166                    Some(paths.into_iter().map(|s| s.to_string()).collect())
1167                }
1168                CmpOp::In => {
1169                    if let Some(SqlValue::List(items)) = &cmp.value {
1170                        let vals: Vec<Value> = items.iter().map(sql_value_to_index_value).collect();
1171                        let paths = index.lookup_in(&cmp.column, &vals);
1172                        Some(paths.into_iter().map(|s| s.to_string()).collect())
1173                    } else {
1174                        None
1175                    }
1176                }
1177                _ => None, // LIKE, IS NULL, etc. can't use index
1178            }
1179        }
1180        WhereClause::BoolOp(bop) => {
1181            let left = try_index_filter(&bop.left, index);
1182            let right = try_index_filter(&bop.right, index);
1183            match bop.op {
1184                BoolOpKind::And => {
1185                    match (left, right) {
1186                        (Some(l), Some(r)) => Some(l.intersection(&r).cloned().collect()),
1187                        (Some(l), None) => Some(l), // narrow with left, scan-verify right
1188                        (None, Some(r)) => Some(r),
1189                        (None, None) => None,
1190                    }
1191                }
1192                BoolOpKind::Or => {
1193                    match (left, right) {
1194                        (Some(l), Some(r)) => Some(l.union(&r).cloned().collect()),
1195                        _ => None, // Can't use index if either side needs full scan
1196                    }
1197                }
1198            }
1199        }
1200    }
1201}
1202
1203/// If an ORDER BY column matches a SELECT alias, replace its expr with the
1204/// aliased expression so sorting uses the computed value.
1205fn resolve_order_aliases(specs: &[OrderSpec], columns: &ColumnList) -> Vec<OrderSpec> {
1206    let named = match columns {
1207        ColumnList::Named(exprs) => exprs,
1208        _ => return specs.to_vec(),
1209    };
1210
1211    // Build alias → expr map (skip window exprs — their values are already in rows)
1212    let alias_map: HashMap<String, &Expr> = named
1213        .iter()
1214        .filter_map(|se| match se {
1215            SelectExpr::Expr { expr, alias: Some(a) } if !expr.contains_window() => {
1216                Some((a.clone(), expr))
1217            }
1218            _ => None,
1219        })
1220        .collect();
1221
1222    specs
1223        .iter()
1224        .map(|spec| {
1225            // If the ORDER BY column name matches a SELECT alias, use that expression
1226            if let Some(expr) = alias_map.get(&spec.column) {
1227                OrderSpec {
1228                    column: spec.column.clone(),
1229                    expr: Some((*expr).clone()),
1230                    descending: spec.descending,
1231                }
1232            } else {
1233                spec.clone()
1234            }
1235        })
1236        .collect()
1237}
1238
1239fn sort_rows(rows: &mut Vec<Row>, specs: &[OrderSpec]) {
1240    rows.sort_by(|a, b| {
1241        for spec in specs {
1242            let (va, vb) = if let Some(ref expr) = spec.expr {
1243                (evaluate_expr(expr, a), evaluate_expr(expr, b))
1244            } else {
1245                (
1246                    a.get(&spec.column).cloned().unwrap_or(Value::Null),
1247                    b.get(&spec.column).cloned().unwrap_or(Value::Null),
1248                )
1249            };
1250
1251            // NULLs sort last
1252            let ordering = match (&va, &vb) {
1253                (Value::Null, Value::Null) => Ordering::Equal,
1254                (Value::Null, _) => Ordering::Greater,
1255                (_, Value::Null) => Ordering::Less,
1256                (a_val, b_val) => {
1257                    compare_model_values(a_val, b_val).unwrap_or(Ordering::Equal)
1258                }
1259            };
1260
1261            let ordering = if spec.descending {
1262                ordering.reverse()
1263            } else {
1264                ordering
1265            };
1266
1267            if ordering != Ordering::Equal {
1268                return ordering;
1269            }
1270        }
1271        Ordering::Equal
1272    });
1273}
1274
1275/// Convert a SqlValue to our model Value (for use in insert/update).
1276pub(crate) fn sql_value_to_value(sql_val: &SqlValue) -> Value {
1277    match sql_val {
1278        SqlValue::Null => Value::Null,
1279        SqlValue::String(s) => Value::String(s.clone()),
1280        SqlValue::Int(n) => Value::Int(*n),
1281        SqlValue::Float(f) => Value::Float(*f),
1282        SqlValue::Bool(b) => Value::Bool(*b),
1283        SqlValue::List(items) => {
1284            let strings: Vec<String> = items
1285                .iter()
1286                .filter_map(|v| match v {
1287                    SqlValue::String(s) => Some(s.clone()),
1288                    _ => None,
1289                })
1290                .collect();
1291            Value::List(strings)
1292        }
1293    }
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298    use super::*;
1299
1300    fn make_rows() -> Vec<Row> {
1301        vec![
1302            Row::from([
1303                ("path".into(), Value::String("a.md".into())),
1304                ("title".into(), Value::String("Alpha".into())),
1305                ("count".into(), Value::Int(10)),
1306            ]),
1307            Row::from([
1308                ("path".into(), Value::String("b.md".into())),
1309                ("title".into(), Value::String("Beta".into())),
1310                ("count".into(), Value::Int(5)),
1311            ]),
1312            Row::from([
1313                ("path".into(), Value::String("c.md".into())),
1314                ("title".into(), Value::String("Gamma".into())),
1315                ("count".into(), Value::Int(20)),
1316            ]),
1317        ]
1318    }
1319
1320    #[test]
1321    fn test_select_all() {
1322        let q = SelectQuery {
1323            distinct: false,
1324            columns: ColumnList::All,
1325            table: "test".into(),
1326            table_alias: None,
1327            subquery: None,
1328            joins: vec![],
1329            where_clause: None,
1330            group_by: None,
1331            having: None,
1332            order_by: None,
1333            limit: None,
1334            ctes: vec![],
1335        };
1336        let (rows, _cols) = execute_inner(&q, &make_rows(), None).unwrap();
1337        assert_eq!(rows.len(), 3);
1338    }
1339
1340    #[test]
1341    fn test_select_nonexistent_column_null_filled() {
1342        // SELECT naming a column absent from the table must keep header and
1343        // rows aligned: the unknown column appears in every row as Null.
1344        let q = parse_query("SELECT title, missing_col, count FROM test").unwrap();
1345        let q = match q {
1346            Statement::Select(s) => s,
1347            _ => panic!("expected SELECT"),
1348        };
1349        let (rows, cols) = execute_inner(&q, &make_rows(), None).unwrap();
1350        assert_eq!(cols, vec!["title", "missing_col", "count"]);
1351        assert_eq!(rows.len(), 3);
1352        for row in &rows {
1353            assert_eq!(row.len(), cols.len(), "row keys must match header length");
1354            for c in &cols {
1355                assert!(row.contains_key(c), "row missing header column {c}");
1356            }
1357            assert_eq!(row.get("missing_col"), Some(&Value::Null));
1358        }
1359    }
1360
1361    fn make_dict_rows() -> Vec<Row> {
1362        use indexmap::IndexMap;
1363        // Two rows whose `params` is a Dict — mirrors strategies.params etc.
1364        let mk = |path: &str, ev: &str, sl: i64| {
1365            Row::from([
1366                ("path".into(), Value::String(path.into())),
1367                ("params".into(), Value::Dict(IndexMap::from([
1368                    ("event_type".to_string(), Value::String(ev.into())),
1369                    ("sl_pct".to_string(), Value::Int(sl)),
1370                ]))),
1371            ])
1372        };
1373        vec![mk("a.md", "buyback", 5), mk("b.md", "unlock", 8), mk("c.md", "buyback", 3)]
1374    }
1375
1376    #[test]
1377    fn test_select_dotted_dict_key_projects_value() {
1378        // Regression: SELECT of a dict sub-key must resolve, not null out.
1379        // Previously WHERE resolved `params.x` but projection returned Null.
1380        let q = parse_query("SELECT path, params.event_type FROM test").unwrap();
1381        let q = match q { Statement::Select(s) => s, _ => panic!("expected SELECT") };
1382        let (rows, cols) = execute_inner(&q, &make_dict_rows(), None).unwrap();
1383        assert_eq!(cols, vec!["path", "params.event_type"]);
1384        assert_eq!(rows[0].get("params.event_type"), Some(&Value::String("buyback".into())));
1385        assert_eq!(rows[1].get("params.event_type"), Some(&Value::String("unlock".into())));
1386    }
1387
1388    #[test]
1389    fn test_dotted_dict_key_select_and_where_agree() {
1390        let q = parse_query(
1391            "SELECT path FROM test WHERE params.event_type = 'buyback'").unwrap();
1392        let q = match q { Statement::Select(s) => s, _ => panic!("expected SELECT") };
1393        let (rows, _) = execute_inner(&q, &make_dict_rows(), None).unwrap();
1394        let paths: Vec<_> = rows.iter().map(|r| r.get("path").cloned().unwrap()).collect();
1395        assert_eq!(paths, vec![Value::String("a.md".into()), Value::String("c.md".into())]);
1396    }
1397
1398    #[test]
1399    fn test_missing_dotted_dict_key_null_filled() {
1400        let q = parse_query("SELECT path, params.nope FROM test").unwrap();
1401        let q = match q { Statement::Select(s) => s, _ => panic!("expected SELECT") };
1402        let (rows, cols) = execute_inner(&q, &make_dict_rows(), None).unwrap();
1403        assert_eq!(cols, vec!["path", "params.nope"]);
1404        for row in &rows {
1405            assert_eq!(row.get("params.nope"), Some(&Value::Null));
1406        }
1407    }
1408
1409    #[test]
1410    fn test_dotted_dict_key_is_not_null() {
1411        let q = parse_query(
1412            "SELECT path FROM test WHERE params.event_type IS NOT NULL").unwrap();
1413        let q = match q { Statement::Select(s) => s, _ => panic!("expected SELECT") };
1414        let (rows, _) = execute_inner(&q, &make_dict_rows(), None).unwrap();
1415        assert_eq!(rows.len(), 3); // all three rows have params.event_type
1416    }
1417
1418    #[test]
1419    fn test_dotted_dict_key_in_and_like() {
1420        for (sql, expect) in [
1421            ("SELECT path FROM test WHERE params.event_type IN ('buyback','x')", 2),
1422            ("SELECT path FROM test WHERE params.event_type LIKE 'buy%'", 2),
1423            ("SELECT path FROM test WHERE params.event_type IS NULL", 0),
1424        ] {
1425            let q = parse_query(sql).unwrap();
1426            let q = match q { Statement::Select(s) => s, _ => panic!("expected SELECT") };
1427            let (rows, _) = execute_inner(&q, &make_dict_rows(), None).unwrap();
1428            assert_eq!(rows.len(), expect, "query: {sql}");
1429        }
1430    }
1431
1432    #[test]
1433    fn test_group_by_dotted_dict_key() {
1434        let q = parse_query(
1435            "SELECT params.event_type, COUNT(*) FROM test GROUP BY params.event_type").unwrap();
1436        let q = match q { Statement::Select(s) => s, _ => panic!("expected SELECT") };
1437        let (rows, _) = execute_inner(&q, &make_dict_rows(), None).unwrap();
1438        // buyback (2 rows) and unlock (1 row)
1439        assert_eq!(rows.len(), 2);
1440        let buyback = rows.iter().find(|r|
1441            r.get("params.event_type") == Some(&Value::String("buyback".into()))).unwrap();
1442        assert_eq!(buyback.get("COUNT(*)"), Some(&Value::Int(2)));
1443    }
1444
1445    #[test]
1446    fn test_select_duplicate_output_column_errors() {
1447        // A result row is keyed by output name; two columns with the same name
1448        // cannot both be represented, so the query must be rejected.
1449        let q = parse_query("SELECT title, title FROM test").unwrap();
1450        let q = match q {
1451            Statement::Select(s) => s,
1452            _ => panic!("expected SELECT"),
1453        };
1454        let err = execute_inner(&q, &make_rows(), None);
1455        assert!(err.is_err());
1456        let msg = err.unwrap_err().to_string();
1457        assert!(msg.contains("duplicate output column"), "got: {msg}");
1458    }
1459
1460    #[test]
1461    fn test_select_all_sparse_rows_aligned() {
1462        // SELECT * unions keys across rows; a row missing an optional field
1463        // must still carry every header column (as Null) so header/rows align.
1464        let rows = vec![
1465            Row::from([
1466                ("path".into(), Value::String("a.md".into())),
1467                ("title".into(), Value::String("Alpha".into())),
1468                ("kill_reason".into(), Value::String("no edge".into())),
1469            ]),
1470            Row::from([
1471                ("path".into(), Value::String("b.md".into())),
1472                ("title".into(), Value::String("Beta".into())),
1473            ]),
1474        ];
1475        let q = parse_query("SELECT * FROM test").unwrap();
1476        let q = match q {
1477            Statement::Select(s) => s,
1478            _ => panic!("expected SELECT"),
1479        };
1480        let (result, cols) = execute_inner(&q, &rows, None).unwrap();
1481        assert!(cols.contains(&"kill_reason".to_string()));
1482        for row in &result {
1483            assert_eq!(row.len(), cols.len(), "row keys must match header length");
1484            for c in &cols {
1485                assert!(row.contains_key(c), "row missing header column {c}");
1486            }
1487        }
1488        // The row that lacked kill_reason now carries it as Null.
1489        let beta = result.iter().find(|r| r.get("path") == Some(&Value::String("b.md".into()))).unwrap();
1490        assert_eq!(beta.get("kill_reason"), Some(&Value::Null));
1491    }
1492
1493    #[test]
1494    fn test_where_gt() {
1495        let q = SelectQuery {
1496            distinct: false,
1497            columns: ColumnList::All,
1498            table: "test".into(),
1499            table_alias: None,
1500            subquery: None,
1501            joins: vec![],
1502            where_clause: Some(WhereClause::Comparison(Comparison {
1503                column: "count".into(),
1504                op: CmpOp::Gt,
1505                value: Some(SqlValue::Int(5)),
1506                left_expr: Some(Expr::Column("count".into())),
1507                right_expr: Some(Expr::Literal(SqlValue::Int(5))),
1508            })),
1509            group_by: None,
1510            having: None,
1511            order_by: None,
1512            limit: None,
1513            ctes: vec![],
1514        };
1515        let (rows, _) = execute_inner(&q, &make_rows(), None).unwrap();
1516        assert_eq!(rows.len(), 2);
1517    }
1518
1519    #[test]
1520    fn test_order_by_desc() {
1521        let q = SelectQuery {
1522            distinct: false,
1523            columns: ColumnList::All,
1524            table: "test".into(),
1525            table_alias: None,
1526            subquery: None,
1527            joins: vec![],
1528            where_clause: None,
1529            group_by: None,
1530            having: None,
1531            order_by: Some(vec![OrderSpec {
1532                column: "count".into(),
1533                expr: Some(Expr::Column("count".into())),
1534                descending: true,
1535            }]),
1536            limit: None,
1537            ctes: vec![],
1538        };
1539        let (rows, _) = execute_inner(&q, &make_rows(), None).unwrap();
1540        assert_eq!(rows[0]["count"], Value::Int(20));
1541        assert_eq!(rows[2]["count"], Value::Int(5));
1542    }
1543
1544    #[test]
1545    fn test_limit() {
1546        let q = SelectQuery {
1547            distinct: false,
1548            columns: ColumnList::All,
1549            table: "test".into(),
1550            table_alias: None,
1551            subquery: None,
1552            joins: vec![],
1553            where_clause: None,
1554            group_by: None,
1555            having: None,
1556            order_by: None,
1557            limit: Some(2),
1558            ctes: vec![],
1559        };
1560        let (rows, _) = execute_inner(&q, &make_rows(), None).unwrap();
1561        assert_eq!(rows.len(), 2);
1562    }
1563
1564    #[test]
1565    fn test_like() {
1566        let q = SelectQuery {
1567            distinct: false,
1568            columns: ColumnList::All,
1569            table: "test".into(),
1570            table_alias: None,
1571            subquery: None,
1572            joins: vec![],
1573            where_clause: Some(WhereClause::Comparison(Comparison {
1574                column: "title".into(),
1575                op: CmpOp::Like,
1576                value: Some(SqlValue::String("%lph%".into())),
1577                left_expr: Some(Expr::Column("title".into())),
1578                right_expr: None,
1579            })),
1580            group_by: None,
1581            having: None,
1582            order_by: None,
1583            limit: None,
1584            ctes: vec![],
1585        };
1586        let (rows, _) = execute_inner(&q, &make_rows(), None).unwrap();
1587        assert_eq!(rows.len(), 1);
1588        assert_eq!(rows[0]["title"], Value::String("Alpha".into()));
1589    }
1590
1591    #[test]
1592    fn test_is_null() {
1593        let mut rows = make_rows();
1594        rows[1].insert("optional".into(), Value::Null);
1595
1596        let q = SelectQuery {
1597            distinct: false,
1598            columns: ColumnList::All,
1599            table: "test".into(),
1600            table_alias: None,
1601            subquery: None,
1602            joins: vec![],
1603            where_clause: Some(WhereClause::Comparison(Comparison {
1604                column: "optional".into(),
1605                op: CmpOp::IsNull,
1606                value: None,
1607                left_expr: Some(Expr::Column("optional".into())),
1608                right_expr: None,
1609            })),
1610            group_by: None,
1611            having: None,
1612            order_by: None,
1613            limit: None,
1614            ctes: vec![],
1615        };
1616        let (result, _) = execute_inner(&q, &rows, None).unwrap();
1617        // All rows where optional is NULL or missing
1618        assert_eq!(result.len(), 3);
1619    }
1620
1621    #[test]
1622    fn test_select_distinct_dedupes_and_projects() {
1623        // #61: DISTINCT returned every row with the column nulled.
1624        let rows = vec![
1625            Row::from([("path".into(), Value::String("1.md".into())), ("strategy".into(), Value::String("a".into()))]),
1626            Row::from([("path".into(), Value::String("2.md".into())), ("strategy".into(), Value::String("b".into()))]),
1627            Row::from([("path".into(), Value::String("3.md".into())), ("strategy".into(), Value::String("a".into()))]),
1628        ];
1629        let q = match parse_query("SELECT DISTINCT strategy FROM backtests").unwrap() {
1630            Statement::Select(s) => s,
1631            _ => panic!("expected SELECT"),
1632        };
1633        let (result, cols) = execute_inner(&q, &rows, None).unwrap();
1634        assert_eq!(cols, vec!["strategy"]);
1635        assert_eq!(result.len(), 2);
1636        let values: Vec<_> = result.iter().map(|r| r.get("strategy").unwrap().clone()).collect();
1637        assert_eq!(values, vec![Value::String("a".into()), Value::String("b".into())]);
1638    }
1639
1640    #[test]
1641    fn test_select_distinct_applies_before_limit() {
1642        // SQL semantics: dedupe first, then LIMIT.
1643        let rows = vec![
1644            Row::from([("path".into(), Value::String("1.md".into())), ("s".into(), Value::String("a".into()))]),
1645            Row::from([("path".into(), Value::String("2.md".into())), ("s".into(), Value::String("a".into()))]),
1646            Row::from([("path".into(), Value::String("3.md".into())), ("s".into(), Value::String("b".into()))]),
1647            Row::from([("path".into(), Value::String("4.md".into())), ("s".into(), Value::String("c".into()))]),
1648        ];
1649        let q = match parse_query("SELECT DISTINCT s FROM t ORDER BY s LIMIT 2").unwrap() {
1650            Statement::Select(s) => s,
1651            _ => panic!("expected SELECT"),
1652        };
1653        let (result, _) = execute_inner(&q, &rows, None).unwrap();
1654        // Naive limit-then-dedupe would give ["a"] only.
1655        assert_eq!(result.len(), 2);
1656        assert_eq!(result[0].get("s"), Some(&Value::String("a".into())));
1657        assert_eq!(result[1].get("s"), Some(&Value::String("b".into())));
1658    }
1659
1660    #[test]
1661    fn test_select_distinct_star() {
1662        let rows = vec![
1663            Row::from([("path".into(), Value::String("1.md".into())), ("s".into(), Value::String("a".into()))]),
1664            Row::from([("path".into(), Value::String("1.md".into())), ("s".into(), Value::String("a".into()))]),
1665            Row::from([("path".into(), Value::String("2.md".into())), ("s".into(), Value::String("b".into()))]),
1666        ];
1667        let q = match parse_query("SELECT DISTINCT * FROM t").unwrap() {
1668            Statement::Select(s) => s,
1669            _ => panic!("expected SELECT"),
1670        };
1671        let (result, _) = execute_inner(&q, &rows, None).unwrap();
1672        assert_eq!(result.len(), 2);
1673    }
1674
1675    #[test]
1676    fn test_where_boolean_literal_filters_rows() {
1677        // #60 end-to-end: `flag = true` must match Bool rows, not evaluate
1678        // against a nonexistent column named "true" and return nothing.
1679        let mut rows = make_rows();
1680        rows[0].insert("flag".into(), Value::Bool(true));
1681        rows[1].insert("flag".into(), Value::Bool(false));
1682        rows[2].insert("flag".into(), Value::Bool(true));
1683
1684        for (sql, expected) in [
1685            ("SELECT path FROM test WHERE flag = true", 2),
1686            ("SELECT path FROM test WHERE flag = FALSE", 1),
1687            ("SELECT path FROM test WHERE flag != true", 1),
1688        ] {
1689            let q = match parse_query(sql).unwrap() {
1690                Statement::Select(s) => s,
1691                _ => panic!("expected SELECT"),
1692            };
1693            let (result, _) = execute_inner(&q, &rows, None).unwrap();
1694            assert_eq!(result.len(), expected, "query: {sql}");
1695        }
1696    }
1697
1698    // ── Expression evaluation tests ─────────────────────────��─────
1699
1700    #[test]
1701    fn test_evaluate_expr_literal() {
1702        let row = Row::new();
1703        assert_eq!(evaluate_expr(&Expr::Literal(SqlValue::Int(42)), &row), Value::Int(42));
1704        assert_eq!(evaluate_expr(&Expr::Literal(SqlValue::Float(3.14)), &row), Value::Float(3.14));
1705        assert_eq!(evaluate_expr(&Expr::Literal(SqlValue::Null), &row), Value::Null);
1706    }
1707
1708    #[test]
1709    fn test_evaluate_expr_column() {
1710        let row = Row::from([("x".into(), Value::Int(10))]);
1711        assert_eq!(evaluate_expr(&Expr::Column("x".into()), &row), Value::Int(10));
1712        assert_eq!(evaluate_expr(&Expr::Column("missing".into()), &row), Value::Null);
1713    }
1714
1715    #[test]
1716    fn test_evaluate_expr_int_arithmetic() {
1717        let row = Row::from([("a".into(), Value::Int(10)), ("b".into(), Value::Int(3))]);
1718        let add = Expr::BinaryOp {
1719            left: Box::new(Expr::Column("a".into())),
1720            op: ArithOp::Add,
1721            right: Box::new(Expr::Column("b".into())),
1722        };
1723        assert_eq!(evaluate_expr(&add, &row), Value::Int(13));
1724
1725        let sub = Expr::BinaryOp {
1726            left: Box::new(Expr::Column("a".into())),
1727            op: ArithOp::Sub,
1728            right: Box::new(Expr::Column("b".into())),
1729        };
1730        assert_eq!(evaluate_expr(&sub, &row), Value::Int(7));
1731
1732        let mul = Expr::BinaryOp {
1733            left: Box::new(Expr::Column("a".into())),
1734            op: ArithOp::Mul,
1735            right: Box::new(Expr::Column("b".into())),
1736        };
1737        assert_eq!(evaluate_expr(&mul, &row), Value::Int(30));
1738
1739        let div = Expr::BinaryOp {
1740            left: Box::new(Expr::Column("a".into())),
1741            op: ArithOp::Div,
1742            right: Box::new(Expr::Column("b".into())),
1743        };
1744        assert_eq!(evaluate_expr(&div, &row), Value::Int(3)); // integer division
1745
1746        let modulo = Expr::BinaryOp {
1747            left: Box::new(Expr::Column("a".into())),
1748            op: ArithOp::Mod,
1749            right: Box::new(Expr::Column("b".into())),
1750        };
1751        assert_eq!(evaluate_expr(&modulo, &row), Value::Int(1));
1752    }
1753
1754    #[test]
1755    fn test_evaluate_expr_float_coercion() {
1756        let row = Row::from([("a".into(), Value::Int(10)), ("b".into(), Value::Float(3.0))]);
1757        let add = Expr::BinaryOp {
1758            left: Box::new(Expr::Column("a".into())),
1759            op: ArithOp::Add,
1760            right: Box::new(Expr::Column("b".into())),
1761        };
1762        assert_eq!(evaluate_expr(&add, &row), Value::Float(13.0));
1763    }
1764
1765    #[test]
1766    fn test_evaluate_expr_null_propagation() {
1767        let row = Row::from([("a".into(), Value::Int(10))]);
1768        let add = Expr::BinaryOp {
1769            left: Box::new(Expr::Column("a".into())),
1770            op: ArithOp::Add,
1771            right: Box::new(Expr::Column("missing".into())),
1772        };
1773        assert_eq!(evaluate_expr(&add, &row), Value::Null);
1774    }
1775
1776    #[test]
1777    fn test_evaluate_expr_div_by_zero() {
1778        let row = Row::from([("a".into(), Value::Int(10)), ("b".into(), Value::Int(0))]);
1779        let div = Expr::BinaryOp {
1780            left: Box::new(Expr::Column("a".into())),
1781            op: ArithOp::Div,
1782            right: Box::new(Expr::Column("b".into())),
1783        };
1784        assert_eq!(evaluate_expr(&div, &row), Value::Null);
1785    }
1786
1787    #[test]
1788    fn test_evaluate_expr_unary_minus() {
1789        let row = Row::from([("x".into(), Value::Int(5))]);
1790        let neg = Expr::UnaryMinus(Box::new(Expr::Column("x".into())));
1791        assert_eq!(evaluate_expr(&neg, &row), Value::Int(-5));
1792    }
1793
1794    #[test]
1795    fn test_select_with_expression() {
1796        // Integration test: SELECT count * 2 AS doubled FROM test
1797        let stmt = crate::query_parser::parse_query(
1798            "SELECT count * 2 AS doubled FROM test"
1799        ).unwrap();
1800        if let crate::query_parser::Statement::Select(q) = stmt {
1801            let (rows, cols) = execute_inner(&q, &make_rows(), None).unwrap();
1802            assert_eq!(cols, vec!["doubled"]);
1803            assert_eq!(rows.len(), 3);
1804            // Rows are: count=10, count=5, count=20
1805            let values: Vec<Value> = rows.iter().map(|r| r["doubled"].clone()).collect();
1806            assert!(values.contains(&Value::Int(20)));
1807            assert!(values.contains(&Value::Int(10)));
1808            assert!(values.contains(&Value::Int(40)));
1809        } else {
1810            panic!("Expected Select");
1811        }
1812    }
1813
1814    #[test]
1815    fn test_where_with_expression() {
1816        // SELECT * FROM test WHERE count * 2 > 15
1817        let stmt = crate::query_parser::parse_query(
1818            "SELECT * FROM test WHERE count * 2 > 15"
1819        ).unwrap();
1820        if let crate::query_parser::Statement::Select(q) = stmt {
1821            let (rows, _) = execute_inner(&q, &make_rows(), None).unwrap();
1822            // count=10 → 20 > 15 ✓, count=5 → 10 > 15 ✗, count=20 → 40 > 15 ✓
1823            assert_eq!(rows.len(), 2);
1824        } else {
1825            panic!("Expected Select");
1826        }
1827    }
1828
1829    #[test]
1830    fn test_order_by_expression() {
1831        // SELECT * FROM test ORDER BY count * -1 ASC (effectively DESC by count)
1832        let stmt = crate::query_parser::parse_query(
1833            "SELECT title, count FROM test ORDER BY count * -1 ASC"
1834        ).unwrap();
1835        if let crate::query_parser::Statement::Select(q) = stmt {
1836            let (rows, _) = execute_inner(&q, &make_rows(), None).unwrap();
1837            // count: 20 → -20, 10 → -10, 5 → -5, ASC means -20, -10, -5
1838            assert_eq!(rows[0]["count"], Value::Int(20));
1839            assert_eq!(rows[1]["count"], Value::Int(10));
1840            assert_eq!(rows[2]["count"], Value::Int(5));
1841        } else {
1842            panic!("Expected Select");
1843        }
1844    }
1845
1846    // ── CASE WHEN evaluation tests ────────────────────────────────
1847
1848    #[test]
1849    fn test_case_when_eval_basic() {
1850        let row = Row::from([("status".into(), Value::String("ACTIVE".into()))]);
1851        let expr = Expr::Case {
1852            whens: vec![(
1853                WhereClause::Comparison(Comparison {
1854                    column: "status".into(),
1855                    op: CmpOp::Eq,
1856                    value: Some(SqlValue::String("ACTIVE".into())),
1857                    left_expr: Some(Expr::Column("status".into())),
1858                    right_expr: Some(Expr::Literal(SqlValue::String("ACTIVE".into()))),
1859                }),
1860                Box::new(Expr::Literal(SqlValue::Int(1))),
1861            )],
1862            else_expr: Some(Box::new(Expr::Literal(SqlValue::Int(0)))),
1863        };
1864        assert_eq!(evaluate_expr(&expr, &row), Value::Int(1));
1865    }
1866
1867    #[test]
1868    fn test_case_when_eval_else() {
1869        let row = Row::from([("status".into(), Value::String("KILLED".into()))]);
1870        let expr = Expr::Case {
1871            whens: vec![(
1872                WhereClause::Comparison(Comparison {
1873                    column: "status".into(),
1874                    op: CmpOp::Eq,
1875                    value: Some(SqlValue::String("ACTIVE".into())),
1876                    left_expr: Some(Expr::Column("status".into())),
1877                    right_expr: Some(Expr::Literal(SqlValue::String("ACTIVE".into()))),
1878                }),
1879                Box::new(Expr::Literal(SqlValue::Int(1))),
1880            )],
1881            else_expr: Some(Box::new(Expr::Literal(SqlValue::Int(0)))),
1882        };
1883        assert_eq!(evaluate_expr(&expr, &row), Value::Int(0));
1884    }
1885
1886    #[test]
1887    fn test_case_when_eval_no_else_null() {
1888        let row = Row::from([("x".into(), Value::Int(99))]);
1889        let expr = Expr::Case {
1890            whens: vec![(
1891                WhereClause::Comparison(Comparison {
1892                    column: "x".into(),
1893                    op: CmpOp::Eq,
1894                    value: Some(SqlValue::Int(1)),
1895                    left_expr: Some(Expr::Column("x".into())),
1896                    right_expr: Some(Expr::Literal(SqlValue::Int(1))),
1897                }),
1898                Box::new(Expr::Literal(SqlValue::String("one".into()))),
1899            )],
1900            else_expr: None,
1901        };
1902        assert_eq!(evaluate_expr(&expr, &row), Value::Null);
1903    }
1904
1905    #[test]
1906    fn test_case_when_in_aggregate_query() {
1907        // SUM(CASE WHEN count > 5 THEN count ELSE 0 END)
1908        // Rows: count=10, count=5, count=20 → should sum 10 + 0 + 20 = 30
1909        let stmt = crate::query_parser::parse_query(
1910            "SELECT SUM(CASE WHEN count > 5 THEN count ELSE 0 END) AS total FROM test"
1911        ).unwrap();
1912        if let crate::query_parser::Statement::Select(q) = stmt {
1913            let (rows, cols) = execute_inner(&q, &make_rows(), None).unwrap();
1914            assert_eq!(cols, vec!["total"]);
1915            assert_eq!(rows.len(), 1);
1916            assert_eq!(rows[0]["total"], Value::Float(30.0));
1917        } else {
1918            panic!("Expected Select");
1919        }
1920    }
1921
1922    #[test]
1923    fn test_case_when_with_unary_minus_in_aggregate() {
1924        // SUM(CASE WHEN title = 'Alpha' THEN count ELSE -count END)
1925        // Alpha: 10, Beta: -5, Gamma: -20 → 10 - 5 - 20 = -15
1926        let stmt = crate::query_parser::parse_query(
1927            "SELECT SUM(CASE WHEN title = 'Alpha' THEN count ELSE -count END) AS net FROM test"
1928        ).unwrap();
1929        if let crate::query_parser::Statement::Select(q) = stmt {
1930            let (rows, _) = execute_inner(&q, &make_rows(), None).unwrap();
1931            assert_eq!(rows.len(), 1);
1932            assert_eq!(rows[0]["net"], Value::Float(-15.0));
1933        } else {
1934            panic!("Expected Select");
1935        }
1936    }
1937
1938    #[test]
1939    fn test_dateadd_with_dict_in_group_by() {
1940        // Simulate a joined row with a dict field, then GROUP BY + DateAdd expr
1941        use indexmap::IndexMap;
1942        let mut params = IndexMap::new();
1943        params.insert("exit_days".to_string(), Value::Int(21));
1944
1945        let rows = vec![
1946            Row::from([
1947                ("o.token".into(), Value::String("BTC".into())),
1948                ("o.event_date".into(), Value::Date(
1949                    chrono::NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()
1950                )),
1951                ("o.size".into(), Value::Int(100)),
1952                ("s.params".into(), Value::Dict(params.clone())),
1953            ]),
1954            Row::from([
1955                ("o.token".into(), Value::String("BTC".into())),
1956                ("o.event_date".into(), Value::Date(
1957                    chrono::NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()
1958                )),
1959                ("o.size".into(), Value::Int(50)),
1960                ("s.params".into(), Value::Dict(params.clone())),
1961            ]),
1962        ];
1963
1964        let q = SelectQuery {
1965            distinct: false,
1966            columns: ColumnList::Named(vec![
1967                SelectExpr::Column("o.token".into()),
1968                SelectExpr::Column("o.event_date".into()),
1969                SelectExpr::Expr {
1970                    expr: Expr::DateAdd {
1971                        date: Box::new(Expr::Column("o.event_date".into())),
1972                        days: Box::new(Expr::Column("s.params.exit_days".into())),
1973                    },
1974                    alias: Some("exit_date".into()),
1975                },
1976                SelectExpr::Aggregate {
1977                    func: AggFunc::Sum,
1978                    arg: "o.size".into(),
1979                    arg_expr: Some(Expr::Column("o.size".into())),
1980                    alias: Some("total".into()),
1981                },
1982            ]),
1983            table: "orders".into(),
1984            table_alias: None,
1985            subquery: None,
1986            joins: vec![],
1987            where_clause: None,
1988            group_by: Some(vec!["o.token".into(), "o.event_date".into()]),
1989            having: None,
1990            order_by: None,
1991            limit: None,
1992            ctes: vec![],
1993        };
1994
1995        let (rows, cols) = execute_inner(&q, &rows, None).unwrap();
1996        assert_eq!(rows.len(), 1);
1997        assert!(cols.contains(&"exit_date".to_string()));
1998        assert_eq!(rows[0]["total"], Value::Float(150.0));
1999        // The key test: exit_date should be 2026-01-22, not Null
2000        assert_eq!(
2001            rows[0]["exit_date"],
2002            Value::Date(chrono::NaiveDate::from_ymd_opt(2026, 1, 22).unwrap())
2003        );
2004    }
2005
2006    #[test]
2007    fn test_aggregate_arithmetic() {
2008        // SUM(count) for all rows = 10 + 5 + 20 = 35
2009        // COUNT(*) = 3
2010        // SUM produces Float, COUNT produces Int → mixed → Float division
2011        let stmt = crate::query_parser::parse_query(
2012            "SELECT SUM(count) / COUNT(*) AS avg_count FROM test"
2013        ).unwrap();
2014        if let crate::query_parser::Statement::Select(q) = stmt {
2015            let (rows, cols) = execute_inner(&q, &make_rows(), None).unwrap();
2016            assert_eq!(cols, vec!["avg_count"]);
2017            assert_eq!(rows.len(), 1);
2018            match &rows[0]["avg_count"] {
2019                Value::Float(f) => assert!((f - 11.666666666666666).abs() < 0.001),
2020                other => panic!("Expected Float, got {:?}", other),
2021            }
2022        } else {
2023            panic!("Expected Select");
2024        }
2025    }
2026
2027    #[test]
2028    fn test_aggregate_subtraction_with_group_by() {
2029        let rows = vec![
2030            {
2031                let mut r = Row::new();
2032                r.insert("token".into(), Value::String("BTC".into()));
2033                r.insert("side".into(), Value::String("BUY".into()));
2034                r.insert("size".into(), Value::Float(100.0));
2035                r
2036            },
2037            {
2038                let mut r = Row::new();
2039                r.insert("token".into(), Value::String("BTC".into()));
2040                r.insert("side".into(), Value::String("SELL".into()));
2041                r.insert("size".into(), Value::Float(60.0));
2042                r
2043            },
2044        ];
2045        let stmt = crate::query_parser::parse_query(
2046            "SELECT token, SUM(CASE WHEN side = 'BUY' THEN size ELSE 0 END) - SUM(CASE WHEN side = 'SELL' THEN size ELSE 0 END) AS net FROM test GROUP BY token"
2047        ).unwrap();
2048        if let crate::query_parser::Statement::Select(q) = stmt {
2049            let (result, _) = execute_inner(&q, &rows, None).unwrap();
2050            assert_eq!(result.len(), 1);
2051            assert_eq!(result[0]["net"], Value::Float(40.0));
2052        } else {
2053            panic!("Expected Select");
2054        }
2055    }
2056
2057    // ── Issue #42: Aggregate subtraction without GROUP BY ──
2058
2059    #[test]
2060    fn test_aggregate_subtraction_no_group() {
2061        // SUM(count) = 10 + 5 + 20 = 35, COUNT(*) = 3, diff = 35 - 3 = 32
2062        let stmt = crate::query_parser::parse_query(
2063            "SELECT SUM(count) - COUNT(*) as diff FROM test"
2064        ).unwrap();
2065        if let crate::query_parser::Statement::Select(q) = stmt {
2066            let (rows, cols) = execute_inner(&q, &make_rows(), None).unwrap();
2067            assert_eq!(cols, vec!["diff"]);
2068            assert_eq!(rows.len(), 1);
2069            assert_eq!(rows[0]["diff"], Value::Float(32.0));
2070        } else {
2071            panic!("Expected Select");
2072        }
2073    }
2074
2075    // ── Issue #42: Aggregate division with GROUP BY ──
2076
2077    #[test]
2078    fn test_aggregate_division_with_group_by() {
2079        let rows = vec![
2080            {
2081                let mut r = Row::new();
2082                r.insert("category".into(), Value::String("A".into()));
2083                r.insert("count".into(), Value::Int(10));
2084                r
2085            },
2086            {
2087                let mut r = Row::new();
2088                r.insert("category".into(), Value::String("A".into()));
2089                r.insert("count".into(), Value::Int(20));
2090                r
2091            },
2092            {
2093                let mut r = Row::new();
2094                r.insert("category".into(), Value::String("B".into()));
2095                r.insert("count".into(), Value::Int(6));
2096                r
2097            },
2098        ];
2099        // Group A: SUM(count)=30, COUNT(*)=2, ratio=15.0
2100        // Group B: SUM(count)=6, COUNT(*)=1, ratio=6.0
2101        let stmt = crate::query_parser::parse_query(
2102            "SELECT category, SUM(count) / COUNT(*) as ratio FROM test GROUP BY category"
2103        ).unwrap();
2104        if let crate::query_parser::Statement::Select(q) = stmt {
2105            let (result, cols) = execute_inner(&q, &rows, None).unwrap();
2106            assert!(cols.contains(&"ratio".to_string()));
2107            assert_eq!(result.len(), 2);
2108            // Find group A and B by category value
2109            let group_a = result.iter().find(|r| r["category"] == Value::String("A".into())).unwrap();
2110            let group_b = result.iter().find(|r| r["category"] == Value::String("B".into())).unwrap();
2111            match &group_a["ratio"] {
2112                Value::Float(f) => assert!((f - 15.0).abs() < 0.001),
2113                other => panic!("Expected Float for group A ratio, got {:?}", other),
2114            }
2115            match &group_b["ratio"] {
2116                Value::Float(f) => assert!((f - 6.0).abs() < 0.001),
2117                other => panic!("Expected Float for group B ratio, got {:?}", other),
2118            }
2119        } else {
2120            panic!("Expected Select");
2121        }
2122    }
2123
2124    // ── Window function tests ────────────────────────────────────
2125
2126    #[test]
2127    fn test_window_row_number() {
2128        let stmt = crate::query_parser::parse_query(
2129            "SELECT title, ROW_NUMBER() OVER (ORDER BY count DESC) AS rn FROM test"
2130        ).unwrap();
2131        if let crate::query_parser::Statement::Select(q) = stmt {
2132            let (rows, cols) = execute_inner(&q, &make_rows(), None).unwrap();
2133            assert_eq!(cols, vec!["title", "rn"]);
2134            assert_eq!(rows.len(), 3);
2135            let by_title: HashMap<String, i64> = rows.iter()
2136                .map(|r| (r["title"].to_display_string(), match &r["rn"] { Value::Int(n) => *n, _ => panic!("Expected Int") }))
2137                .collect();
2138            assert_eq!(by_title["Gamma"], 1); // count=20
2139            assert_eq!(by_title["Alpha"], 2); // count=10
2140            assert_eq!(by_title["Beta"], 3);  // count=5
2141        } else {
2142            panic!("Expected Select");
2143        }
2144    }
2145
2146    #[test]
2147    fn test_window_rank_with_ties() {
2148        let mut rows = make_rows();
2149        rows[0].insert("count".into(), Value::Int(10));
2150        rows[1].insert("count".into(), Value::Int(10));
2151        rows[2].insert("count".into(), Value::Int(5));
2152
2153        let stmt = crate::query_parser::parse_query(
2154            "SELECT title, RANK() OVER (ORDER BY count DESC) AS rnk FROM test"
2155        ).unwrap();
2156        if let crate::query_parser::Statement::Select(q) = stmt {
2157            let (result, _) = execute_inner(&q, &rows, None).unwrap();
2158            let ranks: Vec<i64> = result.iter()
2159                .map(|r| match &r["rnk"] { Value::Int(n) => *n, _ => panic!("Expected Int") })
2160                .collect();
2161            assert!(ranks.contains(&1)); // two tied at rank 1
2162            assert!(ranks.iter().filter(|&&r| r == 1).count() == 2);
2163            assert!(ranks.contains(&3)); // rank 3 (gap after tie)
2164        } else {
2165            panic!("Expected Select");
2166        }
2167    }
2168
2169    #[test]
2170    fn test_window_dense_rank() {
2171        let mut rows = make_rows();
2172        rows[0].insert("count".into(), Value::Int(10));
2173        rows[1].insert("count".into(), Value::Int(10));
2174        rows[2].insert("count".into(), Value::Int(5));
2175
2176        let stmt = crate::query_parser::parse_query(
2177            "SELECT title, DENSE_RANK() OVER (ORDER BY count DESC) AS dr FROM test"
2178        ).unwrap();
2179        if let crate::query_parser::Statement::Select(q) = stmt {
2180            let (result, _) = execute_inner(&q, &rows, None).unwrap();
2181            let ranks: Vec<i64> = result.iter()
2182                .map(|r| match &r["dr"] { Value::Int(n) => *n, _ => panic!("Expected Int") })
2183                .collect();
2184            assert!(ranks.iter().filter(|&&r| r == 1).count() == 2);
2185            assert!(ranks.contains(&2)); // dense rank: no gap
2186            assert!(!ranks.contains(&3));
2187        } else {
2188            panic!("Expected Select");
2189        }
2190    }
2191
2192    #[test]
2193    fn test_window_lag() {
2194        let stmt = crate::query_parser::parse_query(
2195            "SELECT title, LAG(count, 1) OVER (ORDER BY count ASC) AS prev FROM test"
2196        ).unwrap();
2197        if let crate::query_parser::Statement::Select(q) = stmt {
2198            let (rows, _) = execute_inner(&q, &make_rows(), None).unwrap();
2199            // Sorted ASC: Beta(5), Alpha(10), Gamma(20)
2200            // LAG: NULL, 5, 10
2201            let first = rows.iter().find(|r| r["title"] == Value::String("Beta".into())).unwrap();
2202            assert_eq!(first["prev"], Value::Null);
2203            let second = rows.iter().find(|r| r["title"] == Value::String("Alpha".into())).unwrap();
2204            assert_eq!(second["prev"], Value::Int(5));
2205            let third = rows.iter().find(|r| r["title"] == Value::String("Gamma".into())).unwrap();
2206            assert_eq!(third["prev"], Value::Int(10));
2207        } else {
2208            panic!("Expected Select");
2209        }
2210    }
2211
2212    #[test]
2213    fn test_window_lead() {
2214        let stmt = crate::query_parser::parse_query(
2215            "SELECT title, LEAD(count, 1) OVER (ORDER BY count ASC) AS next FROM test"
2216        ).unwrap();
2217        if let crate::query_parser::Statement::Select(q) = stmt {
2218            let (rows, _) = execute_inner(&q, &make_rows(), None).unwrap();
2219            let first = rows.iter().find(|r| r["title"] == Value::String("Beta".into())).unwrap();
2220            assert_eq!(first["next"], Value::Int(10));
2221            let last = rows.iter().find(|r| r["title"] == Value::String("Gamma".into())).unwrap();
2222            assert_eq!(last["next"], Value::Null);
2223        } else {
2224            panic!("Expected Select");
2225        }
2226    }
2227
2228    #[test]
2229    fn test_window_sum_partition() {
2230        let rows = vec![
2231            Row::from([
2232                ("cat".into(), Value::String("A".into())),
2233                ("val".into(), Value::Int(10)),
2234            ]),
2235            Row::from([
2236                ("cat".into(), Value::String("A".into())),
2237                ("val".into(), Value::Int(20)),
2238            ]),
2239            Row::from([
2240                ("cat".into(), Value::String("B".into())),
2241                ("val".into(), Value::Int(5)),
2242            ]),
2243        ];
2244        let stmt = crate::query_parser::parse_query(
2245            "SELECT cat, val, SUM(val) OVER (PARTITION BY cat) AS cat_total FROM test"
2246        ).unwrap();
2247        if let crate::query_parser::Statement::Select(q) = stmt {
2248            let (result, cols) = execute_inner(&q, &rows, None).unwrap();
2249            assert_eq!(cols, vec!["cat", "val", "cat_total"]);
2250            assert_eq!(result.len(), 3);
2251            let a_rows: Vec<_> = result.iter().filter(|r| r["cat"] == Value::String("A".into())).collect();
2252            assert_eq!(a_rows.len(), 2);
2253            for r in &a_rows {
2254                assert_eq!(r["cat_total"], Value::Float(30.0));
2255            }
2256            let b_row = result.iter().find(|r| r["cat"] == Value::String("B".into())).unwrap();
2257            assert_eq!(b_row["cat_total"], Value::Float(5.0));
2258        } else {
2259            panic!("Expected Select");
2260        }
2261    }
2262
2263    #[test]
2264    fn test_window_with_where_order_limit() {
2265        let stmt = crate::query_parser::parse_query(
2266            "SELECT title, ROW_NUMBER() OVER (ORDER BY count DESC) AS rn FROM test WHERE count > 4 ORDER BY rn LIMIT 2"
2267        ).unwrap();
2268        if let crate::query_parser::Statement::Select(q) = stmt {
2269            let (result, _) = execute_inner(&q, &make_rows(), None).unwrap();
2270            assert_eq!(result.len(), 2);
2271            assert_eq!(result[0]["rn"], Value::Int(1));
2272            assert_eq!(result[1]["rn"], Value::Int(2));
2273        } else {
2274            panic!("Expected Select");
2275        }
2276    }
2277}