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