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
//! Predicate pushdown and filtering logic
//!
//! Handles optimization of WHERE clause predicates by:
//! - Applying pre-decomposed table-local predicates early during table scans
//! - Reducing intermediate result sizes before joins
//!
//! **Phase 1 Optimization**: This module now uses `PredicatePlan` to avoid redundant
//! WHERE clause decomposition. The plan is computed once at query start and passed through.

use std::collections::HashMap;
#[cfg(feature = "parallel")]
use std::sync::Arc;

#[cfg(feature = "parallel")]
use rayon::prelude::*;

#[cfg(feature = "parallel")]
use crate::select::parallel::ParallelConfig;
use crate::{
    errors::ExecutorError, evaluator::CombinedExpressionEvaluator, optimizer::PredicatePlan,
    schema::CombinedSchema, select::cte::CteResult,
};

/// Apply table-local predicates from a pre-computed predicate plan
///
/// This function implements predicate pushdown by filtering rows early,
/// before they contribute to larger Cartesian products in JOINs.
///
/// Uses parallel filtering when beneficial based on row count and hardware.
///
/// **Phase 1**: Now accepts `PredicatePlan` instead of decomposing WHERE clause internally.
/// **Phase 2**: Attempts columnar execution for complex predicates with OR logic.
/// **Phase 4**: Uses cost-based predicate ordering via selectivity estimation.
/// **Issue #3562**: Added CTE context for IN subqueries referencing CTEs.
pub(crate) fn apply_table_local_predicates(
    rows: Vec<vibesql_storage::Row>,
    schema: CombinedSchema,
    predicate_plan: &PredicatePlan,
    table_name: &str,
    database: &vibesql_storage::Database,
    outer_row: Option<&vibesql_storage::Row>,
    outer_schema: Option<&CombinedSchema>,
    cte_context: Option<&HashMap<String, CteResult>>,
) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
    // Get table statistics for selectivity-based ordering
    // If no statistics available, create fallback estimates based on schema
    let table_stats_owned = database.get_table(table_name).map(|table| {
        table.get_statistics().cloned().unwrap_or_else(|| {
            // Fallback: create estimated statistics from table schema
            // This enables cost-based optimization even without ANALYZE
            vibesql_storage::statistics::TableStatistics::estimate_from_schema(
                table.row_count(),
                &table.schema,
            )
        })
    });

    // Get predicates ordered by selectivity (most selective first)
    // Falls back to parse order if statistics unavailable
    let ordered_preds =
        predicate_plan.get_table_filters_ordered(table_name, table_stats_owned.as_ref());

    // If there are table-local predicates, apply them
    if !ordered_preds.is_empty() {
        // Combine ordered predicates with AND
        let combined_where = combine_predicates_with_and(ordered_preds);

        // Fast path: Try columnar execution for non-correlated queries
        // Threshold lowered to >=100 rows for better performance on SQLLogicTest queries
        if outer_row.is_none() && outer_schema.is_none() && rows.len() >= 100 {
            use crate::select::columnar::{create_filter_bitmap_tree, extract_predicate_tree};

            if let Some(predicate_tree) = extract_predicate_tree(&combined_where, &schema, database.case_sensitive_like()) {
                // Use columnar filtering
                let bitmap =
                    create_filter_bitmap_tree(rows.len(), &predicate_tree, |row_idx, col_idx| {
                        rows.get(row_idx).and_then(|row| row.get(col_idx))
                    })?;

                // Keep only rows that pass the filter
                let filtered_rows: Vec<_> = rows
                    .into_iter()
                    .zip(&bitmap)
                    .filter_map(|(row, &passes)| if passes { Some(row) } else { None })
                    .collect();

                return Ok(filtered_rows);
            }
        }

        // Create evaluator for filtering with outer context for correlated subqueries
        // Issue #3562: Add CTE context so IN subqueries can reference CTEs
        let evaluator = match (outer_row, outer_schema, cte_context) {
            (Some(outer_row), Some(outer_schema), Some(cte_ctx)) => {
                CombinedExpressionEvaluator::with_database_and_outer_context_and_cte(
                    &schema,
                    database,
                    outer_row,
                    outer_schema,
                    cte_ctx,
                )
            }
            (Some(outer_row), Some(outer_schema), None) => {
                CombinedExpressionEvaluator::with_database_and_outer_context(
                    &schema,
                    database,
                    outer_row,
                    outer_schema,
                )
            }
            (None, None, Some(cte_ctx)) => {
                CombinedExpressionEvaluator::with_database_and_cte(&schema, database, cte_ctx)
            }
            _ => CombinedExpressionEvaluator::with_database(&schema, database),
        };

        // Check if we should use parallel filtering
        #[cfg(feature = "parallel")]
        {
            let config = ParallelConfig::global();
            if config.should_parallelize_scan(rows.len()) {
                return apply_predicates_parallel(rows, combined_where, evaluator);
            }
        }

        // Sequential path for small datasets
        let mut filtered_rows = Vec::new();
        for row in rows {
            evaluator.clear_cse_cache();

            let include_row = match evaluator.eval(&combined_where, &row)? {
                vibesql_types::SqlValue::Boolean(true) => true,
                vibesql_types::SqlValue::Boolean(false) | vibesql_types::SqlValue::Null => false,
                // SQLLogicTest compatibility: treat integers as truthy/falsy (C-like behavior)
                vibesql_types::SqlValue::Integer(0) => false,
                vibesql_types::SqlValue::Integer(_) => true,
                vibesql_types::SqlValue::Smallint(0) => false,
                vibesql_types::SqlValue::Smallint(_) => true,
                vibesql_types::SqlValue::Bigint(0) => false,
                vibesql_types::SqlValue::Bigint(_) => true,
                vibesql_types::SqlValue::Float(0.0) => false,
                vibesql_types::SqlValue::Float(_) => true,
                vibesql_types::SqlValue::Real(0.0) => false,
                vibesql_types::SqlValue::Real(_) => true,
                vibesql_types::SqlValue::Double(0.0) => false,
                vibesql_types::SqlValue::Double(_) => true,
                vibesql_types::SqlValue::Numeric(n) if n == 0.0 => false,
                vibesql_types::SqlValue::Numeric(_) => true,
                // String types (SQLite coerces strings to numeric for boolean context)
                vibesql_types::SqlValue::Varchar(s) | vibesql_types::SqlValue::Character(s) => {
                    string_to_truthy(&s)
                }
                other => {
                    return Err(ExecutorError::InvalidWhereClause(format!(
                        "WHERE clause must evaluate to boolean, got: {:?}",
                        other
                    )))
                }
            };

            if include_row {
                filtered_rows.push(row);
            }
        }
        return Ok(filtered_rows);
    }

    // No table-local predicates - return rows as-is
    Ok(rows)
}

/// Apply predicates using parallel execution
///
/// Issue #3562: Now properly propagates CTE context to parallel threads,
/// enabling IN subqueries that reference CTEs during parallel filtering.
#[cfg(feature = "parallel")]
fn apply_predicates_parallel(
    rows: Vec<vibesql_storage::Row>,
    combined_where: vibesql_ast::Expression,
    evaluator: CombinedExpressionEvaluator,
) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
    // Clone expression for thread-safe sharing
    let where_expr_arc = Arc::new(combined_where);

    // Extract evaluator components for parallel execution (including CTE context)
    let (schema, database, outer_row, outer_schema, window_mapping, cte_context, enable_cse) =
        evaluator.get_parallel_components();

    // Use rayon's parallel iterator for filtering
    let result: Result<Vec<_>, ExecutorError> = rows
        .into_par_iter()
        .map(|row| {
            // Create thread-local evaluator with independent caches
            let thread_evaluator = CombinedExpressionEvaluator::from_parallel_components(
                schema,
                database,
                outer_row,
                outer_schema,
                window_mapping,
                cte_context,
                enable_cse,
            );

            // Evaluate predicate for this row
            let include_row = match thread_evaluator.eval(&where_expr_arc, &row)? {
                vibesql_types::SqlValue::Boolean(true) => true,
                vibesql_types::SqlValue::Boolean(false) | vibesql_types::SqlValue::Null => false,
                // SQLLogicTest compatibility: treat integers as truthy/falsy
                vibesql_types::SqlValue::Integer(0) => false,
                vibesql_types::SqlValue::Integer(_) => true,
                vibesql_types::SqlValue::Smallint(0) => false,
                vibesql_types::SqlValue::Smallint(_) => true,
                vibesql_types::SqlValue::Bigint(0) => false,
                vibesql_types::SqlValue::Bigint(_) => true,
                vibesql_types::SqlValue::Float(0.0) => false,
                vibesql_types::SqlValue::Float(_) => true,
                vibesql_types::SqlValue::Real(0.0) => false,
                vibesql_types::SqlValue::Real(_) => true,
                vibesql_types::SqlValue::Double(0.0) => false,
                vibesql_types::SqlValue::Double(_) => true,
                vibesql_types::SqlValue::Numeric(n) if n == 0.0 => false,
                vibesql_types::SqlValue::Numeric(_) => true,
                // String types (SQLite coerces strings to numeric for boolean context)
                vibesql_types::SqlValue::Varchar(s) | vibesql_types::SqlValue::Character(s) => {
                    string_to_truthy(&s)
                }
                other => {
                    return Err(ExecutorError::InvalidWhereClause(format!(
                        "WHERE clause must evaluate to boolean, got: {:?}",
                        other
                    )))
                }
            };

            if include_row {
                Ok(Some(row))
            } else {
                Ok(None)
            }
        })
        .collect();

    // Filter out None values and extract Ok rows
    result.map(|v| v.into_iter().flatten().collect())
}

/// Convert string to boolean using SQLite semantics
#[inline(always)]
fn string_to_truthy(s: &str) -> bool {
    if s.is_empty() {
        return false;
    }
    let trimmed = s.trim();
    if trimmed.is_empty() {
        return false;
    }
    // Parse leading numeric portion
    let mut end = 0;
    let mut has_dot = false;
    let mut has_digit = false;
    let chars: Vec<char> = trimmed.chars().collect();
    if !chars.is_empty() && (chars[0] == '-' || chars[0] == '+') {
        end = 1;
    }
    while end < chars.len() {
        let c = chars[end];
        if c.is_ascii_digit() {
            has_digit = true;
            end += 1;
        } else if c == '.' && !has_dot {
            has_dot = true;
            end += 1;
        } else {
            break;
        }
    }
    if !has_digit {
        return false;
    }
    let num_str: String = chars[..end].iter().collect();
    num_str.parse::<f64>().map(|n| n != 0.0).unwrap_or(false)
}

/// Helper function to combine predicates with AND operator
pub(crate) fn combine_predicates_with_and(
    mut predicates: Vec<vibesql_ast::Expression>,
) -> vibesql_ast::Expression {
    if predicates.is_empty() {
        // This shouldn't happen, but default to TRUE
        vibesql_ast::Expression::Literal(vibesql_types::SqlValue::Boolean(true))
    } else if predicates.len() == 1 {
        predicates.pop().unwrap()
    } else {
        let mut result = predicates.remove(0);
        for predicate in predicates {
            result = vibesql_ast::Expression::BinaryOp {
                op: vibesql_ast::BinaryOperator::And,
                left: Box::new(result),
                right: Box::new(predicate),
            };
        }
        result
    }
}

/// Filter-while-copy optimization for CTE scans
///
/// Unlike `apply_table_local_predicates` which takes ownership of rows,
/// this function takes a borrowed slice and only clones rows that pass
/// the filter. This is critical for CTE performance when a CTE is scanned
/// multiple times with different filters (e.g., TPC-DS Q4 with 6-way self-join).
///
/// **Issue #4199**: Avoids cloning all CTE rows (O(n)) when only a small
/// fraction pass the filter. For Q4, this reduces row clones from ~54,000
/// to ~3,000.
pub(crate) fn filter_and_clone_rows(
    rows: &[vibesql_storage::Row],
    schema: CombinedSchema,
    predicate_plan: &PredicatePlan,
    table_name: &str,
    database: &vibesql_storage::Database,
    cte_context: Option<&HashMap<String, CteResult>>,
) -> Result<Vec<vibesql_storage::Row>, ExecutorError> {
    // Get predicates ordered by selectivity (most selective first)
    let ordered_preds = predicate_plan.get_table_filters_ordered(table_name, None);

    // If there are no table-local predicates, clone all rows
    if ordered_preds.is_empty() {
        return Ok(rows.to_vec());
    }

    // Combine ordered predicates with AND
    let combined_where = combine_predicates_with_and(ordered_preds);

    // Fast path: Try columnar execution for larger datasets
    // Columnar still requires building a bitmap, but avoids per-row evaluator overhead
    if rows.len() >= 100 {
        use crate::select::columnar::{create_filter_bitmap_tree, extract_predicate_tree};

        if let Some(predicate_tree) = extract_predicate_tree(&combined_where, &schema, database.case_sensitive_like()) {
            // Use columnar filtering with bitmap
            let bitmap =
                create_filter_bitmap_tree(rows.len(), &predicate_tree, |row_idx, col_idx| {
                    rows.get(row_idx).and_then(|row| row.get(col_idx))
                })?;

            // Only clone rows that pass the filter
            let filtered_rows: Vec<_> = rows
                .iter()
                .zip(&bitmap)
                .filter_map(|(row, &passes)| if passes { Some(row.clone()) } else { None })
                .collect();

            return Ok(filtered_rows);
        }
    }

    // Create evaluator for filtering
    let evaluator = match cte_context {
        Some(cte_ctx) => {
            CombinedExpressionEvaluator::with_database_and_cte(&schema, database, cte_ctx)
        }
        None => CombinedExpressionEvaluator::with_database(&schema, database),
    };

    // Sequential path: evaluate each row and only clone if it passes
    let mut filtered_rows = Vec::new();
    for row in rows {
        evaluator.clear_cse_cache();

        let include_row = match evaluator.eval(&combined_where, row)? {
            vibesql_types::SqlValue::Boolean(true) => true,
            vibesql_types::SqlValue::Boolean(false) | vibesql_types::SqlValue::Null => false,
            // SQLLogicTest compatibility: treat integers as truthy/falsy (C-like behavior)
            vibesql_types::SqlValue::Integer(0) => false,
            vibesql_types::SqlValue::Integer(_) => true,
            vibesql_types::SqlValue::Smallint(0) => false,
            vibesql_types::SqlValue::Smallint(_) => true,
            vibesql_types::SqlValue::Bigint(0) => false,
            vibesql_types::SqlValue::Bigint(_) => true,
            vibesql_types::SqlValue::Float(0.0) => false,
            vibesql_types::SqlValue::Float(_) => true,
            vibesql_types::SqlValue::Real(0.0) => false,
            vibesql_types::SqlValue::Real(_) => true,
            vibesql_types::SqlValue::Double(0.0) => false,
            vibesql_types::SqlValue::Double(_) => true,
            vibesql_types::SqlValue::Numeric(n) if n == 0.0 => false,
            vibesql_types::SqlValue::Numeric(_) => true,
            // String types (SQLite coerces strings to numeric for boolean context)
            vibesql_types::SqlValue::Varchar(s) | vibesql_types::SqlValue::Character(s) => {
                string_to_truthy(&s)
            }
            other => {
                return Err(ExecutorError::InvalidWhereClause(format!(
                    "WHERE clause must evaluate to boolean, got: {:?}",
                    other
                )))
            }
        };

        if include_row {
            filtered_rows.push(row.clone());
        }
    }

    Ok(filtered_rows)
}