Skip to main content

uqa_sql/expr/
binary.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL comparison, three-valued logic, and numeric arithmetic.
8
9use super::{
10    eval, json_delete, time, to_decimal, BinaryOp, DecimalValue, EvalContext, Expr, Result,
11    SQLError, SQLParam, Value,
12};
13
14pub(super) fn eval_binary(
15    op: BinaryOp,
16    lhs: &Expr,
17    rhs: &Expr,
18    ctx: &EvalContext<'_>,
19) -> Result<Value> {
20    if let Some(value) = eval_binary_borrowed(op, lhs, rhs, ctx)? {
21        return Ok(value);
22    }
23    let l = eval(lhs, ctx)?;
24    let r = eval(rhs, ctx)?;
25    if is_arithmetic(op) && real_expr(lhs, ctx.params) && real_expr(rhs, ctx.params) {
26        return super::eval_float_arithmetic(op, &l, &r, super::FloatWidth::Real);
27    }
28    eval_binary_values_with_integer_width(op, &l, &r, integer_binary_width(lhs, rhs))
29}
30
31pub(super) fn is_arithmetic(op: BinaryOp) -> bool {
32    matches!(
33        op,
34        BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide
35    )
36}
37
38fn real_expr(expression: &Expr, params: &[SQLParam]) -> bool {
39    match expression {
40        Expr::Cast { ty, .. } | Expr::TypedLiteral { ty, .. } => {
41            matches!(
42                crate::ast::ColumnType::from_sql_name(ty),
43                Ok(crate::ast::ColumnType::Real)
44            )
45        }
46        Expr::Param(index) => index
47            .checked_sub(1)
48            .and_then(|index| params.get(index))
49            .and_then(SQLParam::declared_scalar_type)
50            .is_some_and(|ty| matches!(ty, crate::ast::ColumnType::Real)),
51        Expr::UnaryMinus(inner) => real_expr(inner, params),
52        Expr::Binary { op, lhs, rhs } if is_arithmetic(*op) => {
53            real_expr(lhs, params) && real_expr(rhs, params)
54        }
55        _ => false,
56    }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
60pub enum IntegerWidth {
61    SmallInt,
62    Integer,
63    BigInt,
64}
65
66#[must_use]
67pub fn integer_width_for_literal(value: i64) -> IntegerWidth {
68    if i32::try_from(value).is_ok() {
69        IntegerWidth::Integer
70    } else {
71        IntegerWidth::BigInt
72    }
73}
74
75#[must_use]
76pub fn integer_width_for_type(ty: &str) -> Option<IntegerWidth> {
77    let ty = ty.trim().to_ascii_lowercase();
78    match ty.as_str() {
79        "smallint" | "int2" | "pg_catalog.int2" => Some(IntegerWidth::SmallInt),
80        "integer" | "int" | "int4" | "serial" | "serial4" | "pg_catalog.int4" => {
81            Some(IntegerWidth::Integer)
82        }
83        "bigint" | "int8" | "bigserial" | "serial8" | "pg_catalog.int8" => {
84            Some(IntegerWidth::BigInt)
85        }
86        _ => None,
87    }
88}
89
90fn integer_expr_width(expr: &Expr) -> Option<IntegerWidth> {
91    match expr {
92        Expr::Literal(Value::Int(value)) => Some(integer_width_for_literal(*value)),
93        Expr::Cast { ty, .. } | Expr::TypedLiteral { ty, .. } => integer_width_for_type(ty),
94        Expr::Binary {
95            op: BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide,
96            lhs,
97            rhs,
98        } => Some(integer_expr_width(lhs)?.max(integer_expr_width(rhs)?)),
99        _ => None,
100    }
101}
102
103fn integer_binary_width(lhs: &Expr, rhs: &Expr) -> Option<IntegerWidth> {
104    Some(integer_expr_width(lhs)?.max(integer_expr_width(rhs)?))
105}
106
107/// Apply a binary SQL operator to values that have already been evaluated.
108/// Execution engines use this when a hot path compiles expression traversal
109/// ahead of time but must retain the evaluator's exact comparison, numeric
110/// promotion, NULL, overflow, and division-by-zero semantics.
111pub fn eval_binary_values(op: BinaryOp, l: &Value, r: &Value) -> Result<Value> {
112    match op {
113        BinaryOp::Equal
114        | BinaryOp::NotEqual
115        | BinaryOp::Less
116        | BinaryOp::LessEqual
117        | BinaryOp::Greater
118        | BinaryOp::GreaterEqual => eval_comparison_op(op, l, r),
119        BinaryOp::Add => arith(l, r, op),
120        BinaryOp::Subtract => arith(l, r, op),
121        BinaryOp::Multiply => arith(l, r, op),
122        BinaryOp::Divide => arith(l, r, op),
123    }
124}
125
126/// Evaluate an operator while retaining the integer type selected by SQL
127/// operator resolution. The dynamic [`Value`] carrier stores all integers as
128/// `i64`, so expression plans pass this width alongside the operands.
129pub fn eval_binary_values_with_integer_width(
130    op: BinaryOp,
131    l: &Value,
132    r: &Value,
133    integer_width: Option<IntegerWidth>,
134) -> Result<Value> {
135    let value = eval_binary_values(op, l, r)?;
136    let Some(integer_width) = integer_width else {
137        return Ok(value);
138    };
139    let Value::Int(value) = value else {
140        return Ok(value);
141    };
142    let in_range = match integer_width {
143        IntegerWidth::SmallInt => i16::try_from(value).is_ok(),
144        IntegerWidth::Integer => i32::try_from(value).is_ok(),
145        IntegerWidth::BigInt => true,
146    };
147    if in_range {
148        Ok(Value::Int(value))
149    } else {
150        Err(out_of_range(match integer_width {
151            IntegerWidth::SmallInt => "smallint",
152            IntegerWidth::Integer => "integer",
153            IntegerWidth::BigInt => "bigint",
154        }))
155    }
156}
157
158/// Comparison operators under SQL three-valued logic: any NULL operand
159/// makes the result NULL.
160pub(super) fn eval_comparison_op(op: BinaryOp, l: &Value, r: &Value) -> Result<Value> {
161    Ok(eval_comparison_truth(op, l, r)?
162        .map(Value::Bool)
163        .unwrap_or(Value::Null))
164}
165
166/// Compare two values without allocating an intermediate [`Value::Bool`].
167///
168/// `None` is SQL UNKNOWN (normally caused by NULL). Predicate executors use
169/// this form so comparisons and boolean composition stay in a compact
170/// tri-state representation throughout the row-filtering hot path.
171#[inline]
172pub fn eval_comparison_truth(op: BinaryOp, l: &Value, r: &Value) -> Result<Option<bool>> {
173    let out = match op {
174        BinaryOp::Equal => values_equal_nullable(l, r),
175        BinaryOp::NotEqual => values_equal_nullable(l, r).map(|equal| !equal),
176        BinaryOp::Less => compare_nullable(l, r)?.map(|ord| ord.is_lt()),
177        BinaryOp::LessEqual => compare_nullable(l, r)?.map(|ord| ord.is_le()),
178        BinaryOp::Greater => compare_nullable(l, r)?.map(|ord| ord.is_gt()),
179        BinaryOp::GreaterEqual => compare_nullable(l, r)?.map(|ord| ord.is_ge()),
180        _ => {
181            return Err(SQLError::Internal(format!(
182                "non-comparison operator {op:?} reached comparison evaluation"
183            )))
184        }
185    };
186    Ok(out)
187}
188
189pub(super) enum EvalOperand<'a> {
190    Borrowed(&'a Value),
191    Owned(Value),
192}
193
194impl EvalOperand<'_> {
195    fn as_value(&self) -> &Value {
196        match self {
197            Self::Borrowed(value) => value,
198            Self::Owned(value) => value,
199        }
200    }
201}
202
203pub(super) fn eval_binary_borrowed(
204    op: BinaryOp,
205    lhs: &Expr,
206    rhs: &Expr,
207    ctx: &EvalContext<'_>,
208) -> Result<Option<Value>> {
209    if !matches!(
210        op,
211        BinaryOp::Equal
212            | BinaryOp::NotEqual
213            | BinaryOp::Less
214            | BinaryOp::LessEqual
215            | BinaryOp::Greater
216            | BinaryOp::GreaterEqual
217    ) {
218        return Ok(None);
219    }
220    let Some(l) = eval_operand_borrowed(lhs, ctx)? else {
221        return Ok(None);
222    };
223    let Some(r) = eval_operand_borrowed(rhs, ctx)? else {
224        return Ok(None);
225    };
226    let l = l.as_value();
227    let r = r.as_value();
228    Ok(Some(eval_comparison_op(op, l, r)?))
229}
230
231pub(super) fn eval_operand_borrowed<'a>(
232    expr: &Expr,
233    ctx: &EvalContext<'a>,
234) -> Result<Option<EvalOperand<'a>>> {
235    match expr {
236        Expr::Literal(value) => Ok(Some(EvalOperand::Owned(value.clone()))),
237        Expr::Param(i) => match i.checked_sub(1).and_then(|index| ctx.params.get(index)) {
238            Some(SQLParam::Scalar(value) | SQLParam::TypedScalar { value, .. }) => {
239                Ok(Some(EvalOperand::Borrowed(value)))
240            }
241            Some(SQLParam::Vector(_)) | Some(SQLParam::Tensor(_)) => Ok(None),
242            None => Err(SQLError::MissingParam(*i)),
243        },
244        Expr::Column(name) => {
245            if ctx.row_lookup()?.column_is_ambiguous(name) {
246                return Err(SQLError::AmbiguousColumn(name.clone()));
247            }
248            Ok(Some(match ctx.row_lookup()?.column(name) {
249                Some(value) => EvalOperand::Borrowed(value),
250                None => EvalOperand::Owned(Value::Null),
251            }))
252        }
253        Expr::QualifiedColumn { qualifier, column } => {
254            if ctx
255                .row_lookup()?
256                .qualified_column_is_ambiguous(qualifier, column)
257            {
258                return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
259            }
260            Ok(Some(
261                match ctx.row_lookup()?.qualified_column(qualifier, column) {
262                    Some(value) => EvalOperand::Borrowed(value),
263                    None => EvalOperand::Owned(Value::Null),
264                },
265            ))
266        }
267        _ => Ok(None),
268    }
269}
270
271/// `NULL` is falsy; otherwise truthy iff the value coerces to a non-zero
272/// boolean / number / non-empty string.
273pub fn truthy(v: &Value) -> bool {
274    match v {
275        Value::Null => false,
276        Value::Bool(b) => *b,
277        Value::Int(n) => *n != 0,
278        Value::Float(f) => *f != 0.0,
279        Value::Decimal(d) => !d.is_zero(),
280        Value::Str(s) | Value::FixedChar(s) => !s.is_empty(),
281        _ => true,
282    }
283}
284
285/// Two-valued equality used where SQL treats a NULL comparison as
286/// simply "no match" (CASE base matching, NULLIF, IN-subquery probes).
287pub(super) fn values_equal(a: &Value, b: &Value) -> bool {
288    values_equal_nullable(a, b) == Some(true)
289}
290
291/// Three-valued equality: `None` when either side is NULL (or, for row
292/// values, when element NULLs leave the outcome undecided).
293pub(super) fn values_equal_nullable(a: &Value, b: &Value) -> Option<bool> {
294    match (a, b) {
295        (Value::Null, _) | (_, Value::Null) => None,
296        (
297            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
298            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
299        ) => Some(a.cmp(b) == std::cmp::Ordering::Equal),
300        (Value::Bool(x), Value::Decimal(y)) | (Value::Decimal(y), Value::Bool(x)) => {
301            Some(DecimalValue::from_bool(*x) == *y)
302        }
303        // Temporal equality goes through the ordering key so
304        // `interval '1 mon' = interval '30 days'` holds like in
305        // PostgreSQL (30-day months for comparison purposes).
306        (Value::Temporal(x), Value::Temporal(y)) => Some(x.cmp(y) == std::cmp::Ordering::Equal),
307        (Value::Temporal(x), Value::Str(y)) | (Value::Str(y), Value::Temporal(x)) => Some(
308            x.parse_same_kind(y)
309                .is_some_and(|parsed| x.cmp(&parsed) == std::cmp::Ordering::Equal),
310        ),
311        (Value::FixedChar(x), Value::FixedChar(y)) => {
312            Some(x.trim_end_matches(' ') == y.trim_end_matches(' '))
313        }
314        (Value::FixedChar(x), Value::Str(y)) | (Value::Str(y), Value::FixedChar(x)) => {
315            Some(x.trim_end_matches(' ') == y.trim_end_matches(' '))
316        }
317        // PostgreSQL arrays and stored composite records use total element
318        // equality: corresponding NULLs compare equal.
319        (Value::Array(_), Value::Array(_))
320        | (Value::List(_), Value::List(_))
321        | (Value::Record(_), Value::Record(_)) => Some(a == b),
322        // Anonymous row constructors use SQL three-valued comparison: any
323        // definite mismatch wins, otherwise a NULL field leaves equality
324        // unknown.
325        (Value::Row(xs), Value::Row(ys)) => {
326            if xs.len() != ys.len() {
327                return Some(false);
328            }
329            let mut unknown = false;
330            for (x, y) in xs.iter().zip(ys) {
331                match values_equal_nullable(x, y) {
332                    Some(false) => return Some(false),
333                    Some(true) => {}
334                    None => unknown = true,
335                }
336            }
337            if unknown {
338                None
339            } else {
340                Some(true)
341            }
342        }
343        _ => Some(a == b),
344    }
345}
346
347pub(super) fn compare(a: &Value, b: &Value) -> Result<std::cmp::Ordering> {
348    Ok(compare_nullable(a, b)?.unwrap_or(std::cmp::Ordering::Equal))
349}
350
351/// Three-valued ordering: `None` when a NULL operand (or an undecided
352/// NULL row element) leaves the comparison unknown.
353pub(super) fn compare_nullable(a: &Value, b: &Value) -> Result<Option<std::cmp::Ordering>> {
354    use std::cmp::Ordering;
355    match (a, b) {
356        (Value::Null, _) | (_, Value::Null) => Ok(None),
357        (
358            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
359            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
360        ) => Ok(Some(a.cmp(b))),
361        (Value::Bool(x), Value::Decimal(y)) => Ok(Some(DecimalValue::from_bool(*x).cmp(y))),
362        (Value::Decimal(x), Value::Bool(y)) => Ok(Some(x.cmp(&DecimalValue::from_bool(*y)))),
363        (Value::Str(x), Value::Str(y)) => Ok(Some(x.cmp(y))),
364        (Value::FixedChar(x), Value::FixedChar(y)) => {
365            Ok(Some(x.trim_end_matches(' ').cmp(y.trim_end_matches(' '))))
366        }
367        (Value::FixedChar(x), Value::Str(y)) => {
368            Ok(Some(x.trim_end_matches(' ').cmp(y.trim_end_matches(' '))))
369        }
370        (Value::Str(x), Value::FixedChar(y)) => {
371            Ok(Some(x.trim_end_matches(' ').cmp(y.trim_end_matches(' '))))
372        }
373        (Value::JsonB(_), Value::JsonB(_)) => Ok(Some(a.cmp(b))),
374        (Value::Temporal(x), Value::Temporal(y)) => Ok(Some(x.cmp(y))),
375        (Value::Temporal(x), Value::Str(y)) => x
376            .parse_same_kind(y)
377            .map(|parsed| Some(x.cmp(&parsed)))
378            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot compare {a:?} with {b:?}"))),
379        (Value::Str(x), Value::Temporal(y)) => y
380            .parse_same_kind(x)
381            .map(|parsed| Some(parsed.cmp(y)))
382            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot compare {a:?} with {b:?}"))),
383        (Value::Bool(x), Value::Bool(y)) => Ok(Some(x.cmp(y))),
384        (Value::Array(_), Value::Array(_))
385        | (Value::List(_), Value::List(_))
386        | (Value::Record(_), Value::Record(_)) => Ok(Some(a.cmp(b))),
387        // Anonymous row-constructor ordering is lexicographic, with a NULL
388        // field making the result unknown if reached before a decision.
389        (Value::Row(xs), Value::Row(ys)) => {
390            for (x, y) in xs.iter().zip(ys) {
391                match compare_nullable(x, y)? {
392                    Some(Ordering::Equal) => {}
393                    Some(other) => return Ok(Some(other)),
394                    None => return Ok(None),
395                }
396            }
397            Ok(Some(xs.len().cmp(&ys.len())))
398        }
399        (lhs, rhs) => Err(SQLError::TypeMismatch(format!(
400            "cannot compare {lhs:?} with {rhs:?}"
401        ))),
402    }
403}
404
405/// `PostgreSQL` `division by zero` error (SQLSTATE 22012).
406pub(crate) fn division_by_zero() -> SQLError {
407    SQLError::Routine {
408        sqlstate: "22012".into(),
409        message: "division by zero".into(),
410    }
411}
412
413/// `PostgreSQL` numeric overflow error (SQLSTATE 22003).
414pub(crate) fn out_of_range(type_name: &str) -> SQLError {
415    SQLError::Routine {
416        sqlstate: "22003".into(),
417        message: format!("{type_name} out of range"),
418    }
419}
420
421pub(super) fn arith(a: &Value, b: &Value, op: BinaryOp) -> Result<Value> {
422    // SQL three-valued logic: NULL `op` anything == NULL.
423    if matches!(a, Value::Null) || matches!(b, Value::Null) {
424        return Ok(Value::Null);
425    }
426    // Integer x integer is the overwhelmingly common analytical path.
427    // Resolve it before probing unrelated temporal / decimal / floating
428    // representations, while retaining PostgreSQL overflow behavior. The
429    // caller applies the SQL operator's int2/int4/int8 result width after this
430    // carrier-level i64 operation.
431    if let (Value::Int(li), Value::Int(ri)) = (a, b) {
432        let out = match op {
433            BinaryOp::Add => li.checked_add(*ri),
434            BinaryOp::Subtract => li.checked_sub(*ri),
435            BinaryOp::Multiply => li.checked_mul(*ri),
436            BinaryOp::Divide => {
437                if *ri == 0 {
438                    return Err(division_by_zero());
439                }
440                // Integer / integer in SQL truncates toward zero.
441                li.checked_div(*ri)
442            }
443            _ => {
444                return Err(SQLError::Internal(format!(
445                    "non-arithmetic operator {op:?} reached integer arithmetic"
446                )))
447            }
448        };
449        return out.map(Value::Int).ok_or_else(|| out_of_range("bigint"));
450    }
451    if matches!(op, BinaryOp::Subtract)
452        && matches!(a, Value::JsonB(_) | Value::Map(_) | Value::List(_))
453    {
454        if let Some(value) = json_delete(&[a.clone(), b.clone()])? {
455            return Ok(value);
456        }
457    }
458    if matches!(a, Value::Temporal(_)) || matches!(b, Value::Temporal(_)) {
459        return time::temporal_arith(a, b, op);
460    }
461    let has_decimal = matches!(a, Value::Decimal(_)) || matches!(b, Value::Decimal(_));
462    let has_float = matches!(a, Value::Float(_)) || matches!(b, Value::Float(_));
463    // PostgreSQL numeric promotion: double precision wins mixed
464    // float/numeric arithmetic. Exact decimal arithmetic only applies
465    // when no float operand is involved.
466    if has_decimal && !has_float {
467        return decimal_arith(a, b, op);
468    }
469    super::eval_float_arithmetic(op, a, b, super::FloatWidth::DoublePrecision)
470}
471
472pub(super) fn decimal_arith(a: &Value, b: &Value, op: BinaryOp) -> Result<Value> {
473    let left = to_decimal(a)?;
474    let right = to_decimal(b)?;
475    let value = match op {
476        BinaryOp::Add => left.checked_add(&right),
477        BinaryOp::Subtract => left.checked_sub(&right),
478        BinaryOp::Multiply => left.checked_mul(&right),
479        BinaryOp::Divide => {
480            if right.is_zero() {
481                return Err(division_by_zero());
482            }
483            left.checked_div_postgres(&right)
484        }
485        _ => {
486            return Err(SQLError::Internal(format!(
487                "non-arithmetic operator {op:?} reached decimal arithmetic"
488            )))
489        }
490    }
491    .ok_or_else(|| out_of_range("numeric"))?;
492    Ok(Value::Decimal(value))
493}