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