vibesql-executor 0.2.0

Query execution engine for vibesql SQL database
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
//! HAVING clause evaluation for columnar GROUP BY results
//!
//! This module provides evaluation of HAVING conditions on the results
//! of columnar GROUP BY execution. The key insight is that after GROUP BY
//! completes, aggregate values are already computed and stored in result rows.
//!
//! ## Result Row Structure
//!
//! After columnar GROUP BY, each result row has:
//! ```text
//! [group_key_col_0, group_key_col_1, ..., agg_0, agg_1, ...]
//! ```
//!
//! ## HAVING Evaluation Strategy
//!
//! 1. Map aggregate function references in HAVING to their column positions
//! 2. Evaluate the HAVING expression using the pre-computed aggregate values
//! 3. Filter out rows where HAVING evaluates to false

use vibesql_ast::Expression;
use vibesql_storage::Row;
use vibesql_types::{SqlMode, SqlValue};

use crate::{
    errors::ExecutorError,
    evaluator::casting::cast_value,
    schema::CombinedSchema,
    select::columnar::{AggregateOp, AggregateSource, AggregateSpec},
};

/// Filter GROUP BY results using a HAVING clause
///
/// This function evaluates the HAVING expression on each result row from columnar
/// GROUP BY and filters out rows that don't match the condition.
///
/// # Arguments
///
/// * `rows` - Result rows from columnar GROUP BY (group keys + aggregates)
/// * `having_expr` - The HAVING clause expression
/// * `group_col_count` - Number of GROUP BY columns (prefix of each row)
/// * `aggregates` - The aggregate specs (in same order as aggregate columns in rows)
/// * `schema` - Schema for resolving column references
///
/// # Returns
///
/// Filtered rows where HAVING condition is true
pub fn apply_having_filter(
    rows: Vec<Row>,
    having_expr: &Expression,
    group_col_count: usize,
    aggregates: &[AggregateSpec],
    schema: &CombinedSchema,
) -> Result<Vec<Row>, ExecutorError> {
    if rows.is_empty() {
        return Ok(rows);
    }

    let total = rows.len();
    let mut filtered = Vec::with_capacity(total);

    for row in rows {
        let include =
            evaluate_having_on_result_row(having_expr, &row, group_col_count, aggregates, schema)?;

        if include {
            filtered.push(row);
        }
    }

    log::debug!("HAVING filter: {} of {} rows passed", filtered.len(), total);

    Ok(filtered)
}

/// Evaluate a HAVING expression on a single result row
///
/// The result row structure is: [group_cols..., aggregate_values...]
/// We need to map aggregate references in the expression to their positions.
fn evaluate_having_on_result_row(
    expr: &Expression,
    row: &Row,
    group_col_count: usize,
    aggregates: &[AggregateSpec],
    schema: &CombinedSchema,
) -> Result<bool, ExecutorError> {
    let result = evaluate_expr_on_result_row(expr, row, group_col_count, aggregates, schema)?;
    is_truthy(&result)
}

/// Evaluate an expression against a result row, resolving aggregate references
fn evaluate_expr_on_result_row(
    expr: &Expression,
    row: &Row,
    group_col_count: usize,
    aggregates: &[AggregateSpec],
    schema: &CombinedSchema,
) -> Result<SqlValue, ExecutorError> {
    match expr {
        // Aggregate function - look up pre-computed value in result row
        Expression::AggregateFunction { name, args, distinct, .. } => {
            if *distinct {
                return Err(ExecutorError::UnsupportedFeature(
                    "DISTINCT aggregates in HAVING not supported in columnar path".to_string(),
                ));
            }

            let op = match name.to_uppercase().as_str() {
                "SUM" => AggregateOp::Sum,
                "COUNT" => AggregateOp::Count,
                "AVG" => AggregateOp::Avg,
                "MIN" => AggregateOp::Min,
                "MAX" => AggregateOp::Max,
                _ => {
                    return Err(ExecutorError::UnsupportedFeature(format!(
                        "Aggregate function {} not supported in columnar HAVING",
                        name
                    )));
                }
            };

            // Find the aggregate source
            let source = if op == AggregateOp::Count && args.is_empty() {
                AggregateSource::CountStar
            } else if args.len() == 1 {
                match &args[0] {
                    Expression::ColumnRef(col_id) => {
                        if let Some(idx) = schema
                            .get_column_index(col_id.table_canonical(), col_id.column_canonical())
                        {
                            AggregateSource::Column(idx)
                        } else {
                            // Try expression source for complex args
                            AggregateSource::Expression(args[0].clone())
                        }
                    }
                    Expression::Wildcard => AggregateSource::CountStar,
                    other => AggregateSource::Expression(other.clone()),
                }
            } else {
                return Err(ExecutorError::UnsupportedFeature(
                    "Multi-argument aggregates not supported in columnar HAVING".to_string(),
                ));
            };

            // Find matching aggregate in the spec list
            let agg_idx = aggregates
                .iter()
                .position(|spec| spec.op == op && sources_match(&spec.source, &source))
                .ok_or_else(|| {
                    ExecutorError::Other(format!(
                        "Aggregate {}({:?}) not found in computed aggregates",
                        name, source
                    ))
                })?;

            // Aggregate values start after group columns
            let value_idx = group_col_count + agg_idx;
            if value_idx < row.values.len() {
                Ok(row.values[value_idx].clone())
            } else {
                Err(ExecutorError::Other(format!(
                    "Aggregate index {} out of bounds (row has {} values)",
                    value_idx,
                    row.values.len()
                )))
            }
        }

        // Binary operations - evaluate both sides
        Expression::BinaryOp { left, op, right } => {
            let left_val =
                evaluate_expr_on_result_row(left, row, group_col_count, aggregates, schema)?;
            let right_val =
                evaluate_expr_on_result_row(right, row, group_col_count, aggregates, schema)?;
            crate::evaluator::ExpressionEvaluator::eval_binary_op_static(
                &left_val,
                op,
                &right_val,
                SqlMode::default(),
            )
        }

        // Conjunction (AND chain)
        Expression::Conjunction(exprs) => {
            for e in exprs {
                let val = evaluate_expr_on_result_row(e, row, group_col_count, aggregates, schema)?;
                if !is_truthy(&val)? {
                    return Ok(SqlValue::Boolean(false));
                }
            }
            Ok(SqlValue::Boolean(true))
        }

        // Disjunction (OR chain)
        Expression::Disjunction(exprs) => {
            for e in exprs {
                let val = evaluate_expr_on_result_row(e, row, group_col_count, aggregates, schema)?;
                if is_truthy(&val)? {
                    return Ok(SqlValue::Boolean(true));
                }
            }
            Ok(SqlValue::Boolean(false))
        }

        // Unary operations
        Expression::UnaryOp { op, expr: inner } => {
            let inner_val =
                evaluate_expr_on_result_row(inner, row, group_col_count, aggregates, schema)?;
            crate::evaluator::eval_unary_op(op, &inner_val)
        }

        // Literal values
        Expression::Literal(lit) => Ok(lit.clone()),

        // Column references in HAVING (must be GROUP BY columns)
        // Note: The result row has group columns first, then aggregate values.
        // The group columns are stored in GROUP BY clause order, which may not match
        // schema order. We fall back to row-based for this case as a temporary fix.
        Expression::ColumnRef(col_id) => {
            // Column references in HAVING with GROUP BY require knowing the mapping
            // from GROUP BY expression order to result row positions. Since the columnar
            // path currently doesn't have this mapping, fall back to row-based execution.
            Err(ExecutorError::Other(format!(
                "Column reference {}.{} in HAVING not supported in columnar path - falling back to row-based",
                col_id.table_canonical().unwrap_or(""),
                col_id.column_canonical()
            )))
        }

        // Subquery variants - not supported in columnar path
        Expression::ScalarSubquery(_)
        | Expression::In { .. }
        | Expression::Exists { .. }
        | Expression::QuantifiedComparison { .. } => Err(ExecutorError::UnsupportedFeature(
            "Subqueries in HAVING not supported in columnar path".to_string(),
        )),

        // IN list
        Expression::InList { expr, values, negated } => {
            let val = evaluate_expr_on_result_row(expr, row, group_col_count, aggregates, schema)?;

            // SQL three-valued logic for IN:
            // - NULL IN (list) -> NULL (unless list is empty)
            // - x IN (NULL, ...) -> TRUE if x matches any non-NULL, else NULL
            if matches!(val, SqlValue::Null) {
                // NULL IN (empty list) -> FALSE per SQLite behavior
                if values.is_empty() {
                    return Ok(SqlValue::Boolean(*negated));
                }
                // NULL IN (non-empty list) -> NULL
                return Ok(SqlValue::Null);
            }

            let mut found = false;
            let mut found_null = false;
            for v in values {
                let list_val =
                    evaluate_expr_on_result_row(v, row, group_col_count, aggregates, schema)?;

                // Track if we encounter NULL in the list
                if matches!(list_val, SqlValue::Null) {
                    found_null = true;
                    continue;
                }

                if crate::evaluator::ExpressionEvaluator::values_are_equal(&val, &list_val) {
                    found = true;
                    break;
                }
            }

            // SQL three-valued logic:
            // - If found a match: return TRUE (or FALSE if negated)
            // - If not found but list contains NULL: return NULL
            // - If not found and no NULL: return FALSE (or TRUE if negated)
            if found {
                Ok(SqlValue::Boolean(!negated))
            } else if found_null {
                Ok(SqlValue::Null)
            } else {
                Ok(SqlValue::Boolean(*negated))
            }
        }

        // BETWEEN
        Expression::Between { expr, low, high, negated, .. } => {
            let val = evaluate_expr_on_result_row(expr, row, group_col_count, aggregates, schema)?;
            let low_val =
                evaluate_expr_on_result_row(low, row, group_col_count, aggregates, schema)?;
            let high_val =
                evaluate_expr_on_result_row(high, row, group_col_count, aggregates, schema)?;

            let ge_low = crate::evaluator::ExpressionEvaluator::eval_binary_op_static(
                &val,
                &vibesql_ast::BinaryOperator::GreaterThanOrEqual,
                &low_val,
                SqlMode::default(),
            )?;
            let le_high = crate::evaluator::ExpressionEvaluator::eval_binary_op_static(
                &val,
                &vibesql_ast::BinaryOperator::LessThanOrEqual,
                &high_val,
                SqlMode::default(),
            )?;

            // SQL three-valued logic: if either comparison is NULL, the result is NULL
            // (unless one is definitively false, which would make the whole thing false)
            let in_range = match (&ge_low, &le_high) {
                // Both are true booleans -> true
                (SqlValue::Boolean(true), SqlValue::Boolean(true)) => SqlValue::Boolean(true),
                // One is definitively false -> false (regardless of NULL on other side)
                (SqlValue::Boolean(false), _) | (_, SqlValue::Boolean(false)) => {
                    SqlValue::Boolean(false)
                }
                // Any NULL with non-false -> NULL
                (SqlValue::Null, _) | (_, SqlValue::Null) => SqlValue::Null,
                // Treat other truthy values as true
                _ => {
                    let left_truthy = is_truthy(&ge_low)?;
                    let right_truthy = is_truthy(&le_high)?;
                    SqlValue::Boolean(left_truthy && right_truthy)
                }
            };

            // Apply negation with NULL propagation
            match in_range {
                SqlValue::Null => Ok(SqlValue::Null),
                SqlValue::Boolean(b) => Ok(SqlValue::Boolean(if *negated { !b } else { b })),
                other => {
                    // Should not happen but handle gracefully
                    let b = is_truthy(&other)?;
                    Ok(SqlValue::Boolean(if *negated { !b } else { b }))
                }
            }
        }

        // IS NULL / IS NOT NULL
        Expression::IsNull { expr, negated } => {
            let val = evaluate_expr_on_result_row(expr, row, group_col_count, aggregates, schema)?;
            let is_null = matches!(val, SqlValue::Null);
            Ok(SqlValue::Boolean(if *negated { !is_null } else { is_null }))
        }

        // CASE expression
        Expression::Case { operand, when_clauses, else_result } => {
            match operand {
                Some(operand_expr) => {
                    // Simple CASE: CASE x WHEN v THEN result
                    let operand_val = evaluate_expr_on_result_row(
                        operand_expr,
                        row,
                        group_col_count,
                        aggregates,
                        schema,
                    )?;

                    for when_clause in when_clauses {
                        for condition in &when_clause.conditions {
                            let when_val = evaluate_expr_on_result_row(
                                condition,
                                row,
                                group_col_count,
                                aggregates,
                                schema,
                            )?;
                            if crate::evaluator::ExpressionEvaluator::values_are_equal(
                                &operand_val,
                                &when_val,
                            ) {
                                return evaluate_expr_on_result_row(
                                    &when_clause.result,
                                    row,
                                    group_col_count,
                                    aggregates,
                                    schema,
                                );
                            }
                        }
                    }

                    if let Some(else_expr) = else_result {
                        evaluate_expr_on_result_row(
                            else_expr,
                            row,
                            group_col_count,
                            aggregates,
                            schema,
                        )
                    } else {
                        Ok(SqlValue::Null)
                    }
                }
                None => {
                    // Searched CASE: CASE WHEN cond THEN result
                    for when_clause in when_clauses {
                        for condition in &when_clause.conditions {
                            let cond_val = evaluate_expr_on_result_row(
                                condition,
                                row,
                                group_col_count,
                                aggregates,
                                schema,
                            )?;
                            if is_truthy(&cond_val)? {
                                return evaluate_expr_on_result_row(
                                    &when_clause.result,
                                    row,
                                    group_col_count,
                                    aggregates,
                                    schema,
                                );
                            }
                        }
                    }

                    if let Some(else_expr) = else_result {
                        evaluate_expr_on_result_row(
                            else_expr,
                            row,
                            group_col_count,
                            aggregates,
                            schema,
                        )
                    } else {
                        Ok(SqlValue::Null)
                    }
                }
            }
        }

        // Function call (non-aggregate) - not currently supported in columnar HAVING
        // This is rare in practice and would require access to the database for sql_mode
        Expression::Function { name, .. } => Err(ExecutorError::UnsupportedFeature(format!(
            "Scalar function {} in HAVING not supported in columnar path",
            name
        ))),

        // Cast expression
        Expression::Cast { expr, data_type } => {
            let val = evaluate_expr_on_result_row(expr, row, group_col_count, aggregates, schema)?;
            cast_value(&val, data_type, &SqlMode::default())
        }

        // Other expressions not supported
        _ => Err(ExecutorError::UnsupportedFeature(format!(
            "Expression type {:?} not supported in columnar HAVING",
            std::mem::discriminant(expr)
        ))),
    }
}

/// Check if two aggregate sources match
fn sources_match(spec_source: &AggregateSource, having_source: &AggregateSource) -> bool {
    match (spec_source, having_source) {
        (AggregateSource::Column(a), AggregateSource::Column(b)) => a == b,
        (AggregateSource::CountStar, AggregateSource::CountStar) => true,
        (AggregateSource::Expression(a), AggregateSource::Expression(b)) => {
            // Compare expressions structurally
            expressions_equal(a, b)
        }
        _ => false,
    }
}

/// Check if two expressions are structurally equal
fn expressions_equal(a: &Expression, b: &Expression) -> bool {
    match (a, b) {
        (Expression::ColumnRef(col_id1), Expression::ColumnRef(col_id2))
            if col_id1.schema_canonical().is_none() && col_id2.schema_canonical().is_none() =>
        {
            col_id1.table_canonical() == col_id2.table_canonical()
                && col_id1.column_canonical() == col_id2.column_canonical()
        }
        (
            Expression::BinaryOp { left: l1, op: o1, right: r1 },
            Expression::BinaryOp { left: l2, op: o2, right: r2 },
        ) => o1 == o2 && expressions_equal(l1, l2) && expressions_equal(r1, r2),
        (Expression::Literal(l1), Expression::Literal(l2)) => l1 == l2,
        _ => false,
    }
}

/// Check if a SqlValue is truthy (evaluates to true in boolean context)
fn is_truthy(value: &SqlValue) -> Result<bool, ExecutorError> {
    match value {
        SqlValue::Boolean(b) => Ok(*b),
        SqlValue::Integer(i) => Ok(*i != 0),
        SqlValue::Float(f) => Ok(*f != 0.0),
        SqlValue::Null => Ok(false),
        _ => Ok(true), // Non-empty strings, etc. are truthy
    }
}