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