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