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)) => Ok(Some(EvalOperand::Borrowed(value))),
208            Some(SQLParam::Vector(_)) | Some(SQLParam::Tensor(_)) => Ok(None),
209            None => Err(SQLError::MissingParam(*i)),
210        },
211        Expr::Column(name) => {
212            if ctx.row_lookup()?.column_is_ambiguous(name) {
213                return Err(SQLError::AmbiguousColumn(name.clone()));
214            }
215            Ok(Some(match ctx.row_lookup()?.column(name) {
216                Some(value) => EvalOperand::Borrowed(value),
217                None => EvalOperand::Owned(Value::Null),
218            }))
219        }
220        Expr::QualifiedColumn { qualifier, column } => {
221            if ctx
222                .row_lookup()?
223                .qualified_column_is_ambiguous(qualifier, column)
224            {
225                return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
226            }
227            Ok(Some(
228                match ctx.row_lookup()?.qualified_column(qualifier, column) {
229                    Some(value) => EvalOperand::Borrowed(value),
230                    None => EvalOperand::Owned(Value::Null),
231                },
232            ))
233        }
234        _ => Ok(None),
235    }
236}
237
238/// `NULL` is falsy; otherwise truthy iff the value coerces to a non-zero
239/// boolean / number / non-empty string.
240pub fn truthy(v: &Value) -> bool {
241    match v {
242        Value::Null => false,
243        Value::Bool(b) => *b,
244        Value::Int(n) => *n != 0,
245        Value::Float(f) => *f != 0.0,
246        Value::Decimal(d) => !d.is_zero(),
247        Value::Str(s) | Value::FixedChar(s) => !s.is_empty(),
248        _ => true,
249    }
250}
251
252/// Two-valued equality used where SQL treats a NULL comparison as
253/// simply "no match" (CASE base matching, NULLIF, IN-subquery probes).
254pub(super) fn values_equal(a: &Value, b: &Value) -> bool {
255    values_equal_nullable(a, b) == Some(true)
256}
257
258/// Three-valued equality: `None` when either side is NULL (or, for row
259/// values, when element NULLs leave the outcome undecided).
260pub(super) fn values_equal_nullable(a: &Value, b: &Value) -> Option<bool> {
261    match (a, b) {
262        (Value::Null, _) | (_, Value::Null) => None,
263        (
264            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
265            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
266        ) => Some(a.cmp(b) == std::cmp::Ordering::Equal),
267        (Value::Bool(x), Value::Decimal(y)) | (Value::Decimal(y), Value::Bool(x)) => {
268            Some(DecimalValue::from_bool(*x) == *y)
269        }
270        // Temporal equality goes through the ordering key so
271        // `interval '1 mon' = interval '30 days'` holds like in
272        // PostgreSQL (30-day months for comparison purposes).
273        (Value::Temporal(x), Value::Temporal(y)) => Some(x.cmp(y) == std::cmp::Ordering::Equal),
274        (Value::Temporal(x), Value::Str(y)) | (Value::Str(y), Value::Temporal(x)) => Some(
275            x.parse_same_kind(y)
276                .is_some_and(|parsed| x.cmp(&parsed) == std::cmp::Ordering::Equal),
277        ),
278        (Value::FixedChar(x), Value::FixedChar(y)) => {
279            Some(x.trim_end_matches(' ') == y.trim_end_matches(' '))
280        }
281        (Value::FixedChar(x), Value::Str(y)) | (Value::Str(y), Value::FixedChar(x)) => {
282            Some(x.trim_end_matches(' ') == y.trim_end_matches(' '))
283        }
284        // PostgreSQL arrays and stored composite records use total element
285        // equality: corresponding NULLs compare equal.
286        (Value::Array(_), Value::Array(_))
287        | (Value::List(_), Value::List(_))
288        | (Value::Record(_), Value::Record(_)) => Some(a == b),
289        // Anonymous row constructors use SQL three-valued comparison: any
290        // definite mismatch wins, otherwise a NULL field leaves equality
291        // unknown.
292        (Value::Row(xs), Value::Row(ys)) => {
293            if xs.len() != ys.len() {
294                return Some(false);
295            }
296            let mut unknown = false;
297            for (x, y) in xs.iter().zip(ys) {
298                match values_equal_nullable(x, y) {
299                    Some(false) => return Some(false),
300                    Some(true) => {}
301                    None => unknown = true,
302                }
303            }
304            if unknown {
305                None
306            } else {
307                Some(true)
308            }
309        }
310        _ => Some(a == b),
311    }
312}
313
314pub(super) fn compare(a: &Value, b: &Value) -> Result<std::cmp::Ordering> {
315    Ok(compare_nullable(a, b)?.unwrap_or(std::cmp::Ordering::Equal))
316}
317
318/// Three-valued ordering: `None` when a NULL operand (or an undecided
319/// NULL row element) leaves the comparison unknown.
320pub(super) fn compare_nullable(a: &Value, b: &Value) -> Result<Option<std::cmp::Ordering>> {
321    use std::cmp::Ordering;
322    match (a, b) {
323        (Value::Null, _) | (_, Value::Null) => Ok(None),
324        (
325            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
326            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
327        ) => Ok(Some(a.cmp(b))),
328        (Value::Bool(x), Value::Decimal(y)) => Ok(Some(DecimalValue::from_bool(*x).cmp(y))),
329        (Value::Decimal(x), Value::Bool(y)) => Ok(Some(x.cmp(&DecimalValue::from_bool(*y)))),
330        (Value::Str(x), Value::Str(y)) => Ok(Some(x.cmp(y))),
331        (Value::FixedChar(x), Value::FixedChar(y)) => {
332            Ok(Some(x.trim_end_matches(' ').cmp(y.trim_end_matches(' '))))
333        }
334        (Value::FixedChar(x), Value::Str(y)) => {
335            Ok(Some(x.trim_end_matches(' ').cmp(y.trim_end_matches(' '))))
336        }
337        (Value::Str(x), Value::FixedChar(y)) => {
338            Ok(Some(x.trim_end_matches(' ').cmp(y.trim_end_matches(' '))))
339        }
340        (Value::JsonB(_), Value::JsonB(_)) => Ok(Some(a.cmp(b))),
341        (Value::Temporal(x), Value::Temporal(y)) => Ok(Some(x.cmp(y))),
342        (Value::Temporal(x), Value::Str(y)) => x
343            .parse_same_kind(y)
344            .map(|parsed| Some(x.cmp(&parsed)))
345            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot compare {a:?} with {b:?}"))),
346        (Value::Str(x), Value::Temporal(y)) => y
347            .parse_same_kind(x)
348            .map(|parsed| Some(parsed.cmp(y)))
349            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot compare {a:?} with {b:?}"))),
350        (Value::Bool(x), Value::Bool(y)) => Ok(Some(x.cmp(y))),
351        (Value::Array(_), Value::Array(_))
352        | (Value::List(_), Value::List(_))
353        | (Value::Record(_), Value::Record(_)) => Ok(Some(a.cmp(b))),
354        // Anonymous row-constructor ordering is lexicographic, with a NULL
355        // field making the result unknown if reached before a decision.
356        (Value::Row(xs), Value::Row(ys)) => {
357            for (x, y) in xs.iter().zip(ys) {
358                match compare_nullable(x, y)? {
359                    Some(Ordering::Equal) => {}
360                    Some(other) => return Ok(Some(other)),
361                    None => return Ok(None),
362                }
363            }
364            Ok(Some(xs.len().cmp(&ys.len())))
365        }
366        (lhs, rhs) => Err(SQLError::TypeMismatch(format!(
367            "cannot compare {lhs:?} with {rhs:?}"
368        ))),
369    }
370}
371
372/// `PostgreSQL` `division by zero` error (SQLSTATE 22012).
373pub(crate) fn division_by_zero() -> SQLError {
374    SQLError::Routine {
375        sqlstate: "22012".into(),
376        message: "division by zero".into(),
377    }
378}
379
380/// `PostgreSQL` numeric overflow error (SQLSTATE 22003).
381pub(crate) fn out_of_range(type_name: &str) -> SQLError {
382    SQLError::Routine {
383        sqlstate: "22003".into(),
384        message: format!("{type_name} out of range"),
385    }
386}
387
388pub(super) fn arith(a: &Value, b: &Value, op: BinaryOp) -> Result<Value> {
389    // SQL three-valued logic: NULL `op` anything == NULL.
390    if matches!(a, Value::Null) || matches!(b, Value::Null) {
391        return Ok(Value::Null);
392    }
393    // Integer x integer is the overwhelmingly common analytical path.
394    // Resolve it before probing unrelated temporal / decimal / floating
395    // representations, while retaining PostgreSQL overflow behavior. The
396    // caller applies the SQL operator's int2/int4/int8 result width after this
397    // carrier-level i64 operation.
398    if let (Value::Int(li), Value::Int(ri)) = (a, b) {
399        let out = match op {
400            BinaryOp::Add => li.checked_add(*ri),
401            BinaryOp::Subtract => li.checked_sub(*ri),
402            BinaryOp::Multiply => li.checked_mul(*ri),
403            BinaryOp::Divide => {
404                if *ri == 0 {
405                    return Err(division_by_zero());
406                }
407                // Integer / integer in SQL truncates toward zero.
408                li.checked_div(*ri)
409            }
410            _ => {
411                return Err(SQLError::Internal(format!(
412                    "non-arithmetic operator {op:?} reached integer arithmetic"
413                )))
414            }
415        };
416        return out.map(Value::Int).ok_or_else(|| out_of_range("bigint"));
417    }
418    if matches!(op, BinaryOp::Subtract)
419        && matches!(a, Value::JsonB(_) | Value::Map(_) | Value::List(_))
420    {
421        if let Some(value) = json_delete(&[a.clone(), b.clone()])? {
422            return Ok(value);
423        }
424    }
425    if matches!(a, Value::Temporal(_)) || matches!(b, Value::Temporal(_)) {
426        return time::temporal_arith(a, b, op);
427    }
428    let has_decimal = matches!(a, Value::Decimal(_)) || matches!(b, Value::Decimal(_));
429    let has_float = matches!(a, Value::Float(_)) || matches!(b, Value::Float(_));
430    // PostgreSQL numeric promotion: double precision wins mixed
431    // float/numeric arithmetic. Exact decimal arithmetic only applies
432    // when no float operand is involved.
433    if has_decimal && !has_float {
434        return decimal_arith(a, b, op);
435    }
436    let lf = to_f64(a)?;
437    let rf = to_f64(b)?;
438    let result = match op {
439        BinaryOp::Add => lf + rf,
440        BinaryOp::Subtract => lf - rf,
441        BinaryOp::Multiply => lf * rf,
442        BinaryOp::Divide => {
443            if rf == 0.0 {
444                return Err(division_by_zero());
445            }
446            lf / rf
447        }
448        _ => {
449            return Err(SQLError::Internal(format!(
450                "non-arithmetic operator {op:?} reached floating arithmetic"
451            )))
452        }
453    };
454    Ok(Value::Float(result))
455}
456
457pub(super) fn decimal_arith(a: &Value, b: &Value, op: BinaryOp) -> Result<Value> {
458    let left = to_decimal(a)?;
459    let right = to_decimal(b)?;
460    let value = match op {
461        BinaryOp::Add => left.checked_add(&right),
462        BinaryOp::Subtract => left.checked_sub(&right),
463        BinaryOp::Multiply => left.checked_mul(&right),
464        BinaryOp::Divide => {
465            if right.is_zero() {
466                return Err(division_by_zero());
467            }
468            left.checked_div_postgres(&right)
469        }
470        _ => {
471            return Err(SQLError::Internal(format!(
472                "non-arithmetic operator {op:?} reached decimal arithmetic"
473            )))
474        }
475    }
476    .ok_or_else(|| out_of_range("numeric"))?;
477    Ok(Value::Decimal(value))
478}