1use alloc::borrow::Cow;
19use alloc::format;
20use alloc::string::{String, ToString};
21use alloc::vec::Vec;
22
23use spg_sql::ast::{BinOp, ColumnName, Expr, Literal};
24use spg_storage::{ColumnSchema, Row, Value};
25
26mod binop;
27mod cast;
28mod compiled;
29mod datetime;
30mod encoding;
31mod format;
32mod functions;
33mod inet;
34mod math;
35mod regexp;
36mod resolve;
37mod strings;
38mod textsearch;
39mod values;
40
41pub(crate) use binop::{and_3vl, apply_binary_interval};
42use binop::{apply_binary, apply_unary, compare, pow10_i128};
43pub use cast::{cast_to_vector, cast_value, parse_vector_text};
44pub(crate) use compiled::{
45 CompiledExpr, compile_expr, eval_compiled, eval_compiled_ref, fully_compilable,
46};
47use datetime::{
48 age, date_format_mysql, date_part, date_trunc, extract_field, from_unixtime, unix_timestamp_of,
49};
50use encoding::{decode_text, encode_text};
51pub use format::{
52 days_from_civil, format_bigint_array, format_bytea_hex, format_date, format_int_array,
53 format_interval, format_money, format_numeric, format_text_array, format_time,
54 format_timestamp, format_timestamptz, format_timetz, parse_date_literal,
55 parse_timestamp_literal,
56};
57use functions::apply_function;
58use inet::{inet_host, inet_masklen, inet_network, inet_op_bool_result};
59pub(crate) use math::{f64_ceil, f64_floor, f64_sqrt};
60use math::{
61 f64_exp, f64_ln, f64_powi, f64_round_half_away, f64_trunc, prng_next_f64, prng_next_u64,
62};
63use regexp::{regexp_matches, regexp_replace, regexp_split_to_array};
64use resolve::{
65 collation_fold_for_compare, compare_is_case_insensitive, composite_eq, eval_expr_cow,
66 is_owned_compare_value, resolve_column, resolve_column_borrowed, text_prefix_chars,
67};
68pub(crate) use resolve::{column_collation, find_column_pos};
69use strings::{
70 TrimSide, format_string, pg_typeof_name, string_left_right, string_pad, string_trim, to_char,
71 value_to_format_text,
72};
73pub use textsearch::{
74 decode_tsquery_external, decode_tsvector_external, format_tsquery, format_tsvector,
75};
76use textsearch::{
77 fts_phraseto_tsquery, fts_plainto_tsquery, fts_setweight, fts_to_tsquery, fts_to_tsvector,
78 fts_ts_rank, fts_ts_rank_cd, fts_websearch_to_tsquery, ts_match, tsvector_concat,
79};
80pub use values::gen_random_uuid_bytes;
81use values::{value_cmp_for_min_max, value_to_f64, value_to_text, values_equal_for_nullif};
82
83#[derive(Clone)]
87#[allow(missing_debug_implementations)] pub struct EvalContext<'a> {
89 pub columns: &'a [ColumnSchema],
90 pub table_alias: Option<&'a str>,
91 pub params: &'a [Value],
96 pub default_text_search_config: Option<&'a str>,
103 pub sequence_resolver: Option<&'a SequenceResolver<'a>>,
109}
110
111pub type SequenceResolver<'a> = dyn Fn(SequenceOp) -> Result<i64, EvalError> + 'a;
116
117#[derive(Debug, Clone)]
119pub enum SequenceOp {
120 Next(String),
121 Curr(String),
122 Set {
123 name: String,
124 value: i64,
125 is_called: bool,
126 },
127}
128
129impl<'a> EvalContext<'a> {
130 pub const fn new(columns: &'a [ColumnSchema], table_alias: Option<&'a str>) -> Self {
131 Self {
132 columns,
133 table_alias,
134 params: &[],
135 default_text_search_config: None,
136 sequence_resolver: None,
137 }
138 }
139
140 #[must_use]
144 pub const fn with_sequence_resolver(mut self, resolver: &'a SequenceResolver<'a>) -> Self {
145 self.sequence_resolver = Some(resolver);
146 self
147 }
148
149 #[must_use]
153 pub const fn with_params(mut self, params: &'a [Value]) -> Self {
154 self.params = params;
155 self
156 }
157
158 #[must_use]
162 pub const fn with_default_text_search_config(mut self, cfg: Option<&'a str>) -> Self {
163 self.default_text_search_config = cfg;
164 self
165 }
166}
167
168#[derive(Debug, Clone, PartialEq)]
169pub enum EvalError {
170 ColumnNotFound {
171 name: String,
172 },
173 UnknownQualifier {
174 qualifier: String,
175 },
176 DivisionByZero,
177 TypeMismatch {
178 detail: String,
179 },
180 PlaceholderOutOfRange {
184 n: u16,
185 bound: u16,
186 },
187}
188
189impl core::fmt::Display for EvalError {
190 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
191 match self {
192 Self::ColumnNotFound { name } => write!(f, "column not found: {name}"),
193 Self::UnknownQualifier { qualifier } => {
194 write!(f, "unknown table qualifier: {qualifier}")
195 }
196 Self::DivisionByZero => f.write_str("division by zero"),
197 Self::TypeMismatch { detail } => write!(f, "type mismatch: {detail}"),
198 Self::PlaceholderOutOfRange { n, bound } => write!(
199 f,
200 "parameter ${n} referenced but only {bound} bound by client"
201 ),
202 }
203 }
204}
205
206pub fn eval_expr(expr: &Expr, row: &Row, ctx: &EvalContext<'_>) -> Result<Value, EvalError> {
207 match expr {
208 Expr::AggregateOrdered { .. } => Err(EvalError::TypeMismatch {
209 detail: "aggregate ORDER BY is only valid inside an aggregating SELECT".into(),
210 }),
211 Expr::Literal(l) => Ok(literal_to_value(l)),
212 Expr::Column(c) => resolve_column(c, row, ctx),
213 Expr::Placeholder(n) => {
214 let idx = usize::from(*n).saturating_sub(1);
215 ctx.params
216 .get(idx)
217 .cloned()
218 .ok_or_else(|| EvalError::PlaceholderOutOfRange {
219 n: *n,
220 bound: u16::try_from(ctx.params.len()).unwrap_or(u16::MAX),
221 })
222 }
223 Expr::Unary { op, expr } => {
224 let v = eval_expr(expr, row, ctx)?;
225 apply_unary(*op, v)
226 }
227 Expr::Binary { lhs, op, rhs } => {
228 if matches!(
240 op,
241 BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
242 ) {
243 let lc = eval_expr_cow(lhs, row, ctx)?;
244 let rc = eval_expr_cow(rhs, row, ctx)?;
245 let owned_path = is_owned_compare_value(lc.as_ref())
246 || is_owned_compare_value(rc.as_ref())
247 || compare_is_case_insensitive(lhs, rhs, ctx);
248 if !owned_path {
249 if lc.as_ref().is_null() || rc.as_ref().is_null() {
250 return Ok(Value::Null);
251 }
252 return compare(*op, lc.as_ref(), rc.as_ref());
253 }
254 let (l, r) = collation_fold_for_compare(
255 *op,
256 lhs,
257 rhs,
258 lc.into_owned(),
259 rc.into_owned(),
260 ctx,
261 );
262 return apply_binary(*op, l, r);
263 }
264 let l = eval_expr(lhs, row, ctx)?;
265 let r = eval_expr(rhs, row, ctx)?;
266 let (l, r) = collation_fold_for_compare(*op, lhs, rhs, l, r, ctx);
275 apply_binary(*op, l, r)
276 }
277 Expr::Cast { expr, target } => {
278 let v = eval_expr(expr, row, ctx)?;
279 cast_value(v, *target)
280 }
281 Expr::IsNull { expr, negated } => {
282 let v = eval_expr(expr, row, ctx)?;
283 let is_null = matches!(v, Value::Null);
284 Ok(Value::Bool(if *negated { !is_null } else { is_null }))
285 }
286 Expr::FunctionCall { name, args } => {
287 if args.len() == 2
293 && name.eq_ignore_ascii_case("left")
294 && let Expr::Column(c) = &args[0]
295 && let Some(cell) = resolve_column_borrowed(c, row, ctx)?
296 {
297 {
298 match cell {
299 Value::Null => return Ok(Value::Null),
300 Value::Text(t) => {
301 let n_v = eval_expr(&args[1], row, ctx)?;
302 if let Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) = n_v {
303 let n = match n_v {
304 Value::SmallInt(x) => i64::from(x),
305 Value::Int(x) => i64::from(x),
306 Value::BigInt(x) => x,
307 _ => 0,
308 };
309 return Ok(Value::Text(text_prefix_chars(t, n)));
310 }
311 }
312 _ => {}
313 }
314 }
315 }
316 let evaluated: Result<Vec<Value>, _> =
317 args.iter().map(|a| eval_expr(a, row, ctx)).collect();
318 apply_function(name, &evaluated?, ctx)
319 }
320 Expr::Like {
321 expr,
322 pattern,
323 negated,
324 case_insensitive,
325 } => {
326 let v = eval_expr(expr, row, ctx)?;
327 let p = eval_expr(pattern, row, ctx)?;
328 let (text, pat) = match (v, p) {
330 (Value::Null, _) | (_, Value::Null) => return Ok(Value::Null),
331 (Value::Text(a), Value::Text(b)) => (a, b),
332 (Value::Text(_), other) | (other, _) => {
333 return Err(EvalError::TypeMismatch {
334 detail: format!("LIKE requires text operands, got {:?}", other.data_type()),
335 });
336 }
337 };
338 let m = if *case_insensitive {
341 like_match(&text.to_lowercase(), &pat.to_lowercase())
342 } else {
343 like_match(&text, &pat)
344 };
345 Ok(Value::Bool(if *negated { !m } else { m }))
346 }
347 Expr::Extract { field, source } => {
348 let v = eval_expr(source, row, ctx)?;
349 extract_field(*field, &v)
350 }
351 Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => {
355 Err(EvalError::TypeMismatch {
356 detail: "subquery reached row eval — engine resolver bug".into(),
357 })
358 }
359 Expr::InList {
366 expr,
367 list,
368 negated,
369 } => {
370 let needle = eval_expr(expr, row, ctx)?;
371 let needle_null = matches!(needle, Value::Null);
372 let mut saw_null = needle_null && !list.is_empty();
373 let mut matched = false;
374 if !needle_null {
375 for item in list {
376 let v = eval_expr(item, row, ctx)?;
377 if matches!(v, Value::Null) {
378 saw_null = true;
379 continue;
380 }
381 match apply_binary(BinOp::Eq, needle.clone(), v)? {
382 Value::Bool(true) => {
383 matched = true;
384 break;
385 }
386 Value::Bool(false) => {}
387 Value::Null => saw_null = true,
388 other => {
389 return Err(EvalError::TypeMismatch {
390 detail: format!(
391 "IN comparison didn't return Bool: {:?}",
392 other.data_type()
393 ),
394 });
395 }
396 }
397 }
398 }
399 let inner = if matched {
400 Value::Bool(true)
401 } else if saw_null {
402 Value::Null
403 } else {
404 Value::Bool(false)
405 };
406 Ok(match (negated, inner) {
407 (true, Value::Bool(b)) => Value::Bool(!b),
408 (_, v) => v,
409 })
410 }
411 Expr::WindowFunction { .. } => Err(EvalError::TypeMismatch {
416 detail: "window function reached row eval — engine rewrite bug".into(),
417 }),
418 Expr::Array(items) => {
424 let mut materialised: Vec<Value> = Vec::with_capacity(items.len());
425 for elem in items {
426 materialised.push(eval_expr(elem, row, ctx)?);
427 }
428 let mut has_text = false;
429 let mut has_bigint = false;
430 let mut has_int = false;
431 for v in &materialised {
432 match v {
433 Value::Null => {}
434 Value::Int(_) | Value::SmallInt(_) => has_int = true,
435 Value::BigInt(_) => has_bigint = true,
436 Value::Text(_) | Value::Json(_) => has_text = true,
437 _ => has_text = true,
438 }
439 }
440 if has_text || (!has_int && !has_bigint) {
441 let out: Vec<Option<String>> = materialised
442 .into_iter()
443 .map(|v| match v {
444 Value::Null => None,
445 Value::Text(s) | Value::Json(s) => Some(s),
446 other => Some(value_to_text_for_array(&other)),
447 })
448 .collect();
449 return Ok(Value::TextArray(out));
450 }
451 if has_bigint {
452 let out: Vec<Option<i64>> = materialised
453 .into_iter()
454 .map(|v| match v {
455 Value::Null => None,
456 Value::Int(n) => Some(i64::from(n)),
457 Value::SmallInt(n) => Some(i64::from(n)),
458 Value::BigInt(n) => Some(n),
459 _ => unreachable!(),
460 })
461 .collect();
462 return Ok(Value::BigIntArray(out));
463 }
464 let out: Vec<Option<i32>> = materialised
465 .into_iter()
466 .map(|v| match v {
467 Value::Null => None,
468 Value::Int(n) => Some(n),
469 Value::SmallInt(n) => Some(i32::from(n)),
470 _ => unreachable!(),
471 })
472 .collect();
473 Ok(Value::IntArray(out))
474 }
475 Expr::ArraySubscript { target, index } => {
478 let target_v = eval_expr(target, row, ctx)?;
479 let idx_v = eval_expr(index, row, ctx)?;
480 if matches!(target_v, Value::Null) || matches!(idx_v, Value::Null) {
481 return Ok(Value::Null);
482 }
483 let i: i64 = match idx_v {
484 Value::Int(n) => i64::from(n),
485 Value::BigInt(n) => n,
486 Value::SmallInt(n) => i64::from(n),
487 other => {
488 return Err(EvalError::TypeMismatch {
489 detail: format!(
490 "array subscript must be integer, got {:?}",
491 other.data_type()
492 ),
493 });
494 }
495 };
496 if i < 1 {
497 return Ok(Value::Null);
498 }
499 let pos = (i - 1) as usize;
500 match target_v {
501 Value::TextArray(items) => match items.get(pos) {
502 Some(Some(s)) => Ok(Value::Text(s.clone())),
503 Some(None) | None => Ok(Value::Null),
504 },
505 Value::IntArray(items) => match items.get(pos) {
506 Some(Some(n)) => Ok(Value::Int(*n)),
507 Some(None) | None => Ok(Value::Null),
508 },
509 Value::BigIntArray(items) => match items.get(pos) {
510 Some(Some(n)) => Ok(Value::BigInt(*n)),
511 Some(None) | None => Ok(Value::Null),
512 },
513 other => Err(EvalError::TypeMismatch {
514 detail: format!(
515 "subscript target must be an array, got {:?}",
516 other.data_type()
517 ),
518 }),
519 }
520 }
521 Expr::AnyAll {
527 expr,
528 op,
529 array,
530 is_any,
531 } => {
532 let lhs = eval_expr(expr, row, ctx)?;
533 let arr = eval_expr(array, row, ctx)?;
534 if matches!(arr, Value::Null) {
535 return Ok(Value::Null);
536 }
537 let elems: Vec<Option<Value>> = match arr {
538 Value::TextArray(items) => items.into_iter().map(|o| o.map(Value::Text)).collect(),
539 Value::IntArray(items) => items.into_iter().map(|o| o.map(Value::Int)).collect(),
540 Value::BigIntArray(items) => {
541 items.into_iter().map(|o| o.map(Value::BigInt)).collect()
542 }
543 other => {
544 return Err(EvalError::TypeMismatch {
545 detail: format!(
546 "ANY/ALL right-hand side must be an array, got {:?}",
547 other.data_type()
548 ),
549 });
550 }
551 };
552 let mut saw_null = matches!(lhs, Value::Null);
553 let mut saw_match = false;
554 let mut saw_mismatch = false;
555 for elem in elems {
556 let elem_v = match elem {
557 Some(v) => v,
558 None => {
559 saw_null = true;
560 continue;
561 }
562 };
563 if matches!(lhs, Value::Null) {
564 saw_null = true;
565 continue;
566 }
567 match apply_binary(*op, lhs.clone(), elem_v) {
568 Ok(Value::Bool(true)) => saw_match = true,
569 Ok(Value::Bool(false)) => saw_mismatch = true,
570 Ok(Value::Null) => saw_null = true,
571 Ok(other) => {
572 return Err(EvalError::TypeMismatch {
573 detail: format!(
574 "ANY/ALL comparison didn't return Bool: {:?}",
575 other.data_type()
576 ),
577 });
578 }
579 Err(e) => return Err(e),
580 }
581 }
582 let result = if *is_any {
583 if saw_match {
584 Value::Bool(true)
585 } else if saw_null {
586 Value::Null
587 } else {
588 Value::Bool(false)
589 }
590 } else if saw_mismatch {
591 Value::Bool(false)
592 } else if saw_null {
593 Value::Null
594 } else {
595 Value::Bool(true)
596 };
597 Ok(result)
598 }
599 Expr::Case {
605 operand,
606 branches,
607 else_branch,
608 } => {
609 let operand_value = match operand {
610 Some(o) => Some(eval_expr(o, row, ctx)?),
611 None => None,
612 };
613 for (when_expr, then_expr) in branches {
614 let when_value = eval_expr(when_expr, row, ctx)?;
615 let matched = match &operand_value {
616 None => matches!(when_value, Value::Bool(true)),
617 Some(op_v) => matches!(
618 apply_binary(spg_sql::ast::BinOp::Eq, op_v.clone(), when_value)?,
619 Value::Bool(true)
620 ),
621 };
622 if matched {
623 return eval_expr(then_expr, row, ctx);
624 }
625 }
626 match else_branch {
627 Some(e) => eval_expr(e, row, ctx),
628 None => Ok(Value::Null),
629 }
630 }
631 }
632}
633
634fn value_to_text_for_array(v: &Value) -> String {
641 match v {
642 Value::Text(s) | Value::Json(s) => s.clone(),
643 Value::Int(n) => n.to_string(),
644 Value::BigInt(n) => n.to_string(),
645 Value::SmallInt(n) => n.to_string(),
646 Value::Bool(b) => {
647 if *b {
648 "true".into()
649 } else {
650 "false".into()
651 }
652 }
653 Value::Float(x) => format!("{x}"),
654 Value::Date(d) => format_date(*d),
655 Value::Timestamp(t) => format_timestamp(*t),
656 Value::Numeric { scaled, scale } => format_numeric(*scaled, *scale),
657 _ => format!("{v:?}"),
658 }
659}
660
661fn like_match(text: &str, pattern: &str) -> bool {
666 let text: Vec<char> = text.chars().collect();
667 let pat: Vec<char> = pattern.chars().collect();
668 like_match_inner(&text, 0, &pat, 0)
669}
670
671fn like_match_inner(text: &[char], mut ti: usize, pat: &[char], mut pi: usize) -> bool {
672 while pi < pat.len() {
673 match pat[pi] {
674 '%' => {
675 while pi < pat.len() && pat[pi] == '%' {
677 pi += 1;
678 }
679 if pi == pat.len() {
680 return true;
681 }
682 for k in ti..=text.len() {
683 if like_match_inner(text, k, pat, pi) {
684 return true;
685 }
686 }
687 return false;
688 }
689 '_' => {
690 if ti >= text.len() {
691 return false;
692 }
693 ti += 1;
694 pi += 1;
695 }
696 '\\' if pi + 1 < pat.len() => {
697 let want = pat[pi + 1];
698 if ti >= text.len() || text[ti] != want {
699 return false;
700 }
701 ti += 1;
702 pi += 2;
703 }
704 c => {
705 if ti >= text.len() || text[ti] != c {
706 return false;
707 }
708 ti += 1;
709 pi += 1;
710 }
711 }
712 }
713 ti == text.len()
714}
715
716fn fn_string_to_array(args: &[Value]) -> Result<Value, EvalError> {
718 let [text_arg, delim_arg] = args else {
719 return Err(EvalError::TypeMismatch {
720 detail: alloc::format!("string_to_array expects 2 arguments, got {}", args.len()),
721 });
722 };
723 let text = match text_arg {
724 Value::Null => return Ok(Value::Null),
725 Value::Text(t) => t,
726 other => {
727 return Err(EvalError::TypeMismatch {
728 detail: alloc::format!("string_to_array expects text, got {:?}", other.data_type()),
729 });
730 }
731 };
732 if text.is_empty() {
734 return Ok(Value::TextArray(Vec::new()));
735 }
736 let parts: Vec<Option<String>> = match delim_arg {
737 Value::Null => text.chars().map(|c| Some(c.to_string())).collect(),
739 Value::Text(d) if d.is_empty() => alloc::vec![Some(text.clone())],
740 Value::Text(d) => text
741 .split(d.as_str())
742 .map(|p| Some(p.to_string()))
743 .collect(),
744 other => {
745 return Err(EvalError::TypeMismatch {
746 detail: alloc::format!(
747 "string_to_array delimiter must be text, got {:?}",
748 other.data_type()
749 ),
750 });
751 }
752 };
753 Ok(Value::TextArray(parts))
754}
755
756fn error_on_null(args: &[Value]) -> Result<Value, EvalError> {
760 if args.len() != 1 {
761 return Err(EvalError::TypeMismatch {
762 detail: format!("error_on_null() takes 1 arg, got {}", args.len()),
763 });
764 }
765 if matches!(args[0], Value::Null) {
766 return Err(EvalError::TypeMismatch {
767 detail: "error_on_null(): argument is NULL".into(),
768 });
769 }
770 Ok(args[0].clone())
771}
772
773fn text_arg(v: &Value) -> Result<Option<String>, EvalError> {
776 match v {
777 Value::Text(s) => Ok(Some(s.clone())),
778 Value::Null => Ok(None),
779 other => Err(EvalError::TypeMismatch {
780 detail: alloc::format!(
781 "regex function expects TEXT arg, got {:?}",
782 other.data_type()
783 ),
784 }),
785 }
786}
787
788const MONTH_FULL: [&str; 12] = [
793 "January",
794 "February",
795 "March",
796 "April",
797 "May",
798 "June",
799 "July",
800 "August",
801 "September",
802 "October",
803 "November",
804 "December",
805];
806const MONTH_ABBR: [&str; 12] = [
807 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
808];
809
810#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
816fn civil_from_days(days: i32) -> (i32, u32, u32) {
817 let z = i64::from(days) + 719_468;
818 let era = z.div_euclid(146_097);
819 let doe = (z - era * 146_097) as u32;
823 let yoe = (doe.saturating_sub(doe / 1460) + doe / 36524 - doe / 146_096) / 365;
824 let y_base = i64::from(yoe) + era * 400;
825 let doy = doe.saturating_sub(365 * yoe + yoe / 4 - yoe / 100);
826 let mp = (5 * doy + 2) / 153;
827 let d = doy.saturating_sub((153 * mp + 2) / 5) + 1;
828 let m = if mp < 10 { mp + 3 } else { mp - 9 };
829 let y = if m <= 2 { y_base + 1 } else { y_base };
830 (y as i32, m, d)
831}
832
833fn add_months_to_civil(y: i32, m: u32, d: u32, months: i32) -> (i32, u32, u32) {
836 let total_months = i64::from(y) * 12 + i64::from(m) - 1 + i64::from(months);
837 let new_year = i32::try_from(total_months.div_euclid(12)).unwrap_or(i32::MAX);
838 let new_month_zero = total_months.rem_euclid(12);
839 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
840 let new_month = (new_month_zero as u32) + 1;
841 let max_day = days_in_month(new_year, new_month);
842 (new_year, new_month, d.min(max_day))
843}
844
845const fn days_in_month(y: i32, m: u32) -> u32 {
846 match m {
847 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
848 2 => {
849 if y.rem_euclid(4) == 0 && (y.rem_euclid(100) != 0 || y.rem_euclid(400) == 0) {
851 29
852 } else {
853 28
854 }
855 }
856 _ => 30,
859 }
860}
861
862pub(crate) fn literal_to_value(l: &Literal) -> Value {
863 match l {
864 Literal::Integer(n) => {
865 if let Ok(small) = i32::try_from(*n) {
866 Value::Int(small)
867 } else {
868 Value::BigInt(*n)
869 }
870 }
871 Literal::Float(x) => Value::Float(*x),
872 Literal::String(s) => Value::Text(s.clone()),
873 Literal::Vector(v) => Value::Vector(v.clone()),
874 Literal::TextArray(items) => Value::TextArray(items.clone()),
875 Literal::IntArray(items) => Value::IntArray(items.clone()),
876 Literal::BigIntArray(items) => Value::BigIntArray(items.clone()),
877 Literal::Bool(b) => Value::Bool(*b),
878 Literal::Null => Value::Null,
879 Literal::Interval { months, micros, .. } => Value::Interval {
880 months: *months,
881 micros: *micros,
882 },
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889 use alloc::vec;
890 use spg_sql::ast::UnOp;
891 use spg_storage::{ColumnSchema, DataType, Row};
892
893 fn col(name: &str, ty: DataType) -> ColumnSchema {
894 ColumnSchema::new(name, ty, true)
895 }
896
897 fn ctx<'a>(cols: &'a [ColumnSchema], alias: Option<&'a str>) -> EvalContext<'a> {
898 EvalContext::new(cols, alias)
899 }
900
901 #[test]
908 fn borrowed_compare_equals_owned_apply_binary() {
909 let vals = vec![
910 Value::Null,
911 Value::Bool(true),
912 Value::Bool(false),
913 Value::SmallInt(3),
914 Value::Int(3),
915 Value::Int(-1),
916 Value::BigInt(3),
917 Value::BigInt(100),
918 Value::Float(3.0),
919 Value::Float(2.5),
920 Value::Text(String::new()),
921 Value::Text("a".into()),
922 Value::Text("b".into()),
923 Value::Date(10),
924 Value::Timestamp(1000),
925 Value::Numeric {
926 scaled: 30,
927 scale: 1,
928 },
929 Value::Interval {
930 months: 0,
931 micros: 5,
932 },
933 ];
934 let ops = [
935 BinOp::Eq,
936 BinOp::NotEq,
937 BinOp::Lt,
938 BinOp::LtEq,
939 BinOp::Gt,
940 BinOp::GtEq,
941 ];
942 let cs = vec![col("x", DataType::Int), col("y", DataType::Int)];
943 let c = ctx(&cs, None);
944 let lhs = Expr::Column(ColumnName {
945 qualifier: None,
946 name: "x".into(),
947 });
948 let rhs = Expr::Column(ColumnName {
949 qualifier: None,
950 name: "y".into(),
951 });
952 for l in &vals {
953 for r in &vals {
954 let row = Row::new(vec![l.clone(), r.clone()]);
955 for op in ops {
956 let got = eval_expr(
957 &Expr::Binary {
958 lhs: alloc::boxed::Box::new(lhs.clone()),
959 op,
960 rhs: alloc::boxed::Box::new(rhs.clone()),
961 },
962 &row,
963 &c,
964 );
965 let want = apply_binary(op, l.clone(), r.clone());
968 assert_eq!(
969 format!("{got:?}"),
970 format!("{want:?}"),
971 "op={op:?} l={l:?} r={r:?}"
972 );
973 }
974 }
975 }
976 }
977
978 fn lit(n: i64) -> Expr {
979 Expr::Literal(Literal::Integer(n))
980 }
981
982 fn null() -> Expr {
983 Expr::Literal(Literal::Null)
984 }
985
986 fn col_ref(name: &str) -> Expr {
987 Expr::Column(ColumnName {
988 qualifier: None,
989 name: name.into(),
990 })
991 }
992
993 #[test]
994 fn literal_evaluates_to_value() {
995 let r = Row::new(vec![]);
996 let cs: [ColumnSchema; 0] = [];
997 let c = ctx(&cs, None);
998 assert_eq!(eval_expr(&lit(42), &r, &c).unwrap(), Value::Int(42));
999 assert_eq!(
1000 eval_expr(&Expr::Literal(Literal::Float(1.5)), &r, &c).unwrap(),
1001 Value::Float(1.5)
1002 );
1003 assert_eq!(eval_expr(&null(), &r, &c).unwrap(), Value::Null);
1004 }
1005
1006 #[test]
1007 fn column_lookup_unqualified() {
1008 let cs = vec![col("a", DataType::Int), col("b", DataType::Text)];
1009 let r = Row::new(vec![Value::Int(7), Value::Text("hi".into())]);
1010 let c = ctx(&cs, None);
1011 assert_eq!(eval_expr(&col_ref("a"), &r, &c).unwrap(), Value::Int(7));
1012 assert_eq!(
1013 eval_expr(&col_ref("b"), &r, &c).unwrap(),
1014 Value::Text("hi".into())
1015 );
1016 }
1017
1018 #[test]
1019 fn column_not_found_errors() {
1020 let cs = vec![col("a", DataType::Int)];
1021 let r = Row::new(vec![Value::Int(0)]);
1022 let c = ctx(&cs, None);
1023 let err = eval_expr(&col_ref("ghost"), &r, &c).unwrap_err();
1024 assert!(matches!(err, EvalError::ColumnNotFound { ref name } if name == "ghost"));
1025 }
1026
1027 #[test]
1028 fn qualified_column_matches_alias() {
1029 let cs = vec![col("a", DataType::Int)];
1030 let r = Row::new(vec![Value::Int(5)]);
1031 let c = ctx(&cs, Some("u"));
1032 let qualified = Expr::Column(ColumnName {
1033 qualifier: Some("u".into()),
1034 name: "a".into(),
1035 });
1036 assert_eq!(eval_expr(&qualified, &r, &c).unwrap(), Value::Int(5));
1037 }
1038
1039 #[test]
1040 fn qualified_column_unknown_alias_errors() {
1041 let cs = vec![col("a", DataType::Int)];
1042 let r = Row::new(vec![Value::Int(5)]);
1043 let c = ctx(&cs, Some("u"));
1044 let wrong = Expr::Column(ColumnName {
1045 qualifier: Some("x".into()),
1046 name: "a".into(),
1047 });
1048 assert!(matches!(
1049 eval_expr(&wrong, &r, &c).unwrap_err(),
1050 EvalError::UnknownQualifier { .. }
1051 ));
1052 }
1053
1054 #[test]
1055 fn arithmetic_with_widening() {
1056 let r = Row::new(vec![]);
1057 let cs: [ColumnSchema; 0] = [];
1058 let c = ctx(&cs, None);
1059 let e = Expr::Binary {
1060 lhs: alloc::boxed::Box::new(lit(2)),
1061 op: BinOp::Add,
1062 rhs: alloc::boxed::Box::new(Expr::Literal(Literal::Float(0.5))),
1063 };
1064 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Float(2.5));
1065 }
1066
1067 #[test]
1068 fn division_by_zero_errors() {
1069 let r = Row::new(vec![]);
1070 let cs: [ColumnSchema; 0] = [];
1071 let c = ctx(&cs, None);
1072 let e = Expr::Binary {
1073 lhs: alloc::boxed::Box::new(lit(1)),
1074 op: BinOp::Div,
1075 rhs: alloc::boxed::Box::new(lit(0)),
1076 };
1077 assert_eq!(
1078 eval_expr(&e, &r, &c).unwrap_err(),
1079 EvalError::DivisionByZero
1080 );
1081 }
1082
1083 #[test]
1084 fn comparison_returns_bool() {
1085 let r = Row::new(vec![]);
1086 let cs: [ColumnSchema; 0] = [];
1087 let c = ctx(&cs, None);
1088 let e = Expr::Binary {
1089 lhs: alloc::boxed::Box::new(lit(1)),
1090 op: BinOp::Lt,
1091 rhs: alloc::boxed::Box::new(lit(2)),
1092 };
1093 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Bool(true));
1094 }
1095
1096 #[test]
1097 fn null_propagates_through_arithmetic() {
1098 let r = Row::new(vec![]);
1099 let cs: [ColumnSchema; 0] = [];
1100 let c = ctx(&cs, None);
1101 let e = Expr::Binary {
1102 lhs: alloc::boxed::Box::new(lit(1)),
1103 op: BinOp::Add,
1104 rhs: alloc::boxed::Box::new(null()),
1105 };
1106 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Null);
1107 }
1108
1109 #[test]
1110 fn and_three_valued_logic() {
1111 let r = Row::new(vec![]);
1112 let cs: [ColumnSchema; 0] = [];
1113 let c = ctx(&cs, None);
1114 let tt = |a: bool, b_null: bool| Expr::Binary {
1115 lhs: alloc::boxed::Box::new(Expr::Literal(Literal::Bool(a))),
1116 op: BinOp::And,
1117 rhs: alloc::boxed::Box::new(if b_null {
1118 null()
1119 } else {
1120 Expr::Literal(Literal::Bool(true))
1121 }),
1122 };
1123 assert_eq!(
1125 eval_expr(&tt(false, true), &r, &c).unwrap(),
1126 Value::Bool(false)
1127 );
1128 assert_eq!(eval_expr(&tt(true, true), &r, &c).unwrap(), Value::Null);
1130 assert_eq!(
1132 eval_expr(&tt(true, false), &r, &c).unwrap(),
1133 Value::Bool(true)
1134 );
1135 }
1136
1137 #[test]
1138 fn or_three_valued_logic() {
1139 let r = Row::new(vec![]);
1140 let cs: [ColumnSchema; 0] = [];
1141 let c = ctx(&cs, None);
1142 let or_with_null = |a: bool| Expr::Binary {
1143 lhs: alloc::boxed::Box::new(Expr::Literal(Literal::Bool(a))),
1144 op: BinOp::Or,
1145 rhs: alloc::boxed::Box::new(null()),
1146 };
1147 assert_eq!(
1149 eval_expr(&or_with_null(true), &r, &c).unwrap(),
1150 Value::Bool(true)
1151 );
1152 assert_eq!(
1154 eval_expr(&or_with_null(false), &r, &c).unwrap(),
1155 Value::Null
1156 );
1157 }
1158
1159 #[test]
1160 fn not_on_null_is_null() {
1161 let r = Row::new(vec![]);
1162 let cs: [ColumnSchema; 0] = [];
1163 let c = ctx(&cs, None);
1164 let e = Expr::Unary {
1165 op: UnOp::Not,
1166 expr: alloc::boxed::Box::new(null()),
1167 };
1168 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Null);
1169 }
1170
1171 #[test]
1172 fn text_comparison_lexicographic() {
1173 let r = Row::new(vec![]);
1174 let cs: [ColumnSchema; 0] = [];
1175 let c = ctx(&cs, None);
1176 let e = Expr::Binary {
1177 lhs: alloc::boxed::Box::new(Expr::Literal(Literal::String("apple".into()))),
1178 op: BinOp::Lt,
1179 rhs: alloc::boxed::Box::new(Expr::Literal(Literal::String("banana".into()))),
1180 };
1181 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Bool(true));
1182 }
1183
1184 #[test]
1185 fn interval_format_basics() {
1186 assert_eq!(format_interval(0, 0), "0");
1187 assert_eq!(format_interval(0, 86_400_000_000), "1 day");
1188 assert_eq!(format_interval(0, -86_400_000_000), "-1 days");
1189 assert_eq!(format_interval(0, 3_600_000_000), "01:00:00");
1190 assert_eq!(
1191 format_interval(0, 86_400_000_000 + 9_000_000),
1192 "1 day 00:00:09"
1193 );
1194 assert_eq!(format_interval(14, 0), "1 year 2 mons");
1195 assert_eq!(format_interval(-1, 0), "-1 mons");
1196 }
1197}