vibesql_executor/evaluator/expressions/
eval.rs

1//! Main evaluation entry point and basic expression types
2
3use vibesql_types::{SqlValue, TypeAffinity};
4
5use super::super::core::ExpressionEvaluator;
6use crate::errors::ExecutorError;
7
8/// Format a float value in SQLite-compatible text format.
9///
10/// SQLite ensures floats always have a decimal point when converted to text.
11/// For example, 10.0 becomes "10.0", not "10". This is important for type
12/// affinity comparisons where TEXT '10' should not equal REAL 10.0.
13fn format_float_for_text_comparison(n: f64) -> String {
14    if n.is_nan() {
15        return "NaN".to_string();
16    }
17    if n.is_infinite() {
18        return if n > 0.0 {
19            "Inf".to_string()
20        } else {
21            "-Inf".to_string()
22        };
23    }
24
25    // Use Rust's default formatting
26    let s = n.to_string();
27
28    // If the string doesn't contain a decimal point or 'e'/'E' (scientific notation),
29    // it's a whole number that needs ".0" appended to match SQLite behavior
30    if !s.contains('.') && !s.contains('e') && !s.contains('E') {
31        format!("{}.0", s)
32    } else {
33        s
34    }
35}
36
37impl ExpressionEvaluator<'_> {
38    /// Get the SQLite type affinity of an expression if it's a column reference.
39    ///
40    /// Returns Some(affinity) if the expression is a column reference and we can
41    /// determine its declared type from the schema. Returns None for literals,
42    /// function calls, and other non-column expressions.
43    ///
44    /// This is used to implement SQLite's type affinity rules for comparisons:
45    /// - TEXT column vs INTEGER literal → convert INTEGER to TEXT, string compare
46    /// - Bare column (NONE affinity) vs INTEGER → type ordering (TEXT > INTEGER)
47    pub(super) fn get_expression_affinity(
48        &self,
49        expr: &vibesql_ast::Expression,
50    ) -> Option<TypeAffinity> {
51        match expr {
52            vibesql_ast::Expression::ColumnRef(col_id) => {
53                // Look up the column in the schema to get its declared type
54                let column_name = col_id.column_canonical();
55                if let Some(col_idx) = self.schema.get_column_index(column_name) {
56                    let col_schema = &self.schema.columns[col_idx];
57                    Some(col_schema.data_type.sqlite_affinity())
58                } else {
59                    // Column not found in schema - treat as NONE affinity
60                    Some(TypeAffinity::None)
61                }
62            }
63            // For COLLATE expressions, get affinity of the inner expression
64            vibesql_ast::Expression::Collate { expr, .. } => self.get_expression_affinity(expr),
65            // Literals, functions, and other expressions don't have column affinity
66            _ => None,
67        }
68    }
69
70    /// Get the effective collation for an expression.
71    ///
72    /// Returns the collation from:
73    /// 1. Explicit COLLATE clause (highest priority)
74    /// 2. Column-level collation from CREATE TABLE definition
75    /// 3. None (use default binary collation)
76    ///
77    /// SQLite documentation states:
78    /// "A column's collating function can be specified using the COLLATE clause
79    /// in the column definition within the CREATE TABLE statement."
80    ///
81    /// Explicit COLLATE in the query overrides column-level collation.
82    pub(super) fn get_expression_collation(
83        &self,
84        expr: &vibesql_ast::Expression,
85    ) -> Option<String> {
86        match expr {
87            // Explicit COLLATE has highest priority
88            vibesql_ast::Expression::Collate { collation, .. } => Some(collation.clone()),
89            // Column reference - look up column's declared collation
90            vibesql_ast::Expression::ColumnRef(col_id) => {
91                let column_name = col_id.column_canonical();
92                if let Some(col_idx) = self.schema.get_column_index(column_name) {
93                    self.schema.columns[col_idx].collation.clone()
94                } else {
95                    None
96                }
97            }
98            // Other expressions don't have intrinsic collation
99            _ => None,
100        }
101    }
102
103    /// Check if an expression is a numeric literal (INTEGER or REAL).
104    pub(super) fn is_numeric_literal(&self, expr: &vibesql_ast::Expression) -> bool {
105        match expr {
106            vibesql_ast::Expression::Literal(val) => {
107                matches!(
108                    val,
109                    SqlValue::Integer(_)
110                        | SqlValue::Smallint(_)
111                        | SqlValue::Bigint(_)
112                        | SqlValue::Unsigned(_)
113                        | SqlValue::Float(_)
114                        | SqlValue::Real(_)
115                        | SqlValue::Double(_)
116                        | SqlValue::Numeric(_)
117                )
118            }
119            _ => false,
120        }
121    }
122
123    /// Check if an expression is a string literal (VARCHAR or CHAR).
124    pub(super) fn is_string_literal(&self, expr: &vibesql_ast::Expression) -> bool {
125        match expr {
126            vibesql_ast::Expression::Literal(val) => {
127                matches!(val, SqlValue::Varchar(_) | SqlValue::Character(_))
128            }
129            _ => false,
130        }
131    }
132
133    /// Try to convert a string SqlValue to a numeric SqlValue.
134    /// Returns the original value if the string doesn't look like a number.
135    /// This implements SQLite's NUMERIC affinity coercion rules.
136    fn try_coerce_string_to_numeric(val: &SqlValue) -> SqlValue {
137        match val {
138            SqlValue::Varchar(s) | SqlValue::Character(s) => {
139                let trimmed = s.trim();
140                // Try parsing as integer first
141                if let Ok(n) = trimmed.parse::<i64>() {
142                    return SqlValue::Integer(n);
143                }
144                // Try parsing as float (use Double for higher precision)
145                if let Ok(n) = trimmed.parse::<f64>() {
146                    return SqlValue::Double(n);
147                }
148                // Not a number - return original value
149                val.clone()
150            }
151            _ => val.clone(),
152        }
153    }
154
155    /// Apply SQLite affinity rules for comparisons.
156    ///
157    /// When comparing a TEXT-affinity column to an INTEGER literal, SQLite:
158    /// 1. Converts the INTEGER to TEXT
159    /// 2. Performs string comparison
160    ///
161    /// This function returns modified values based on affinity rules.
162    pub(super) fn apply_affinity_for_comparison(
163        &self,
164        left_expr: &vibesql_ast::Expression,
165        left_val: SqlValue,
166        right_expr: &vibesql_ast::Expression,
167        right_val: SqlValue,
168    ) -> (SqlValue, SqlValue) {
169        let left_affinity = self.get_expression_affinity(left_expr);
170        let right_affinity = self.get_expression_affinity(right_expr);
171
172        // Case 1: Left is TEXT column, right is numeric literal
173        // SQLite converts numeric literals to text for comparison with TEXT columns.
174        // Floats must preserve their decimal representation (10.0 → "10.0", not "10")
175        // so that TEXT '10' != REAL 10.0 (different string representations).
176        if left_affinity == Some(TypeAffinity::Text) && self.is_numeric_literal(right_expr) {
177            let right_as_text = match &right_val {
178                SqlValue::Integer(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
179                SqlValue::Smallint(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
180                SqlValue::Bigint(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
181                SqlValue::Unsigned(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
182                SqlValue::Float(n) => SqlValue::Varchar(arcstr::ArcStr::from(
183                    format_float_for_text_comparison(*n as f64),
184                )),
185                SqlValue::Real(n) => SqlValue::Varchar(arcstr::ArcStr::from(
186                    format_float_for_text_comparison(*n),
187                )),
188                SqlValue::Double(n) => SqlValue::Varchar(arcstr::ArcStr::from(
189                    format_float_for_text_comparison(*n),
190                )),
191                SqlValue::Numeric(n) => SqlValue::Varchar(arcstr::ArcStr::from(
192                    format_float_for_text_comparison(*n),
193                )),
194                _ => right_val,
195            };
196            return (left_val, right_as_text);
197        }
198
199        // Case 2: Right is TEXT column, left is numeric literal
200        // Same as Case 1 but symmetric - floats must preserve decimal representation.
201        if right_affinity == Some(TypeAffinity::Text) && self.is_numeric_literal(left_expr) {
202            let left_as_text = match &left_val {
203                SqlValue::Integer(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
204                SqlValue::Smallint(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
205                SqlValue::Bigint(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
206                SqlValue::Unsigned(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
207                SqlValue::Float(n) => SqlValue::Varchar(arcstr::ArcStr::from(
208                    format_float_for_text_comparison(*n as f64),
209                )),
210                SqlValue::Real(n) => SqlValue::Varchar(arcstr::ArcStr::from(
211                    format_float_for_text_comparison(*n),
212                )),
213                SqlValue::Double(n) => SqlValue::Varchar(arcstr::ArcStr::from(
214                    format_float_for_text_comparison(*n),
215                )),
216                SqlValue::Numeric(n) => SqlValue::Varchar(arcstr::ArcStr::from(
217                    format_float_for_text_comparison(*n),
218                )),
219                _ => left_val,
220            };
221            return (left_as_text, right_val);
222        }
223
224        // Case 3: Left is NUMERIC/INTEGER/REAL column, right is string literal
225        // Try to convert the string literal to a number for numeric comparison
226        // Per SQLite: NUMERIC affinity tries to convert strings to numbers if possible
227        let is_left_numeric_affinity = matches!(
228            left_affinity,
229            Some(TypeAffinity::Numeric) | Some(TypeAffinity::Integer) | Some(TypeAffinity::Real)
230        );
231        if is_left_numeric_affinity && self.is_string_literal(right_expr) {
232            let right_coerced = Self::try_coerce_string_to_numeric(&right_val);
233            return (left_val, right_coerced);
234        }
235
236        // Case 4: Right is NUMERIC/INTEGER/REAL column, left is string literal
237        // Try to convert the string literal to a number for numeric comparison
238        let is_right_numeric_affinity = matches!(
239            right_affinity,
240            Some(TypeAffinity::Numeric) | Some(TypeAffinity::Integer) | Some(TypeAffinity::Real)
241        );
242        if is_right_numeric_affinity && self.is_string_literal(left_expr) {
243            let left_coerced = Self::try_coerce_string_to_numeric(&left_val);
244            return (left_coerced, right_val);
245        }
246
247        // Case 5: Left is NUMERIC/INTEGER/REAL column, right is NONE/TEXT column with TEXT value
248        // Per SQLite: when one operand has NUMERIC affinity and the other has TEXT or NONE affinity,
249        // try to convert the TEXT value to a number for comparison.
250        // This handles column-to-column comparisons like: NUMERIC_col = NONE_col
251        let is_right_non_numeric_affinity = matches!(
252            right_affinity,
253            Some(TypeAffinity::None) | Some(TypeAffinity::Text) | None
254        );
255        if is_left_numeric_affinity
256            && is_right_non_numeric_affinity
257            && matches!(right_val, SqlValue::Varchar(_) | SqlValue::Character(_))
258        {
259            let right_coerced = Self::try_coerce_string_to_numeric(&right_val);
260            return (left_val, right_coerced);
261        }
262
263        // Case 6: Right is NUMERIC/INTEGER/REAL column, left is NONE/TEXT column with TEXT value
264        // Symmetric case of Case 5
265        let is_left_non_numeric_affinity = matches!(
266            left_affinity,
267            Some(TypeAffinity::None) | Some(TypeAffinity::Text) | None
268        );
269        if is_right_numeric_affinity
270            && is_left_non_numeric_affinity
271            && matches!(left_val, SqlValue::Varchar(_) | SqlValue::Character(_))
272        {
273            let left_coerced = Self::try_coerce_string_to_numeric(&left_val);
274            return (left_coerced, right_val);
275        }
276
277        // No affinity conversion needed - use original values
278        // This includes:
279        // - Bare columns (NONE affinity) vs bare columns (NONE affinity) → type ordering
280        // - TEXT column vs NONE column → type ordering (no numeric affinity involved)
281        // - Literal vs literal → type ordering (handled in compare)
282        // - Same-type comparisons → direct comparison
283        (left_val, right_val)
284    }
285
286    /// Apply SQLite type affinity rules for IN expression comparisons.
287    ///
288    /// IN expressions have different affinity rules than regular comparisons:
289    /// - For INTEGER columns, string literals are NOT coerced to integers
290    /// - For REAL columns, string literals ARE coerced to REAL
291    /// - For TEXT columns, numeric literals are converted to text
292    pub(super) fn apply_affinity_for_in_comparison(
293        &self,
294        left_expr: &vibesql_ast::Expression,
295        left_val: SqlValue,
296        right_expr: &vibesql_ast::Expression,
297        right_val: SqlValue,
298    ) -> (SqlValue, SqlValue) {
299        let left_affinity = self.get_expression_affinity(left_expr);
300        let right_affinity = self.get_expression_affinity(right_expr);
301
302        // Case 1: Left is TEXT column, right is numeric literal
303        if left_affinity == Some(TypeAffinity::Text) && self.is_numeric_literal(right_expr) {
304            let right_as_text = match &right_val {
305                SqlValue::Integer(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
306                SqlValue::Smallint(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
307                SqlValue::Bigint(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
308                SqlValue::Unsigned(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
309                SqlValue::Float(n) => SqlValue::Varchar(arcstr::ArcStr::from(
310                    format_float_for_text_comparison(*n as f64),
311                )),
312                SqlValue::Real(n) => SqlValue::Varchar(arcstr::ArcStr::from(
313                    format_float_for_text_comparison(*n),
314                )),
315                SqlValue::Double(n) => SqlValue::Varchar(arcstr::ArcStr::from(
316                    format_float_for_text_comparison(*n),
317                )),
318                SqlValue::Numeric(n) => SqlValue::Varchar(arcstr::ArcStr::from(
319                    format_float_for_text_comparison(*n),
320                )),
321                _ => right_val,
322            };
323            return (left_val, right_as_text);
324        }
325
326        // Case 2: Right is TEXT column, left is numeric literal
327        if right_affinity == Some(TypeAffinity::Text) && self.is_numeric_literal(left_expr) {
328            let left_as_text = match &left_val {
329                SqlValue::Integer(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
330                SqlValue::Smallint(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
331                SqlValue::Bigint(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
332                SqlValue::Unsigned(n) => SqlValue::Varchar(arcstr::ArcStr::from(n.to_string())),
333                SqlValue::Float(n) => SqlValue::Varchar(arcstr::ArcStr::from(
334                    format_float_for_text_comparison(*n as f64),
335                )),
336                SqlValue::Real(n) => SqlValue::Varchar(arcstr::ArcStr::from(
337                    format_float_for_text_comparison(*n),
338                )),
339                SqlValue::Double(n) => SqlValue::Varchar(arcstr::ArcStr::from(
340                    format_float_for_text_comparison(*n),
341                )),
342                SqlValue::Numeric(n) => SqlValue::Varchar(arcstr::ArcStr::from(
343                    format_float_for_text_comparison(*n),
344                )),
345                _ => left_val,
346            };
347            return (left_as_text, right_val);
348        }
349
350        // Case 3: Left is REAL column, right is string literal
351        // REAL affinity DOES coerce strings to numbers in IN expressions
352        if left_affinity == Some(TypeAffinity::Real) && self.is_string_literal(right_expr) {
353            let right_coerced = Self::try_coerce_string_to_numeric(&right_val);
354            return (left_val, right_coerced);
355        }
356
357        // Case 4: Right is REAL column, left is string literal
358        if right_affinity == Some(TypeAffinity::Real) && self.is_string_literal(left_expr) {
359            let left_coerced = Self::try_coerce_string_to_numeric(&left_val);
360            return (left_coerced, right_val);
361        }
362
363        // For IN expressions, INTEGER affinity does NOT coerce strings
364        // (unlike regular comparison). No affinity conversion needed.
365        (left_val, right_val)
366    }
367
368    /// Evaluate an expression in the context of a row
369    #[inline]
370    pub fn eval(
371        &self,
372        expr: &vibesql_ast::Expression,
373        row: &vibesql_storage::Row,
374    ) -> Result<vibesql_types::SqlValue, ExecutorError> {
375        // Check depth limit to prevent stack overflow from deeply nested expressions
376        if self.depth >= crate::limits::MAX_EXPRESSION_DEPTH {
377            return Err(ExecutorError::ExpressionDepthExceeded {
378                depth: self.depth,
379                max_depth: crate::limits::MAX_EXPRESSION_DEPTH,
380            });
381        }
382
383        // CSE: Check cache if enabled and expression is deterministic
384        if self.enable_cse
385            && super::super::expression_hash::ExpressionHasher::is_deterministic(expr)
386        {
387            let hash = super::super::expression_hash::ExpressionHasher::hash(expr);
388
389            // Check cache (get requires mut borrow to update LRU order)
390            if let Some(cached) = self.cse_cache.borrow_mut().get(&hash) {
391                return Ok(cached.clone());
392            }
393
394            // Evaluate with depth increment and cache result
395            let result = self.with_incremented_depth(|evaluator| evaluator.eval_impl(expr, row))?;
396            self.cse_cache.borrow_mut().put(hash, result.clone());
397            return Ok(result);
398        }
399
400        // Non-cached path: increment depth and evaluate
401        self.with_incremented_depth(|evaluator| evaluator.eval_impl(expr, row))
402    }
403
404    /// Internal implementation of eval with depth already incremented
405    #[inline]
406    fn eval_impl(
407        &self,
408        expr: &vibesql_ast::Expression,
409        row: &vibesql_storage::Row,
410    ) -> Result<vibesql_types::SqlValue, ExecutorError> {
411        match expr {
412            // Literals - just return the value
413            vibesql_ast::Expression::Literal(val) => Ok(val.clone()),
414
415            // DEFAULT keyword - not allowed in SELECT/WHERE expressions
416            vibesql_ast::Expression::Default => Err(ExecutorError::UnsupportedExpression(
417                "DEFAULT keyword is only valid in INSERT VALUES and UPDATE SET clauses".to_string(),
418            )),
419
420            // VALUES() function - not allowed in SELECT/WHERE expressions
421            vibesql_ast::Expression::DuplicateKeyValue { .. } => Err(ExecutorError::UnsupportedExpression(
422                "VALUES() function is only valid in ON DUPLICATE KEY UPDATE clauses".to_string(),
423            )),
424
425            // Column reference - look up column index and get value from row
426            vibesql_ast::Expression::ColumnRef(col_id) => {
427                self.eval_column_ref(col_id.schema_canonical(), col_id.table_canonical(), col_id.column_canonical(), row)
428            }
429
430            // Binary operations
431            vibesql_ast::Expression::BinaryOp { left, op, right } => {
432                // Short-circuit evaluation for AND/OR operators
433                match op {
434                    vibesql_ast::BinaryOperator::And => {
435                        let left_val = self.eval(left, row)?;
436                        // Short-circuit: if left is false, return false immediately
437                        match left_val {
438                            SqlValue::Boolean(false) => Ok(SqlValue::Boolean(false)),
439                            // For NULL and TRUE, must evaluate right side
440                            // SQL three-valued logic:
441                            // - NULL AND FALSE = FALSE (not NULL!)
442                            // - NULL AND TRUE = NULL
443                            // - TRUE AND x = x
444                            _ => {
445                                let right_val = self.eval(right, row)?;
446
447                                // Special case: NULL AND FALSE = FALSE
448                                if matches!(left_val, SqlValue::Null)
449                                    && matches!(right_val, SqlValue::Boolean(false))
450                                {
451                                    return Ok(SqlValue::Boolean(false));
452                                }
453
454                                self.eval_binary_op(&left_val, op, &right_val)
455                            }
456                        }
457                    }
458                    vibesql_ast::BinaryOperator::Or => {
459                        let left_val = self.eval(left, row)?;
460                        // Short-circuit: if left is true, return true immediately
461                        match left_val {
462                            SqlValue::Boolean(true) => Ok(SqlValue::Boolean(true)),
463                            // For NULL and FALSE, must evaluate right side
464                            // SQL three-valued logic:
465                            // - NULL OR TRUE = TRUE (not NULL!)
466                            // - NULL OR FALSE = NULL
467                            // - FALSE OR x = x
468                            _ => {
469                                let right_val = self.eval(right, row)?;
470
471                                // Special case: NULL OR TRUE = TRUE
472                                if matches!(left_val, SqlValue::Null)
473                                    && matches!(right_val, SqlValue::Boolean(true))
474                                {
475                                    return Ok(SqlValue::Boolean(true));
476                                }
477
478                                self.eval_binary_op(&left_val, op, &right_val)
479                            }
480                        }
481                    }
482                    // For all other operators, evaluate both sides as before
483                    _ => {
484                        // Check for COLLATE expressions on either side for comparison operators
485                        let is_comparison = matches!(
486                            op,
487                            vibesql_ast::BinaryOperator::Equal
488                                | vibesql_ast::BinaryOperator::NotEqual
489                                | vibesql_ast::BinaryOperator::LessThan
490                                | vibesql_ast::BinaryOperator::LessThanOrEqual
491                                | vibesql_ast::BinaryOperator::GreaterThan
492                                | vibesql_ast::BinaryOperator::GreaterThanOrEqual
493                        );
494
495                        // Handle row value constructor comparisons
496                        // SQL:1999 Section 7.1: Row value constructor comparison
497                        // (a, b) op (c, d) uses lexicographic ordering
498                        if is_comparison {
499                            if let (
500                                vibesql_ast::Expression::RowValueConstructor(left_values),
501                                vibesql_ast::Expression::RowValueConstructor(right_values),
502                            ) = (left.as_ref(), right.as_ref())
503                            {
504                                return self.eval_row_value_comparison(left_values, op, right_values, row);
505                            }
506                        }
507
508                        // Get effective collation from either side
509                        // Priority: explicit COLLATE > column-level collation
510                        // Check left side first, then right side
511                        let collation = if is_comparison {
512                            self.get_expression_collation(left)
513                                .or_else(|| self.get_expression_collation(right))
514                        } else {
515                            None
516                        };
517
518                        let left_val = self.eval(left, row)?;
519                        let right_val = self.eval(right, row)?;
520
521                        // Apply collation to string values if needed
522                        let (left_val, right_val) = if let Some(ref collation_name) = collation {
523                            let collation_lower = collation_name.to_lowercase();
524                            if collation_lower == "nocase" {
525                                // For NOCASE collation, uppercase both string values
526                                let left_transformed = match &left_val {
527                                    SqlValue::Varchar(s) => SqlValue::Varchar(arcstr::ArcStr::from(s.to_uppercase())),
528                                    SqlValue::Character(s) => SqlValue::Character(arcstr::ArcStr::from(s.to_uppercase())),
529                                    other => other.clone(),
530                                };
531                                let right_transformed = match &right_val {
532                                    SqlValue::Varchar(s) => SqlValue::Varchar(arcstr::ArcStr::from(s.to_uppercase())),
533                                    SqlValue::Character(s) => SqlValue::Character(arcstr::ArcStr::from(s.to_uppercase())),
534                                    other => other.clone(),
535                                };
536                                (left_transformed, right_transformed)
537                            } else if collation_lower == "rtrim" {
538                                // For RTRIM collation, trim trailing whitespace from both string values
539                                let left_transformed = match &left_val {
540                                    SqlValue::Varchar(s) => SqlValue::Varchar(arcstr::ArcStr::from(s.trim_end())),
541                                    SqlValue::Character(s) => SqlValue::Character(arcstr::ArcStr::from(s.trim_end())),
542                                    other => other.clone(),
543                                };
544                                let right_transformed = match &right_val {
545                                    SqlValue::Varchar(s) => SqlValue::Varchar(arcstr::ArcStr::from(s.trim_end())),
546                                    SqlValue::Character(s) => SqlValue::Character(arcstr::ArcStr::from(s.trim_end())),
547                                    other => other.clone(),
548                                };
549                                (left_transformed, right_transformed)
550                            } else {
551                                // For BINARY or other collations, use values as-is
552                                (left_val, right_val)
553                            }
554                        } else {
555                            (left_val, right_val)
556                        };
557
558                        // Apply SQLite type affinity rules for comparisons
559                        // TEXT column vs INTEGER literal → convert INTEGER to TEXT, string compare
560                        let (left_val, right_val) = if is_comparison {
561                            self.apply_affinity_for_comparison(left, left_val, right, right_val)
562                        } else {
563                            (left_val, right_val)
564                        };
565
566                        self.eval_binary_op(&left_val, op, &right_val)
567                    }
568                }
569            }
570
571            // CASE expression
572            vibesql_ast::Expression::Case { operand, when_clauses, else_result } => {
573                self.eval_case(operand, when_clauses, else_result, row)
574            }
575
576            // IN operator with subquery
577            vibesql_ast::Expression::In { expr, subquery, negated } => {
578                self.eval_in_subquery(expr, subquery, *negated, row)
579            }
580
581            // Scalar subquery
582            vibesql_ast::Expression::ScalarSubquery(subquery) => self.eval_scalar_subquery(subquery, row),
583
584            // BETWEEN predicate
585            vibesql_ast::Expression::Between { expr, low, high, negated, symmetric } => {
586                self.eval_between(expr, low, high, *negated, *symmetric, row)
587            }
588
589            // CAST expression
590            vibesql_ast::Expression::Cast { expr, data_type } => self.eval_cast(expr, data_type, row),
591
592            // POSITION expression
593            vibesql_ast::Expression::Position { substring, string, character_unit: _ } => {
594                self.eval_position(substring, string, row)
595            }
596
597            // TRIM expression
598            vibesql_ast::Expression::Trim { position, removal_char, string } => {
599                self.eval_trim(position, removal_char, string, row)
600            }
601
602            // EXTRACT expression
603            vibesql_ast::Expression::Extract { field, expr } => {
604                self.eval_extract(field, expr, row)
605            }
606
607            // LIKE pattern matching
608            vibesql_ast::Expression::Like { expr, pattern, negated, escape } => {
609                self.eval_like(expr, pattern, escape, *negated, row)
610            }
611
612            // GLOB pattern matching (SQLite)
613            vibesql_ast::Expression::Glob { expr, pattern, negated, .. } => {
614                self.eval_glob(expr, pattern, *negated, row)
615            }
616
617            // IN list (value list)
618            vibesql_ast::Expression::InList { expr, values, negated } => {
619                self.eval_in_list(expr, values, *negated, row)
620            }
621
622            // EXISTS predicate
623            vibesql_ast::Expression::Exists { subquery, negated } => {
624                self.eval_exists(subquery, *negated, row)
625            }
626
627            // Quantified comparison (ALL/ANY/SOME)
628            vibesql_ast::Expression::QuantifiedComparison { expr, op, quantifier, subquery } => {
629                self.eval_quantified(expr, op, quantifier, subquery, row)
630            }
631
632            // Function call
633            vibesql_ast::Expression::Function { name, args, character_unit } => {
634                self.eval_function(name.display(), args, character_unit, row)
635            }
636
637            // Current date/time functions
638            vibesql_ast::Expression::CurrentDate => {
639                let sql_mode = self.database.map(|db| db.sql_mode()).unwrap_or_default();
640                super::super::functions::eval_scalar_function("CURRENT_DATE", &[], &None, &sql_mode)
641            }
642            vibesql_ast::Expression::CurrentTime { precision: _ } => {
643                // For now, ignore precision and call existing function
644                // Phase 2 will implement precision-aware formatting
645                let sql_mode = self.database.map(|db| db.sql_mode()).unwrap_or_default();
646                super::super::functions::eval_scalar_function("CURRENT_TIME", &[], &None, &sql_mode)
647            }
648            vibesql_ast::Expression::CurrentTimestamp { precision: _ } => {
649                // For now, ignore precision and call existing function
650                // Phase 2 will implement precision-aware formatting
651                let sql_mode = self.database.map(|db| db.sql_mode()).unwrap_or_default();
652                super::super::functions::eval_scalar_function("CURRENT_TIMESTAMP", &[], &None, &sql_mode)
653            }
654
655            // INTERVAL expression
656            vibesql_ast::Expression::Interval {
657                value,
658                unit,
659                leading_precision: _,
660                fractional_precision: _,
661            } => {
662                // Evaluate the value expression (typically a string literal like '5')
663                let interval_value = self.eval(value, row)?;
664
665                // Convert unit to string for the Interval type
666                let unit_str = Self::interval_unit_to_string(unit);
667
668                // Create an Interval SqlValue
669                // The format is "value unit" (e.g., "5 DAY", "1-6 YEAR TO MONTH")
670                let interval_str = format!("{} {}", interval_value, unit_str);
671                Ok(SqlValue::Interval(vibesql_types::Interval::new(
672                    interval_str,
673                )))
674            }
675
676            // Unsupported expressions
677            vibesql_ast::Expression::Wildcard => Err(ExecutorError::UnsupportedExpression(
678                "Wildcard (*) not supported in expressions".to_string(),
679            )),
680
681            // Unary operations
682            vibesql_ast::Expression::UnaryOp { op, expr } => {
683                let val = self.eval(expr, row)?;
684                super::operators::eval_unary_op(op, &val)
685            }
686
687            vibesql_ast::Expression::IsNull { expr, negated } => {
688                let value = self.eval(expr, row)?;
689                let is_null = matches!(value, SqlValue::Null);
690                let result = if *negated { !is_null } else { is_null };
691                Ok(SqlValue::Boolean(result))
692            }
693
694            vibesql_ast::Expression::IsDistinctFrom { left, right, negated } => {
695                // Handle row value constructor IS [NOT] DISTINCT FROM
696                // (a, b) IS (c, d) is equivalent to (a, b) IS NOT DISTINCT FROM (c, d)
697                // SQLite: a IS b returns TRUE if both are NULL or both are equal (not NULL-propagating)
698                if let (
699                    vibesql_ast::Expression::RowValueConstructor(left_exprs),
700                    vibesql_ast::Expression::RowValueConstructor(right_exprs),
701                ) = (left.as_ref(), right.as_ref())
702                {
703                    return self.eval_row_value_is_distinct(left_exprs, right_exprs, *negated, row);
704                }
705
706                let left_val = self.eval(left, row)?;
707                let right_val = self.eval(right, row)?;
708                let is_distinct = super::super::core::values_are_distinct(&left_val, &right_val);
709                let result = if *negated { !is_distinct } else { is_distinct };
710                Ok(SqlValue::Boolean(result))
711            }
712
713            vibesql_ast::Expression::IsTruthValue { expr, truth_value, negated } => {
714                let val = self.eval(expr, row)?;
715                // SQL:1999 three-valued logic for IS TRUE/FALSE/UNKNOWN:
716                // - IS TRUE: TRUE if expr is TRUE, FALSE if expr is FALSE or UNKNOWN
717                // - IS FALSE: TRUE if expr is FALSE, FALSE if expr is TRUE or UNKNOWN
718                // - IS UNKNOWN: TRUE if expr is UNKNOWN (NULL), FALSE if expr is TRUE or FALSE
719                // - IS NOT X: negates the result
720                //
721                // SQLite compatibility: integers are treated as booleans
722                // - 0 is FALSE
723                // - Non-zero integers are TRUE
724                // - NULL is UNKNOWN
725                let result = match truth_value {
726                    vibesql_ast::TruthValue::True => match &val {
727                        SqlValue::Boolean(true) => true,
728                        SqlValue::Integer(n) => *n != 0,
729                        SqlValue::Bigint(n) => *n != 0,
730                        SqlValue::Smallint(n) => *n != 0,
731                        _ => false,
732                    },
733                    vibesql_ast::TruthValue::False => matches!(
734                        val,
735                        SqlValue::Boolean(false)
736                            | SqlValue::Integer(0)
737                            | SqlValue::Bigint(0)
738                            | SqlValue::Smallint(0)
739                    ),
740                    vibesql_ast::TruthValue::Unknown => matches!(val, SqlValue::Null),
741                };
742                let final_result = if *negated { !result } else { result };
743                Ok(SqlValue::Boolean(final_result))
744            }
745
746            vibesql_ast::Expression::WindowFunction { function, .. } => {
747                // Extract function name for SQLite-compatible error message
748                // Window functions in WHERE, GROUP BY, or HAVING clauses are misuse
749                let function_name = match function {
750                    vibesql_ast::WindowFunctionSpec::Aggregate { name, .. }
751                    | vibesql_ast::WindowFunctionSpec::Ranking { name, .. }
752                    | vibesql_ast::WindowFunctionSpec::Value { name, .. } => name.to_string(),
753                };
754                Err(ExecutorError::MisuseOfWindowFunction { function_name })
755            }
756
757            vibesql_ast::Expression::AggregateFunction { name, .. } => {
758                // SQLite-compatible error message for aggregate misuse in execution context
759                // This error occurs when an aggregate is evaluated outside of aggregation,
760                // such as in ORDER BY clauses of non-aggregate queries.
761                // Uses "misuse of aggregate: X()" format (with colon) to match SQLite's expr.c
762                Err(ExecutorError::MisuseOfAggregateContext { function_name: name.to_string() })
763            }
764
765            // NEXT VALUE FOR sequence expression
766            // TODO: Implement proper sequence evaluation
767            //
768            // Requirements for implementation:
769            // 1. Sequence catalog objects (CREATE SEQUENCE, DROP SEQUENCE, etc.)
770            // 2. Sequence state storage (current value, increment, min/max, cycle, etc.)
771            // 3. Mutable access to catalog to advance sequences (architectural change)
772            // 4. Thread-safe sequence value generation
773            //
774            // Current architecture has immutable database references in evaluator.
775            // Possible solutions:
776            // 1. Use RefCell<Sequence> or Arc<Mutex<Sequence>> for interior mutability
777            // 2. Handle NEXT VALUE FOR at statement execution level (INSERT/SELECT)
778            // 3. Change evaluator to accept mutable database reference
779            // 4. Use a separate sequence manager with thread-safe state
780            //
781            // Note: Parser and AST support already exists (Expression::NextValue).
782            // See SQL:1999 Section 6.13 for sequence expression specification.
783            vibesql_ast::Expression::NextValue { sequence_name } => {
784                Err(ExecutorError::UnsupportedExpression(format!(
785                    "NEXT VALUE FOR {} - Sequence expressions not yet implemented. \
786                    Requires sequence catalog infrastructure (CREATE SEQUENCE support), \
787                    sequence state management, and mutable catalog access. \
788                    Use auto-incrementing primary keys or generate values in application code instead.",
789                    sequence_name
790                )))
791            }
792
793            vibesql_ast::Expression::MatchAgainst { columns, search_modifier, mode } => {
794                self.eval_match_against(columns, search_modifier, mode, row)
795            }
796
797            // Pseudo-variable (OLD.column, NEW.column in triggers)
798            vibesql_ast::Expression::PseudoVariable { pseudo_table, column } => {
799                // Resolve pseudo-variable using trigger context
800                if let Some(ctx) = self.trigger_context {
801                    ctx.resolve_pseudo_var(*pseudo_table, column)
802                } else {
803                    // This expression type is only valid in trigger context
804                    // Return an error if encountered outside triggers
805                    Err(ExecutorError::UnsupportedExpression(
806                        format!(
807                            "Pseudo-variable {}.{} is only valid within trigger bodies",
808                            match pseudo_table {
809                                vibesql_ast::PseudoTable::Old => "OLD",
810                                vibesql_ast::PseudoTable::New => "NEW",
811                            },
812                            column
813                        )
814                    ))
815                }
816            }
817
818            // Session variable (@@sql_mode, @@version, etc.)
819            vibesql_ast::Expression::SessionVariable { name } => {
820                // Get session variable from database metadata
821                if let Some(db) = self.database {
822                    // Get the session variable value from the database metadata
823                    if let Some(value) = db.get_session_variable(name) {
824                        Ok(value.clone())
825                    } else {
826                        // Variable not found - return NULL (MySQL behavior)
827                        Ok(SqlValue::Null)
828                    }
829                } else {
830                    // No database context available
831                    Err(ExecutorError::UnsupportedExpression(
832                        format!("Session variable @@{} cannot be evaluated without database context", name)
833                    ))
834                }
835            }
836
837            // Placeholder (?) - must be bound before evaluation
838            vibesql_ast::Expression::Placeholder(idx) => {
839                Err(ExecutorError::UnsupportedExpression(
840                    format!("Unbound placeholder ?{} - placeholders must be bound to values before execution", idx)
841                ))
842            }
843
844            // Numbered placeholder ($1, $2, etc.) - must be bound before evaluation
845            vibesql_ast::Expression::NumberedPlaceholder(idx) => {
846                Err(ExecutorError::UnsupportedExpression(
847                    format!("Unbound numbered placeholder ${} - placeholders must be bound to values before execution", idx)
848                ))
849            }
850
851            // Named placeholder (:name) - must be bound before evaluation
852            vibesql_ast::Expression::NamedPlaceholder(name) => {
853                Err(ExecutorError::UnsupportedExpression(
854                    format!("Unbound named placeholder :{} - placeholders must be bound to values before execution", name)
855                ))
856            }
857
858            // Conjunction (AND) - evaluate all children with short-circuit
859            vibesql_ast::Expression::Conjunction(children) => {
860                let mut result = SqlValue::Boolean(true);
861                for child in children {
862                    let val = self.eval(child, row)?;
863                    match val {
864                        SqlValue::Boolean(false) => return Ok(SqlValue::Boolean(false)),
865                        SqlValue::Null => result = SqlValue::Null,
866                        SqlValue::Boolean(true) => {}
867                        _ => return Err(ExecutorError::TypeError(
868                            format!("Conjunction requires boolean operands, got {:?}", val)
869                        )),
870                    }
871                }
872                Ok(result)
873            }
874
875            // Disjunction (OR) - evaluate all children with short-circuit
876            vibesql_ast::Expression::Disjunction(children) => {
877                let mut result = SqlValue::Boolean(false);
878                for child in children {
879                    let val = self.eval(child, row)?;
880                    match val {
881                        SqlValue::Boolean(true) => return Ok(SqlValue::Boolean(true)),
882                        SqlValue::Null => result = SqlValue::Null,
883                        SqlValue::Boolean(false) => {}
884                        _ => return Err(ExecutorError::TypeError(
885                            format!("Disjunction requires boolean operands, got {:?}", val)
886                        )),
887                    }
888                }
889                Ok(result)
890            }
891
892            // Row value constructor - evaluate to a vector of values
893            // This is only reached when a row value appears outside a comparison context
894            // (e.g., in SELECT list which is not supported)
895            vibesql_ast::Expression::RowValueConstructor(_) => Err(ExecutorError::UnsupportedExpression(
896                "Row value constructors are only supported in comparison expressions".to_string(),
897            )),
898
899            // COLLATE expression - evaluate inner expression (collation affects string comparison)
900            // TODO: Full collation support - for now just evaluate the inner expression
901            vibesql_ast::Expression::Collate { expr, .. } => self.eval(expr, row),
902        }
903    }
904
905    /// Evaluate a MATCH...AGAINST full-text search expression
906    fn eval_match_against(
907        &self,
908        columns: &[String],
909        search_modifier: &vibesql_ast::Expression,
910        mode: &vibesql_ast::FulltextMode,
911        row: &vibesql_storage::Row,
912    ) -> Result<vibesql_types::SqlValue, ExecutorError> {
913        // Evaluate the search string
914        let search_value = self.eval(search_modifier, row)?;
915        let search_string: arcstr::ArcStr = match search_value {
916            SqlValue::Varchar(s) | SqlValue::Character(s) => s,
917            SqlValue::Null => return Ok(SqlValue::Boolean(false)),
918            other => arcstr::ArcStr::from(other.to_string().as_str()),
919        };
920
921        // Collect text values from the specified columns
922        let mut text_values: Vec<arcstr::ArcStr> = Vec::new();
923        for column_name in columns {
924            match self.eval_column_ref(None, None, column_name, row) {
925                Ok(SqlValue::Varchar(s)) | Ok(SqlValue::Character(s)) => text_values.push(s),
926                Ok(SqlValue::Null) => {
927                    // NULL values are treated as empty strings in MATCH
928                    text_values.push(arcstr::ArcStr::from(""));
929                }
930                Ok(other) => text_values.push(arcstr::ArcStr::from(other.to_string().as_str())),
931                Err(_) => {
932                    // Column not found - return false for this match
933                    return Ok(SqlValue::Boolean(false));
934                }
935            }
936        }
937
938        // Perform full-text search
939        let result = super::fulltext::eval_match_against(&search_string, &text_values, mode)?;
940        Ok(SqlValue::Boolean(result))
941    }
942
943    /// Evaluate column reference
944    #[inline]
945    fn eval_column_ref(
946        &self,
947        schema_qualifier: Option<&str>,
948        table_qualifier: Option<&str>,
949        column: &str,
950        row: &vibesql_storage::Row,
951    ) -> Result<vibesql_types::SqlValue, ExecutorError> {
952        // Handle schema qualifier (three-part names like schema.table.column)
953        // SQLite schemas: "main" (default), "temp" (temporary tables), or attached database names
954        // For our single-database implementation:
955        // - "main" is silently accepted (treated as default)
956        // - Other schemas return "no such column" error to match SQLite behavior
957        if let Some(schema) = schema_qualifier {
958            let schema_lower = schema.to_lowercase();
959            if schema_lower != "main" {
960                // SQLite returns "no such column: schema.table.column" for unknown schemas
961                return Err(ExecutorError::ColumnNotFound {
962                    column_name: format!("{}.{}.{}", schema, table_qualifier.unwrap_or(""), column),
963                    table_name: self.schema.name.clone(),
964                    searched_tables: vec![self.schema.name.clone()],
965                    available_columns: self.schema.columns.iter().map(|c| c.name.clone()).collect(),
966                });
967            }
968            // "main" schema - continue with normal resolution
969        }
970
971        // Special case: "*" is a wildcard used in COUNT(*) and is not a real column
972        // Return NULL here - the actual COUNT(*) logic handles this specially
973        if column == "*" {
974            return Ok(vibesql_types::SqlValue::Null);
975        }
976
977        // SQLite compatibility: Handle ROWID pseudo-column
978        // ROWID, _rowid_, and oid are aliases that return the row's unique identifier
979        // Note: We check real columns first - real columns take precedence over ROWID
980        // WITHOUT ROWID tables do NOT have the rowid pseudo-column (Issue #4953)
981        let column_lower = column.to_lowercase();
982        if column_lower == "rowid" || column_lower == "_rowid_" || column_lower == "oid" {
983            // First check if schema has a real column with this name
984            if self.schema.get_column_index(column).is_none() {
985                // WITHOUT ROWID tables do not have the rowid pseudo-column
986                if self.schema.without_rowid {
987                    return Err(ExecutorError::ColumnNotFound {
988                        column_name: column.to_string(),
989                        table_name: self.schema.name.clone(),
990                        searched_tables: vec![self.schema.name.clone()],
991                        available_columns: self
992                            .schema
993                            .columns
994                            .iter()
995                            .map(|c| c.name.clone())
996                            .collect(),
997                    });
998                }
999
1000                // Issue #4536: Check for INTEGER PRIMARY KEY alias column
1001                // If the table has an INTEGER PRIMARY KEY, it acts as an alias for rowid.
1002                // The column's value IS the rowid, so return that column's value.
1003                if let Some(ipk_col_idx) = self.schema.rowid_alias_column {
1004                    return row
1005                        .get(ipk_col_idx)
1006                        .cloned()
1007                        .ok_or(ExecutorError::ColumnIndexOutOfBounds { index: ipk_col_idx });
1008                }
1009
1010                // Use get_row_id_for_table to handle both single-table and multi-table (JOIN) rows
1011                // This fixes issue #4370 where qualified ROWIDs like `t1.rowid` returned NULL in JOINs
1012                if let Some(row_id) = row.get_row_id_for_table(table_qualifier) {
1013                    return Ok(vibesql_types::SqlValue::Bigint(row_id as i64));
1014                }
1015                // Fall back to evaluator's row_index (for older code paths)
1016                if let Some(row_index) = self.row_index {
1017                    return Ok(vibesql_types::SqlValue::Bigint(row_index as i64));
1018                }
1019                // ROWID not available - this happens for derived rows without ROWID tracking
1020                // Return NULL in this case (matching SQLite behavior for derived tables)
1021                return Ok(vibesql_types::SqlValue::Null);
1022            }
1023        }
1024
1025        // Check procedural context first (variables/parameters take precedence over table columns)
1026        // This is only checked when there's no table qualifier, as variables don't have table
1027        // prefixes
1028        if table_qualifier.is_none() {
1029            if let Some(proc_ctx) = self.procedural_context {
1030                // Try to get value from procedural context (checks variables then parameters)
1031                if let Some(value) = proc_ctx.get_value(column) {
1032                    return Ok(value.clone());
1033                }
1034            }
1035        }
1036
1037        // Track which tables we searched for better error messages
1038        let mut searched_tables = Vec::new();
1039        let mut available_columns = Vec::new();
1040
1041        // If table qualifier is provided, validate it matches a known schema
1042        if let Some(qualifier) = table_qualifier {
1043            let qualifier_lower = qualifier.to_lowercase();
1044            let inner_name_lower = self.schema.name.to_lowercase();
1045
1046            // Check if qualifier matches the table alias (SQLite extension: UPDATE t1 AS xyz)
1047            let alias_lower = self.table_alias.as_ref().map(|a| a.to_lowercase());
1048            let matches_alias = alias_lower.as_ref().is_some_and(|a| a == &qualifier_lower);
1049
1050            // Check if qualifier matches inner schema or table alias
1051            if qualifier_lower == inner_name_lower || matches_alias {
1052                // Qualifier matches inner schema or alias - search only there
1053                searched_tables.push(self.schema.name.clone());
1054                if let Some(col_index) = self.schema.get_column_index(column) {
1055                    return row
1056                        .get(col_index)
1057                        .cloned()
1058                        .ok_or(ExecutorError::ColumnIndexOutOfBounds { index: col_index });
1059                }
1060            } else if let Some(outer_schema) = self.outer_schema {
1061                let outer_name_lower = outer_schema.name.to_lowercase();
1062
1063                // Check if qualifier matches outer schema
1064                if qualifier_lower == outer_name_lower {
1065                    // Qualifier matches outer schema - search only there
1066                    if let Some(outer_row) = self.outer_row {
1067                        searched_tables.push(outer_schema.name.clone());
1068                        if let Some(col_index) = outer_schema.get_column_index(column) {
1069                            return outer_row
1070                                .get(col_index)
1071                                .cloned()
1072                                .ok_or(ExecutorError::ColumnIndexOutOfBounds { index: col_index });
1073                        }
1074                    }
1075                } else {
1076                    // Qualifier doesn't match any known schema
1077                    let mut known_tables = vec![self.schema.name.clone()];
1078                    if let Some(ref alias) = self.table_alias {
1079                        known_tables.push(alias.clone());
1080                    }
1081                    known_tables.push(outer_schema.name.clone());
1082
1083                    return Err(ExecutorError::InvalidTableQualifier {
1084                        qualifier: qualifier.to_string(),
1085                        column: column.to_string(),
1086                        available_tables: known_tables,
1087                    });
1088                }
1089            } else {
1090                // No outer schema and qualifier doesn't match inner schema
1091                let mut known_tables = vec![self.schema.name.clone()];
1092                if let Some(ref alias) = self.table_alias {
1093                    known_tables.push(alias.clone());
1094                }
1095                return Err(ExecutorError::InvalidTableQualifier {
1096                    qualifier: qualifier.to_string(),
1097                    column: column.to_string(),
1098                    available_tables: known_tables,
1099                });
1100            }
1101
1102            // If we get here, qualifier was valid but column wasn't found
1103            available_columns.extend(self.schema.columns.iter().map(|c| c.name.clone()));
1104            if let Some(outer_schema) = self.outer_schema {
1105                available_columns.extend(outer_schema.columns.iter().map(|c| c.name.clone()));
1106            }
1107
1108            return Err(ExecutorError::ColumnNotFound {
1109                column_name: column.to_string(),
1110                table_name: qualifier.to_string(),
1111                searched_tables,
1112                available_columns,
1113            });
1114        }
1115
1116        // No qualifier provided - use original search logic (inner first, then outer)
1117        // Try to resolve in inner schema first
1118        searched_tables.push(self.schema.name.clone());
1119        if let Some(col_index) = self.schema.get_column_index(column) {
1120            return row
1121                .get(col_index)
1122                .cloned()
1123                .ok_or(ExecutorError::ColumnIndexOutOfBounds { index: col_index });
1124        }
1125
1126        // If not found in inner schema and outer context exists, try outer schema
1127        if let (Some(outer_row), Some(outer_schema)) = (self.outer_row, self.outer_schema) {
1128            searched_tables.push(outer_schema.name.clone());
1129            if let Some(col_index) = outer_schema.get_column_index(column) {
1130                return outer_row
1131                    .get(col_index)
1132                    .cloned()
1133                    .ok_or(ExecutorError::ColumnIndexOutOfBounds { index: col_index });
1134            }
1135        }
1136
1137        // Column not found - collect available columns for suggestions
1138        available_columns.extend(self.schema.columns.iter().map(|c| c.name.clone()));
1139        if let Some(outer_schema) = self.outer_schema {
1140            available_columns.extend(outer_schema.columns.iter().map(|c| c.name.clone()));
1141        }
1142
1143        // Column not found in either schema
1144        Err(ExecutorError::ColumnNotFound {
1145            column_name: column.to_string(),
1146            table_name: table_qualifier.unwrap_or("unknown").to_string(),
1147            searched_tables,
1148            available_columns,
1149        })
1150    }
1151
1152    /// Convert IntervalUnit to string representation for Interval SqlValue
1153    fn interval_unit_to_string(unit: &vibesql_ast::IntervalUnit) -> String {
1154        use vibesql_ast::IntervalUnit;
1155        match unit {
1156            IntervalUnit::Microsecond => "MICROSECOND",
1157            IntervalUnit::Second => "SECOND",
1158            IntervalUnit::Minute => "MINUTE",
1159            IntervalUnit::Hour => "HOUR",
1160            IntervalUnit::Day => "DAY",
1161            IntervalUnit::Week => "WEEK",
1162            IntervalUnit::Month => "MONTH",
1163            IntervalUnit::Quarter => "QUARTER",
1164            IntervalUnit::Year => "YEAR",
1165            IntervalUnit::SecondMicrosecond => "SECOND_MICROSECOND",
1166            IntervalUnit::MinuteMicrosecond => "MINUTE_MICROSECOND",
1167            IntervalUnit::MinuteSecond => "MINUTE_SECOND",
1168            IntervalUnit::HourMicrosecond => "HOUR_MICROSECOND",
1169            IntervalUnit::HourSecond => "HOUR_SECOND",
1170            IntervalUnit::HourMinute => "HOUR_MINUTE",
1171            IntervalUnit::DayMicrosecond => "DAY_MICROSECOND",
1172            IntervalUnit::DaySecond => "DAY_SECOND",
1173            IntervalUnit::DayMinute => "DAY_MINUTE",
1174            IntervalUnit::DayHour => "DAY_HOUR",
1175            IntervalUnit::YearMonth => "YEAR_MONTH",
1176        }
1177        .to_string()
1178    }
1179
1180    /// Evaluate row value constructor comparison
1181    ///
1182    /// SQL:1999 Section 7.1: Row value constructor comparison
1183    /// Row values are compared element by element using lexicographic ordering:
1184    /// - (a, b) < (c, d) is true if a < c OR (a = c AND b < d)
1185    /// - (a, b) = (c, d) is true if a = c AND b = d
1186    /// - (a, b) <> (c, d) is true if a <> c OR b <> d
1187    ///
1188    /// NULL handling: If any element comparison involves NULL, the result follows
1189    /// SQL three-valued logic (NULL propagates unless short-circuited by a definite result).
1190    pub(super) fn eval_row_value_comparison(
1191        &self,
1192        left_exprs: &[vibesql_ast::Expression],
1193        op: &vibesql_ast::BinaryOperator,
1194        right_exprs: &[vibesql_ast::Expression],
1195        row: &vibesql_storage::Row,
1196    ) -> Result<SqlValue, ExecutorError> {
1197        // Row values must have the same number of elements
1198        if left_exprs.len() != right_exprs.len() {
1199            return Err(ExecutorError::UnsupportedExpression(format!(
1200                "Row value constructor size mismatch: left has {} elements, right has {}",
1201                left_exprs.len(),
1202                right_exprs.len()
1203            )));
1204        }
1205
1206        // Empty row values are not allowed
1207        if left_exprs.is_empty() {
1208            return Err(ExecutorError::UnsupportedExpression(
1209                "Empty row value constructors are not allowed".to_string(),
1210            ));
1211        }
1212
1213        // Evaluate all elements
1214        let mut left_values = Vec::with_capacity(left_exprs.len());
1215        let mut right_values = Vec::with_capacity(right_exprs.len());
1216
1217        for (left_expr, right_expr) in left_exprs.iter().zip(right_exprs.iter()) {
1218            left_values.push(self.eval(left_expr, row)?);
1219            right_values.push(self.eval(right_expr, row)?);
1220        }
1221
1222        // Perform comparison based on operator
1223        match op {
1224            vibesql_ast::BinaryOperator::Equal => {
1225                // (a, b) = (c, d) → a = c AND b = d
1226                // If any comparison is NULL, result is NULL (unless a definite FALSE)
1227                let mut has_null = false;
1228                for (left_val, right_val) in left_values.iter().zip(right_values.iter()) {
1229                    let cmp_result = self.eval_binary_op(left_val, op, right_val)?;
1230                    match cmp_result {
1231                        SqlValue::Boolean(false) => return Ok(SqlValue::Boolean(false)),
1232                        SqlValue::Null => has_null = true,
1233                        SqlValue::Boolean(true) => {}
1234                        _ => {
1235                            return Err(ExecutorError::TypeError(format!(
1236                                "Comparison returned non-boolean: {:?}",
1237                                cmp_result
1238                            )))
1239                        }
1240                    }
1241                }
1242                if has_null {
1243                    Ok(SqlValue::Null)
1244                } else {
1245                    Ok(SqlValue::Boolean(true))
1246                }
1247            }
1248
1249            vibesql_ast::BinaryOperator::NotEqual => {
1250                // (a, b) <> (c, d) → a <> c OR b <> d
1251                // If any comparison is TRUE, result is TRUE
1252                // If any comparison is NULL and no TRUE found, result is NULL
1253                let mut has_null = false;
1254                for (left_val, right_val) in left_values.iter().zip(right_values.iter()) {
1255                    let eq_result =
1256                        self.eval_binary_op(left_val, &vibesql_ast::BinaryOperator::Equal, right_val)?;
1257                    match eq_result {
1258                        SqlValue::Boolean(false) => return Ok(SqlValue::Boolean(true)), // Not equal found
1259                        SqlValue::Null => has_null = true,
1260                        SqlValue::Boolean(true) => {} // Equal, continue checking
1261                        _ => {
1262                            return Err(ExecutorError::TypeError(format!(
1263                                "Comparison returned non-boolean: {:?}",
1264                                eq_result
1265                            )))
1266                        }
1267                    }
1268                }
1269                if has_null {
1270                    Ok(SqlValue::Null)
1271                } else {
1272                    Ok(SqlValue::Boolean(false)) // All elements were equal
1273                }
1274            }
1275
1276            vibesql_ast::BinaryOperator::LessThan
1277            | vibesql_ast::BinaryOperator::LessThanOrEqual
1278            | vibesql_ast::BinaryOperator::GreaterThan
1279            | vibesql_ast::BinaryOperator::GreaterThanOrEqual => {
1280                // Lexicographic ordering: compare element by element
1281                // (a, b) < (c, d) → a < c OR (a = c AND b < d)
1282                self.eval_row_value_ordering(&left_values, op, &right_values)
1283            }
1284
1285            _ => Err(ExecutorError::UnsupportedExpression(format!(
1286                "Unsupported operator for row value comparison: {:?}",
1287                op
1288            ))),
1289        }
1290    }
1291
1292    /// Helper for lexicographic ordering comparison of row values
1293    ///
1294    /// For < and <=:
1295    /// (a, b) < (c, d) → a < c OR (a = c AND b < d)
1296    ///
1297    /// For > and >=:
1298    /// (a, b) > (c, d) → a > c OR (a = c AND b > d)
1299    fn eval_row_value_ordering(
1300        &self,
1301        left_values: &[SqlValue],
1302        op: &vibesql_ast::BinaryOperator,
1303        right_values: &[SqlValue],
1304    ) -> Result<SqlValue, ExecutorError> {
1305        let is_less = matches!(
1306            op,
1307            vibesql_ast::BinaryOperator::LessThan | vibesql_ast::BinaryOperator::LessThanOrEqual
1308        );
1309        let is_or_equal = matches!(
1310            op,
1311            vibesql_ast::BinaryOperator::LessThanOrEqual
1312                | vibesql_ast::BinaryOperator::GreaterThanOrEqual
1313        );
1314
1315        // The strict comparison operator (without the equality part)
1316        let strict_op = if is_less {
1317            vibesql_ast::BinaryOperator::LessThan
1318        } else {
1319            vibesql_ast::BinaryOperator::GreaterThan
1320        };
1321
1322        let eq_op = vibesql_ast::BinaryOperator::Equal;
1323
1324        // Track if we've seen NULL in any comparison
1325        let mut has_null = false;
1326
1327        // Compare element by element
1328        for i in 0..left_values.len() {
1329            let left_val = &left_values[i];
1330            let right_val = &right_values[i];
1331
1332            // First check if this element satisfies the strict inequality
1333            let strict_result = self.eval_binary_op(left_val, &strict_op, right_val)?;
1334            match strict_result {
1335                SqlValue::Boolean(true) => {
1336                    // Strict inequality satisfied at this position → result is TRUE
1337                    return Ok(SqlValue::Boolean(true));
1338                }
1339                SqlValue::Null => {
1340                    // NULL comparison - we can't determine result yet
1341                    has_null = true;
1342                }
1343                SqlValue::Boolean(false) => {
1344                    // Not strictly less/greater - check if equal
1345                    let eq_result = self.eval_binary_op(left_val, &eq_op, right_val)?;
1346                    match eq_result {
1347                        SqlValue::Boolean(true) => {
1348                            // Equal at this position - continue to next element
1349                        }
1350                        SqlValue::Boolean(false) => {
1351                            // Not equal and not strictly less/greater means strictly greater/less
1352                            // So the comparison fails
1353                            return Ok(SqlValue::Boolean(false));
1354                        }
1355                        SqlValue::Null => {
1356                            has_null = true;
1357                        }
1358                        _ => {
1359                            return Err(ExecutorError::TypeError(format!(
1360                                "Comparison returned non-boolean: {:?}",
1361                                eq_result
1362                            )))
1363                        }
1364                    }
1365                }
1366                _ => {
1367                    return Err(ExecutorError::TypeError(format!(
1368                        "Comparison returned non-boolean: {:?}",
1369                        strict_result
1370                    )))
1371                }
1372            }
1373        }
1374
1375        // We've compared all elements and they were all equal (no strict inequality found)
1376        if has_null {
1377            // NULL was encountered somewhere
1378            Ok(SqlValue::Null)
1379        } else if is_or_equal {
1380            // For <= or >=, all equal means TRUE
1381            Ok(SqlValue::Boolean(true))
1382        } else {
1383            // For < or >, all equal means FALSE
1384            Ok(SqlValue::Boolean(false))
1385        }
1386    }
1387
1388    /// Evaluate row value IS [NOT] DISTINCT FROM comparison
1389    ///
1390    /// In SQLite:
1391    /// - `(a, b) IS (c, d)` means `(a, b) IS NOT DISTINCT FROM (c, d)`
1392    ///   Returns TRUE if a IS c AND b IS d (NULL-safe equality, NULL IS NULL = TRUE)
1393    /// - `(a, b) IS NOT (c, d)` means `(a, b) IS DISTINCT FROM (c, d)`
1394    ///   Returns TRUE if any element is distinct (NULL IS NOT NULL = TRUE)
1395    ///
1396    /// Note: This differs from `=` comparison where NULL = NULL returns NULL
1397    fn eval_row_value_is_distinct(
1398        &self,
1399        left_exprs: &[vibesql_ast::Expression],
1400        right_exprs: &[vibesql_ast::Expression],
1401        negated: bool,
1402        row: &vibesql_storage::Row,
1403    ) -> Result<SqlValue, ExecutorError> {
1404        // Row values must have the same number of elements
1405        if left_exprs.len() != right_exprs.len() {
1406            return Err(ExecutorError::UnsupportedExpression(format!(
1407                "Row value constructor size mismatch: left has {} elements, right has {}",
1408                left_exprs.len(),
1409                right_exprs.len()
1410            )));
1411        }
1412
1413        // Empty row values are not allowed
1414        if left_exprs.is_empty() {
1415            return Err(ExecutorError::UnsupportedExpression(
1416                "Empty row value constructors are not allowed".to_string(),
1417            ));
1418        }
1419
1420        // Evaluate all elements and check distinctness
1421        // IS NOT DISTINCT FROM (negated=true in SQLite's IS syntax):
1422        //   All elements must be "not distinct" (NULL-safe equal)
1423        // IS DISTINCT FROM (negated=false in SQLite's IS NOT syntax):
1424        //   At least one element must be "distinct"
1425        for (left_expr, right_expr) in left_exprs.iter().zip(right_exprs.iter()) {
1426            let left_val = self.eval(left_expr, row)?;
1427            let right_val = self.eval(right_expr, row)?;
1428
1429            let is_distinct = super::super::core::values_are_distinct(&left_val, &right_val);
1430
1431            if negated {
1432                // IS NOT DISTINCT FROM: all must be not distinct
1433                // If any is distinct, return FALSE
1434                if is_distinct {
1435                    return Ok(SqlValue::Boolean(false));
1436                }
1437            } else {
1438                // IS DISTINCT FROM: if any is distinct, return TRUE
1439                if is_distinct {
1440                    return Ok(SqlValue::Boolean(true));
1441                }
1442            }
1443        }
1444
1445        // If we get here:
1446        // - For IS NOT DISTINCT FROM (negated=true): all were not distinct → TRUE
1447        // - For IS DISTINCT FROM (negated=false): none were distinct → FALSE
1448        Ok(SqlValue::Boolean(negated))
1449    }
1450}