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::{eval, time, BinaryOp, EvalContext, Expr, Result, SQLError, SQLParam, Value};
10
11use uqa_core::memory::{Produced, ProductionControl};
12
13mod comparison;
14#[cfg(test)]
15mod production_tests;
16
17pub use comparison::{
18    compare_nullable_with_control, compare_typed_values_with_control, compare_with_control,
19    eval_comparison_truth, eval_comparison_truth_with_control, type_comparison_can_fail,
20    validate_legacy_vector_comparison, value_comparison_can_fail,
21    values_equal_nullable_with_control, values_equal_with_control,
22};
23pub(super) use comparison::{eval_comparison_op, values_equal, values_equal_nullable};
24
25pub(super) fn eval_binary(
26    op: BinaryOp,
27    lhs: &Expr,
28    rhs: &Expr,
29    ctx: &EvalContext<'_>,
30) -> Result<Value> {
31    if let Some(value) = eval_binary_borrowed(op, lhs, rhs, ctx)? {
32        return Ok(value);
33    }
34    let l = eval(lhs, ctx)?;
35    let r = eval(rhs, ctx)?;
36    if is_arithmetic(op) && real_expr(lhs, ctx.params) && real_expr(rhs, ctx.params) {
37        return super::eval_float_arithmetic(op, &l, &r, super::FloatWidth::Real);
38    }
39    eval_binary_values_with_integer_width(op, &l, &r, integer_binary_width(lhs, rhs))
40}
41
42pub(super) fn is_arithmetic(op: BinaryOp) -> bool {
43    matches!(
44        op,
45        BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide
46    )
47}
48
49fn real_expr(expression: &Expr, params: &[SQLParam]) -> bool {
50    match expression {
51        Expr::Cast { ty, .. } | Expr::TypedLiteral { ty, .. } => {
52            matches!(
53                crate::ast::ColumnType::from_sql_name(ty),
54                Ok(crate::ast::ColumnType::Real)
55            )
56        }
57        Expr::Param(index) => index
58            .checked_sub(1)
59            .and_then(|index| params.get(index))
60            .and_then(SQLParam::declared_scalar_type)
61            .is_some_and(|ty| matches!(ty, crate::ast::ColumnType::Real)),
62        Expr::UnaryMinus(inner) => real_expr(inner, params),
63        Expr::Binary { op, lhs, rhs } if is_arithmetic(*op) => {
64            real_expr(lhs, params) && real_expr(rhs, params)
65        }
66        _ => false,
67    }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
71pub enum IntegerWidth {
72    SmallInt,
73    Integer,
74    BigInt,
75}
76
77#[must_use]
78pub fn integer_width_for_literal(value: i64) -> IntegerWidth {
79    if i32::try_from(value).is_ok() {
80        IntegerWidth::Integer
81    } else {
82        IntegerWidth::BigInt
83    }
84}
85
86#[must_use]
87pub fn integer_width_for_type(ty: &str) -> Option<IntegerWidth> {
88    let ty = ty.trim();
89    [
90        (
91            IntegerWidth::SmallInt,
92            &["smallint", "int2", "pg_catalog.int2"][..],
93        ),
94        (
95            IntegerWidth::Integer,
96            &[
97                "integer",
98                "int",
99                "int4",
100                "serial",
101                "serial4",
102                "pg_catalog.int4",
103            ][..],
104        ),
105        (
106            IntegerWidth::BigInt,
107            &["bigint", "int8", "bigserial", "serial8", "pg_catalog.int8"][..],
108        ),
109    ]
110    .into_iter()
111    .find_map(|(width, names)| {
112        names
113            .iter()
114            .any(|name| ty.eq_ignore_ascii_case(name))
115            .then_some(width)
116    })
117}
118
119fn integer_expr_width(expr: &Expr) -> Option<IntegerWidth> {
120    match expr {
121        Expr::Literal(Value::Int(value)) => Some(integer_width_for_literal(*value)),
122        Expr::Cast { ty, .. } | Expr::TypedLiteral { ty, .. } => integer_width_for_type(ty),
123        Expr::Binary {
124            op: BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide,
125            lhs,
126            rhs,
127        } => Some(integer_expr_width(lhs)?.max(integer_expr_width(rhs)?)),
128        _ => None,
129    }
130}
131
132fn integer_binary_width(lhs: &Expr, rhs: &Expr) -> Option<IntegerWidth> {
133    Some(integer_expr_width(lhs)?.max(integer_expr_width(rhs)?))
134}
135
136/// Apply a binary SQL operator to values that have already been evaluated.
137/// Execution engines use this when a hot path compiles expression traversal
138/// ahead of time but must retain the evaluator's exact comparison, numeric
139/// promotion, NULL, overflow, and division-by-zero semantics.
140pub fn eval_binary_values(op: BinaryOp, l: &Value, r: &Value) -> Result<Value> {
141    eval_binary_values_with_control(op, l, r, &ProductionControl::uncontrolled()).map(|value| {
142        value
143            .into_uncontrolled()
144            .expect("ordinary binary result has no reservation")
145    })
146}
147
148/// Evaluate the existing binary operator while owning every value producer and comparison workspace under one allowance.
149pub fn eval_binary_values_with_control(
150    op: BinaryOp,
151    l: &Value,
152    r: &Value,
153    control: &ProductionControl<'_>,
154) -> Result<Produced<Value>> {
155    control.check()?;
156    match op {
157        BinaryOp::Equal
158        | BinaryOp::NotEqual
159        | BinaryOp::Less
160        | BinaryOp::LessEqual
161        | BinaryOp::Greater
162        | BinaryOp::GreaterEqual => {
163            let value = eval_comparison_truth_with_control(op, l, r, control)?
164                .map(Value::Bool)
165                .unwrap_or(Value::Null);
166            control
167                .finish(value, control.empty_reservation())
168                .map_err(Into::into)
169        }
170        BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide => {
171            arith(l, r, op, control)
172        }
173    }
174}
175
176/// Evaluate an operator while retaining the integer type selected by SQL
177/// operator resolution. The dynamic [`Value`] carrier stores all integers as
178/// `i64`, so expression plans pass this width alongside the operands.
179pub fn eval_binary_values_with_integer_width(
180    op: BinaryOp,
181    l: &Value,
182    r: &Value,
183    integer_width: Option<IntegerWidth>,
184) -> Result<Value> {
185    eval_binary_values_with_integer_width_with_control(
186        op,
187        l,
188        r,
189        integer_width,
190        &ProductionControl::uncontrolled(),
191    )
192    .map(|value| {
193        value
194            .into_uncontrolled()
195            .expect("ordinary width-checked result has no reservation")
196    })
197}
198
199/// Preserve the selected integer width without separating an allocated result from its owner on errors.
200pub fn eval_binary_values_with_integer_width_with_control(
201    op: BinaryOp,
202    l: &Value,
203    r: &Value,
204    integer_width: Option<IntegerWidth>,
205    control: &ProductionControl<'_>,
206) -> Result<Produced<Value>> {
207    let result = eval_binary_values_with_control(op, l, r, control)?;
208    let Some(integer_width) = integer_width else {
209        return Ok(result);
210    };
211    let Value::Int(value) = *result else {
212        return Ok(result);
213    };
214    let in_range = match integer_width {
215        IntegerWidth::SmallInt => i16::try_from(value).is_ok(),
216        IntegerWidth::Integer => i32::try_from(value).is_ok(),
217        IntegerWidth::BigInt => true,
218    };
219    if in_range {
220        Ok(result)
221    } else {
222        Err(out_of_range(match integer_width {
223            IntegerWidth::SmallInt => "smallint",
224            IntegerWidth::Integer => "integer",
225            IntegerWidth::BigInt => "bigint",
226        }))
227    }
228}
229
230pub(super) enum EvalOperand<'a> {
231    Borrowed(&'a Value),
232    Owned(Value),
233}
234
235impl EvalOperand<'_> {
236    fn as_value(&self) -> &Value {
237        match self {
238            Self::Borrowed(value) => value,
239            Self::Owned(value) => value,
240        }
241    }
242}
243
244pub(super) fn eval_binary_borrowed(
245    op: BinaryOp,
246    lhs: &Expr,
247    rhs: &Expr,
248    ctx: &EvalContext<'_>,
249) -> Result<Option<Value>> {
250    if !matches!(
251        op,
252        BinaryOp::Equal
253            | BinaryOp::NotEqual
254            | BinaryOp::Less
255            | BinaryOp::LessEqual
256            | BinaryOp::Greater
257            | BinaryOp::GreaterEqual
258    ) {
259        return Ok(None);
260    }
261    let Some(l) = eval_operand_borrowed(lhs, ctx)? else {
262        return Ok(None);
263    };
264    let Some(r) = eval_operand_borrowed(rhs, ctx)? else {
265        return Ok(None);
266    };
267    let l = l.as_value();
268    let r = r.as_value();
269    Ok(Some(eval_comparison_op(op, l, r)?))
270}
271
272pub(super) fn eval_operand_borrowed<'a>(
273    expr: &Expr,
274    ctx: &EvalContext<'a>,
275) -> Result<Option<EvalOperand<'a>>> {
276    match expr {
277        Expr::Literal(value) => Ok(Some(EvalOperand::Owned(value.clone()))),
278        Expr::Param(i) => match i.checked_sub(1).and_then(|index| ctx.params.get(index)) {
279            Some(SQLParam::Scalar(value) | SQLParam::TypedScalar { value, .. }) => {
280                Ok(Some(EvalOperand::Borrowed(value)))
281            }
282            Some(SQLParam::Vector(_) | SQLParam::Tensor(_) | SQLParam::DeferredError { .. }) => {
283                Ok(None)
284            }
285            None => Err(SQLError::MissingParam(*i)),
286        },
287        Expr::Column(name) => {
288            if ctx.row_lookup()?.column_is_ambiguous(name) {
289                return Err(SQLError::AmbiguousColumn(name.clone()));
290            }
291            Ok(Some(match ctx.row_lookup()?.column(name) {
292                Some(value) => EvalOperand::Borrowed(value),
293                None => EvalOperand::Owned(Value::Null),
294            }))
295        }
296        Expr::QualifiedColumn { qualifier, column } => {
297            if ctx
298                .row_lookup()?
299                .qualified_column_is_ambiguous(qualifier, column)
300            {
301                return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
302            }
303            Ok(Some(
304                match ctx.row_lookup()?.qualified_column(qualifier, column) {
305                    Some(value) => EvalOperand::Borrowed(value),
306                    None => EvalOperand::Owned(Value::Null),
307                },
308            ))
309        }
310        _ => Ok(None),
311    }
312}
313
314/// Whether a condition holds. `NULL` does not; text, which reaches a condition only as an `unknown` literal or parameter, holds when `boolin` reads it as true (parse analysis rejects any other text); the engine's numeric retrieval predicates hold when non-zero.
315pub fn truthy(v: &Value) -> bool {
316    match v {
317        Value::Null => false,
318        Value::Bool(b) => *b,
319        Value::Int(n) => *n != 0,
320        Value::Float(f) => *f != 0.0,
321        Value::Decimal(d) => !d.is_zero(),
322        Value::Str(s) | Value::FixedChar(s) => super::parse_boolean_input(s) == Some(true),
323        _ => true,
324    }
325}
326
327/// `PostgreSQL` `division by zero` error (SQLSTATE 22012).
328pub(crate) fn division_by_zero() -> SQLError {
329    SQLError::Routine {
330        sqlstate: "22012".into(),
331        message: "division by zero".into(),
332    }
333}
334
335/// `PostgreSQL`'s error for a date, time, timestamp or interval beyond its type's range (SQLSTATE 22008).
336pub(crate) fn datetime_out_of_range(type_name: &str) -> SQLError {
337    SQLError::Routine {
338        sqlstate: "22008".into(),
339        message: format!("{type_name} out of range"),
340    }
341}
342
343/// `PostgreSQL` numeric overflow error (SQLSTATE 22003).
344pub(crate) fn out_of_range(type_name: &str) -> SQLError {
345    SQLError::Routine {
346        sqlstate: "22003".into(),
347        message: format!("{type_name} out of range"),
348    }
349}
350
351fn arith(
352    a: &Value,
353    b: &Value,
354    op: BinaryOp,
355    control: &ProductionControl<'_>,
356) -> Result<Produced<Value>> {
357    control.check()?;
358    // SQL three-valued logic: NULL `op` anything == NULL.
359    if matches!(a, Value::Null) || matches!(b, Value::Null) {
360        return control
361            .finish(Value::Null, control.empty_reservation())
362            .map_err(Into::into);
363    }
364    // Integer x integer is the overwhelmingly common analytical path.
365    // Resolve it before probing unrelated temporal / decimal / floating
366    // representations, while retaining PostgreSQL overflow behavior. The
367    // caller applies the SQL operator's int2/int4/int8 result width after this
368    // carrier-level i64 operation.
369    if let (Value::Int(li), Value::Int(ri)) = (a, b) {
370        let out = match op {
371            BinaryOp::Add => li.checked_add(*ri),
372            BinaryOp::Subtract => li.checked_sub(*ri),
373            BinaryOp::Multiply => li.checked_mul(*ri),
374            BinaryOp::Divide => {
375                if *ri == 0 {
376                    return Err(division_by_zero());
377                }
378                // Integer / integer in SQL truncates toward zero.
379                li.checked_div(*ri)
380            }
381            _ => {
382                return Err(SQLError::Internal(format!(
383                    "non-arithmetic operator {op:?} reached integer arithmetic"
384                )))
385            }
386        };
387        let value = out.map(Value::Int).ok_or_else(|| out_of_range("bigint"))?;
388        return control
389            .finish(value, control.empty_reservation())
390            .map_err(Into::into);
391    }
392    if matches!(op, BinaryOp::Subtract)
393        && matches!(a, Value::JsonB(_) | Value::Map(_) | Value::List(_))
394    {
395        if let Some(value) = super::json::json_delete_values_with_control(a, b, control)? {
396            return Ok(value);
397        }
398    }
399    if matches!(a, Value::Temporal(_)) || matches!(b, Value::Temporal(_)) {
400        let value = time::temporal_arith_with_control(a, b, op, control)?;
401        return control
402            .finish(value, control.empty_reservation())
403            .map_err(Into::into);
404    }
405    let has_decimal = matches!(a, Value::Decimal(_)) || matches!(b, Value::Decimal(_));
406    let has_float = matches!(a, Value::Float(_)) || matches!(b, Value::Float(_));
407    // PostgreSQL numeric promotion: double precision wins mixed
408    // float/numeric arithmetic. Exact decimal arithmetic only applies
409    // when no float operand is involved.
410    if has_decimal && !has_float {
411        return decimal_arith(a, b, op, control);
412    }
413    let value = super::eval_float_arithmetic_with_control(
414        op,
415        a,
416        b,
417        super::FloatWidth::DoublePrecision,
418        control,
419    )?;
420    control
421        .finish(value, control.empty_reservation())
422        .map_err(Into::into)
423}
424
425fn decimal_arith(
426    a: &Value,
427    b: &Value,
428    op: BinaryOp,
429    control: &ProductionControl<'_>,
430) -> Result<Produced<Value>> {
431    let left = super::conversion::to_decimal_with_control(a, control)?;
432    let right = super::conversion::to_decimal_with_control(b, control)?;
433    let value = match op {
434        BinaryOp::Add => left.checked_add_with_control(&right, control)?,
435        BinaryOp::Subtract => left.checked_sub_with_control(&right, control)?,
436        BinaryOp::Multiply => left.checked_mul_with_control(&right, control)?,
437        BinaryOp::Divide => {
438            if right.is_zero() {
439                return Err(division_by_zero());
440            }
441            left.checked_div_postgres_with_control(&right, control)?
442        }
443        _ => {
444            return Err(SQLError::Internal(format!(
445                "non-arithmetic operator {op:?} reached decimal arithmetic"
446            )))
447        }
448    }
449    .ok_or_else(|| out_of_range("numeric"))?;
450    let (value, memory) = value.into_parts();
451    control
452        .finish(Value::Decimal(value), memory)
453        .map_err(Into::into)
454}