Skip to main content

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