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}