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