vibesql_executor/evaluator/combined/
eval.rs

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