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
//! SELECT without FROM clause execution
//!
//! This module handles the special case of SELECT statements without a FROM clause,
//! which evaluates expressions without any table context.

use std::collections::HashMap;

use super::builder::SelectExecutor;
use crate::{
    errors::ExecutorError,
    evaluator::{CombinedExpressionEvaluator, ExpressionEvaluator},
    select::cte::CteResult,
    select::helpers::{apply_limit_offset, evaluate_limit, evaluate_offset},
    select::window::{evaluate_window_functions, has_window_functions},
};

impl SelectExecutor<'_> {
    /// Execute SELECT without FROM clause
    ///
    /// Evaluates expressions in the SELECT list without any table context.
    /// Returns a single row with the evaluated expressions.
    ///
    /// Example: `SELECT 1 + 1, 'hello', CURRENT_DATE`
    ///
    /// Fix for select1-18.1: When outer context is available (outer_row, outer_schema),
    /// column references can be resolved from the outer scope. This enables queries like:
    /// `SELECT * FROM t2 WHERE x IN (SELECT x)` where the inner `SELECT x` has no FROM
    /// but `x` is resolved from the outer `t2`.
    ///
    /// Fix for #5350: `cte_results` carries the enclosing statement's CTE bindings
    /// (already merged with any outer CTE context by `execute()`), so subqueries in
    /// expression position (scalar, EXISTS, IN) resolve CTE names before falling
    /// back to same-named catalog tables/views, matching SQLite precedence.
    pub(in crate::select::executor) fn execute_select_without_from(
        &self,
        stmt: &vibesql_ast::SelectStmt,
        cte_results: &HashMap<String, CteResult>,
    ) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
        // Validate ORDER BY column positions early (SQLite compatibility)
        if let Some(order_by) = &stmt.order_by {
            for (term_index, order_item) in order_by.iter().enumerate() {
                // Check for numeric column positions
                if let vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(pos)) =
                    &order_item.expr
                {
                    if *pos <= 0 || (*pos as usize) > stmt.select_list.len() {
                        return Err(ExecutorError::OrderByOutOfRange {
                            term_position: term_index + 1,
                            column_number: *pos,
                            select_list_len: stmt.select_list.len(),
                        });
                    }
                }
                // Check for negative column positions (parsed as UnaryOp { Minus, Integer })
                if let vibesql_ast::Expression::UnaryOp {
                    op: vibesql_ast::UnaryOperator::Minus,
                    expr,
                } = &order_item.expr
                {
                    if let vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Integer(pos)) =
                        expr.as_ref()
                    {
                        return Err(ExecutorError::OrderByOutOfRange {
                            term_position: term_index + 1,
                            column_number: -*pos,
                            select_list_len: stmt.select_list.len(),
                        });
                    }
                }
            }
        }

        // CTE context for expression-position subqueries (#5350).
        // `cte_results` already merges local CTEs over the executor's outer CTE
        // context (see execute()); fall back to the executor's context when the
        // merged map is empty (same pattern as the materialized/iterator paths).
        let cte_ctx = if !cte_results.is_empty() { Some(cte_results) } else { self.cte_context };

        // Check if we have outer context (for correlated subqueries)
        let has_outer_context = self.outer_row.is_some() && self.outer_schema.is_some();

        // SQLite allows the WHERE clause of a FROM-less SELECT to reference
        // select-list aliases, e.g. `SELECT (SELECT '') x WHERE x+x` (window1.test
        // 15.2). Without outer context, a column reference in WHERE can only
        // resolve to a select-list alias, so evaluate the select list first and
        // bind the aliases for the WHERE evaluation.
        if let Some(where_clause) = &stmt.where_clause {
            // #5445: inside a trigger body the WHERE can reference NEW/OLD
            // pseudo-variables, which resolve from the firing row rather than a
            // select-list alias; only divert non-pseudo column references to the
            // alias-binding path.
            let references_alias_candidate = if self.trigger_context.is_some() {
                self.expression_references_non_pseudo_column(where_clause)
            } else {
                self.expression_references_column(where_clause)
            };
            if !has_outer_context
                && !has_window_functions(&stmt.select_list)
                && references_alias_candidate
            {
                return self.execute_select_without_from_alias_where(stmt, where_clause, cte_ctx);
            }
        }

        // Evaluate WHERE clause first - if it's false, return empty result (SQLite compatibility)
        // This handles cases like `SELECT 99 WHERE 0` which should return no rows
        if let Some(where_clause) = &stmt.where_clause {
            let where_result = if has_outer_context {
                let outer_row = self.outer_row.unwrap();
                let outer_schema = self.outer_schema.unwrap();
                let empty_combined_schema = crate::schema::CombinedSchema::empty();
                let mut evaluator = CombinedExpressionEvaluator::with_database_and_outer_context(
                    &empty_combined_schema,
                    self.database,
                    outer_row,
                    outer_schema,
                );
                if let Some(cte) = cte_ctx {
                    evaluator = evaluator.with_cte_context(cte);
                }
                evaluator.eval(where_clause, outer_row)?
            } else {
                // No outer context - thread the trigger NEW/OLD context (#5445)
                // so a WHERE over NEW/OLD inside a trigger-body SELECT resolves.
                let empty_schema = vibesql_catalog::TableSchema::new("".to_string(), vec![]);
                let mut evaluator = match self.trigger_context {
                    Some(trigger_ctx) => ExpressionEvaluator::with_trigger_context(
                        &empty_schema,
                        self.database,
                        trigger_ctx,
                    ),
                    None => ExpressionEvaluator::with_database(&empty_schema, self.database),
                };
                if let Some(cte) = cte_ctx {
                    evaluator = evaluator.with_cte_context(cte);
                }
                let empty_row = vibesql_storage::Row::new(vec![]);
                evaluator.eval(where_clause, &empty_row)?
            };

            // Check if WHERE condition is truthy (SQLite boolean semantics)
            let is_truthy = match &where_result {
                vibesql_types::SqlValue::Boolean(b) => *b,
                vibesql_types::SqlValue::Null => false,
                vibesql_types::SqlValue::Integer(n) => *n != 0,
                vibesql_types::SqlValue::Smallint(n) => *n != 0,
                vibesql_types::SqlValue::Bigint(n) => *n != 0,
                vibesql_types::SqlValue::Float(f) => *f != 0.0,
                vibesql_types::SqlValue::Real(f) => *f != 0.0,
                vibesql_types::SqlValue::Double(f) => *f != 0.0,
                vibesql_types::SqlValue::Numeric(n) => *n != 0.0,
                _ => true, // Non-numeric values are truthy
            };

            if !is_truthy {
                // WHERE clause is false - return empty result
                return Ok(vec![]);
            }
        }

        // Check if SELECT list contains window functions
        // Window functions without FROM clause operate on a single implicit row (Issue #4747)
        // Example: SELECT min(1) OVER() returns 1 (the min of a single implicit row)
        if has_window_functions(&stmt.select_list) {
            return self.execute_select_without_from_with_windows(stmt, has_outer_context, cte_ctx);
        }

        // Evaluate each item in the SELECT list
        let mut values = Vec::new();
        for item in &stmt.select_list {
            match item {
                vibesql_ast::SelectItem::Wildcard { .. }
                | vibesql_ast::SelectItem::QualifiedWildcard { .. } => {
                    return Err(ExecutorError::UnsupportedFeature(
                        "SELECT * and qualified wildcards require FROM clause".to_string(),
                    ));
                }
                vibesql_ast::SelectItem::Expression { expr, .. } => {
                    // Check if expression references a column
                    // FIX for select1-18.1: Only error if we don't have outer context
                    // When outer context exists, columns can be resolved from outer scope.
                    //
                    // #5445: when this from-less SELECT runs inside a trigger body
                    // (self.trigger_context is set), NEW/OLD pseudo-variables are
                    // resolvable from the firing row's context, so they must not be
                    // rejected here. A plain (non-pseudo) column reference still
                    // requires a FROM clause — it has nothing to bind to.
                    let column_check_fails = if self.trigger_context.is_some() {
                        self.expression_references_non_pseudo_column(expr)
                    } else {
                        self.expression_references_column(expr)
                    };
                    if !has_outer_context && column_check_fails {
                        return Err(ExecutorError::UnsupportedFeature(
                            "Column reference requires FROM clause".to_string(),
                        ));
                    }

                    // Evaluate the expression
                    let value = if has_outer_context {
                        // Use outer context for column resolution
                        let outer_row = self.outer_row.unwrap();
                        let outer_schema = self.outer_schema.unwrap();
                        // Create an empty CombinedSchema for current level (no FROM clause)
                        let empty_combined_schema = crate::schema::CombinedSchema::empty();
                        // Create evaluator that can resolve columns from outer schema
                        let mut evaluator =
                            CombinedExpressionEvaluator::with_database_and_outer_context(
                                &empty_combined_schema,
                                self.database,
                                outer_row,
                                outer_schema,
                            );
                        if let Some(cte) = cte_ctx {
                            evaluator = evaluator.with_cte_context(cte);
                        }
                        evaluator.eval(expr, outer_row)?
                    } else {
                        // No outer context - use simple evaluation, threading the
                        // trigger NEW/OLD context (#5445) so pseudo-variables resolve.
                        let empty_schema =
                            vibesql_catalog::TableSchema::new("".to_string(), vec![]);
                        let mut evaluator = match self.trigger_context {
                            Some(trigger_ctx) => ExpressionEvaluator::with_trigger_context(
                                &empty_schema,
                                self.database,
                                trigger_ctx,
                            ),
                            None => ExpressionEvaluator::with_database(&empty_schema, self.database),
                        };
                        if let Some(cte) = cte_ctx {
                            evaluator = evaluator.with_cte_context(cte);
                        }
                        let empty_row = vibesql_storage::Row::new(vec![]);
                        evaluator.eval(expr, &empty_row)?
                    };
                    values.push(value);
                }
            }
        }

        // Build result row
        let result = vec![vibesql_storage::Row::new(values)];

        // Apply LIMIT and OFFSET (important for queries like SELECT 1 LIMIT 0)
        let limit = evaluate_limit(&stmt.limit, self.database)?;
        let offset = evaluate_offset(&stmt.offset, self.database)?;
        Ok(apply_limit_offset(result, limit, offset))
    }

    /// Execute a FROM-less SELECT whose WHERE clause references select-list
    /// aliases (SQLite compatibility, e.g. `SELECT (SELECT '') x WHERE x+x`).
    ///
    /// Evaluates the select list first, binds each item's alias as a column in
    /// a synthetic single-row schema, then evaluates the WHERE clause against
    /// that row. Column references that don't match an alias produce the same
    /// "column not found" error as before.
    fn execute_select_without_from_alias_where(
        &self,
        stmt: &vibesql_ast::SelectStmt,
        where_clause: &vibesql_ast::Expression,
        cte_ctx: Option<&HashMap<String, CteResult>>,
    ) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
        let empty_schema = vibesql_catalog::TableSchema::new("".to_string(), vec![]);
        let mut evaluator = ExpressionEvaluator::with_database(&empty_schema, self.database);
        if let Some(cte) = cte_ctx {
            evaluator = evaluator.with_cte_context(cte);
        }
        let empty_row = vibesql_storage::Row::new(vec![]);

        // Evaluate the select list and collect alias bindings
        let mut values = Vec::new();
        let mut alias_columns = Vec::new();
        for item in &stmt.select_list {
            match item {
                vibesql_ast::SelectItem::Wildcard { .. }
                | vibesql_ast::SelectItem::QualifiedWildcard { .. } => {
                    return Err(ExecutorError::UnsupportedFeature(
                        "SELECT * and qualified wildcards require FROM clause".to_string(),
                    ));
                }
                vibesql_ast::SelectItem::Expression { expr, alias, .. } => {
                    if self.expression_references_column(expr) {
                        return Err(ExecutorError::UnsupportedFeature(
                            "Column reference requires FROM clause".to_string(),
                        ));
                    }
                    let value = evaluator.eval(expr, &empty_row)?;
                    let column_name = alias.clone().unwrap_or_default();
                    let data_type = crate::select::cte::infer_type_from_value(&value);
                    alias_columns.push(vibesql_catalog::ColumnSchema::new(
                        column_name,
                        data_type,
                        true,
                    ));
                    values.push(value);
                }
            }
        }

        // Evaluate WHERE with the aliases bound as columns of a single row
        let alias_schema = vibesql_catalog::TableSchema::new("".to_string(), alias_columns);
        let mut where_evaluator = ExpressionEvaluator::with_database(&alias_schema, self.database);
        if let Some(cte) = cte_ctx {
            where_evaluator = where_evaluator.with_cte_context(cte);
        }
        let result_row = vibesql_storage::Row::new(values);
        let where_result = where_evaluator.eval(where_clause, &result_row)?;

        let is_truthy = match &where_result {
            vibesql_types::SqlValue::Boolean(b) => *b,
            vibesql_types::SqlValue::Null => false,
            vibesql_types::SqlValue::Integer(n) => *n != 0,
            vibesql_types::SqlValue::Smallint(n) => *n != 0,
            vibesql_types::SqlValue::Bigint(n) => *n != 0,
            vibesql_types::SqlValue::Float(f) => *f != 0.0,
            vibesql_types::SqlValue::Real(f) => *f != 0.0,
            vibesql_types::SqlValue::Double(f) => *f != 0.0,
            vibesql_types::SqlValue::Numeric(n) => *n != 0.0,
            // Non-numeric, non-NULL values follow SQLite numeric coercion:
            // strings coerce to their numeric prefix (0 when non-numeric)
            vibesql_types::SqlValue::Varchar(s) | vibesql_types::SqlValue::Character(s) => {
                crate::evaluator::casting::string_to_number(s).1 != 0.0
            }
            _ => true,
        };

        if !is_truthy {
            return Ok(vec![]);
        }

        let result = vec![result_row];
        let limit = evaluate_limit(&stmt.limit, self.database)?;
        let offset = evaluate_offset(&stmt.offset, self.database)?;
        Ok(apply_limit_offset(result, limit, offset))
    }

    /// Execute SELECT without FROM clause that contains window functions
    ///
    /// Window functions without FROM clause operate on a single implicit row.
    /// For example: `SELECT min(1) OVER()` returns 1 because the window frame
    /// contains exactly one row (the implicit single row for queries without FROM).
    ///
    /// This is SQLite-compatible behavior where window functions can appear in
    /// scalar subqueries without FROM.
    fn execute_select_without_from_with_windows(
        &self,
        stmt: &vibesql_ast::SelectStmt,
        has_outer_context: bool,
        cte_ctx: Option<&HashMap<String, CteResult>>,
    ) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
        // Create an implicit single empty row for window function processing
        // This represents the single implicit row that exists for SELECT without FROM
        let implicit_row = vibesql_storage::Row::new(vec![]);
        let mut rows = vec![implicit_row];

        // Create evaluator for window function processing
        let empty_combined_schema = crate::schema::CombinedSchema::empty();
        let mut evaluator = if has_outer_context {
            let outer_row = self.outer_row.unwrap();
            let outer_schema = self.outer_schema.unwrap();
            CombinedExpressionEvaluator::with_database_and_outer_context(
                &empty_combined_schema,
                self.database,
                outer_row,
                outer_schema,
            )
        } else {
            CombinedExpressionEvaluator::with_database(&empty_combined_schema, self.database)
        };
        if let Some(cte) = cte_ctx {
            evaluator = evaluator.with_cte_context(cte);
        }

        // Process window functions - this adds computed values to each row
        // and returns a mapping from WindowFunctionKey to column index
        let (rows_with_windows, window_mapping) = evaluate_window_functions(
            rows,
            &stmt.select_list,
            stmt.window_definitions.as_ref(),
            &evaluator,
        )?;
        rows = rows_with_windows;

        // Now evaluate the SELECT list with the window mapping
        // The evaluator needs access to the window mapping to resolve WindowFunction expressions
        let mut values = Vec::new();
        for item in &stmt.select_list {
            match item {
                vibesql_ast::SelectItem::Wildcard { .. }
                | vibesql_ast::SelectItem::QualifiedWildcard { .. } => {
                    return Err(ExecutorError::UnsupportedFeature(
                        "SELECT * and qualified wildcards require FROM clause".to_string(),
                    ));
                }
                vibesql_ast::SelectItem::Expression { expr, .. } => {
                    // Create evaluator with window mapping for this expression
                    let mut eval_with_windows = if has_outer_context {
                        let outer_row = self.outer_row.unwrap();
                        let outer_schema = self.outer_schema.unwrap();
                        CombinedExpressionEvaluator::with_database_outer_and_windows(
                            &empty_combined_schema,
                            self.database,
                            outer_row,
                            outer_schema,
                            &window_mapping,
                        )
                    } else {
                        CombinedExpressionEvaluator::with_database_and_windows(
                            &empty_combined_schema,
                            self.database,
                            &window_mapping,
                        )
                    };
                    if let Some(cte) = cte_ctx {
                        eval_with_windows = eval_with_windows.with_cte_context(cte);
                    }
                    // Use the row with window values appended
                    let row = &rows[0];
                    let value = eval_with_windows.eval(expr, row)?;
                    values.push(value);
                }
            }
        }

        // Build result row
        let result = vec![vibesql_storage::Row::new(values)];

        // Apply LIMIT and OFFSET
        let limit = evaluate_limit(&stmt.limit, self.database)?;
        let offset = evaluate_offset(&stmt.offset, self.database)?;
        Ok(apply_limit_offset(result, limit, offset))
    }
}