Skip to main content

uqa_sql/expr/binary/
comparison.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL NULL and row comparison rules share the native controlled value owners.
8
9use super::{BinaryOp, Result, SQLError, Value};
10use std::cmp::Ordering;
11use uqa_core::memory::ProductionControl;
12
13pub(in crate::expr) fn eval_comparison_op(op: BinaryOp, l: &Value, r: &Value) -> Result<Value> {
14    Ok(eval_comparison_truth(op, l, r)?
15        .map(Value::Bool)
16        .unwrap_or(Value::Null))
17}
18
19/// Compare two values without allocating an intermediate [`Value::Bool`]. `None` represents SQL UNKNOWN, including an undecided anonymous row comparison.
20#[inline]
21pub fn eval_comparison_truth(op: BinaryOp, l: &Value, r: &Value) -> Result<Option<bool>> {
22    eval_comparison_truth_with_control(op, l, r, &ProductionControl::uncontrolled())
23}
24
25/// Evaluate the same three-valued comparison while admitting the native numeric, JSONB and array comparison workspace to the caller's allowance.
26pub fn eval_comparison_truth_with_control(
27    op: BinaryOp,
28    l: &Value,
29    r: &Value,
30    control: &ProductionControl<'_>,
31) -> Result<Option<bool>> {
32    control.check()?;
33    let out = match op {
34        BinaryOp::Equal => values_equal_nullable_with_control(l, r, control)?,
35        BinaryOp::NotEqual => values_equal_nullable_with_control(l, r, control)?.map(|v| !v),
36        BinaryOp::Less => compare_nullable_with_control(l, r, control)?.map(|v| v.is_lt()),
37        BinaryOp::LessEqual => compare_nullable_with_control(l, r, control)?.map(|v| v.is_le()),
38        BinaryOp::Greater => compare_nullable_with_control(l, r, control)?.map(|v| v.is_gt()),
39        BinaryOp::GreaterEqual => compare_nullable_with_control(l, r, control)?.map(|v| v.is_ge()),
40        _ => {
41            return Err(SQLError::Internal(format!(
42                "non-comparison operator {op:?} reached comparison evaluation"
43            )))
44        }
45    };
46    Ok(out)
47}
48
49/// Two-valued equality treats SQL UNKNOWN as no match for CASE, NULLIF and membership probes.
50pub(in crate::expr) fn values_equal(a: &Value, b: &Value) -> Result<bool> {
51    Ok(values_equal_nullable(a, b)? == Some(true))
52}
53
54pub fn values_equal_with_control(
55    a: &Value,
56    b: &Value,
57    control: &ProductionControl<'_>,
58) -> Result<bool> {
59    Ok(values_equal_nullable_with_control(a, b, control)? == Some(true))
60}
61
62pub(in crate::expr) fn values_equal_nullable(a: &Value, b: &Value) -> Result<Option<bool>> {
63    values_equal_nullable_with_control(a, b, &ProductionControl::uncontrolled())
64}
65
66pub fn values_equal_nullable_with_control(
67    a: &Value,
68    b: &Value,
69    control: &ProductionControl<'_>,
70) -> Result<Option<bool>> {
71    control.check()?;
72    let equal = match (a, b) {
73        (Value::Null, _) | (_, Value::Null) => None,
74        (Value::Temporal(x), Value::Str(y)) | (Value::Str(y), Value::Temporal(x)) => Some(
75            x.parse_same_kind_in_order_with_control(
76                y,
77                crate::expr::transaction_timestamp_or_clock(),
78                crate::expr::temporal_date_order(),
79                control,
80            )?
81            .is_some_and(|parsed| x.cmp(&parsed).is_eq()),
82        ),
83        (Value::FixedChar(x), Value::Str(y)) | (Value::Str(y), Value::FixedChar(x)) => {
84            Some(compare_fixed_text(x, y, control)?.is_eq())
85        }
86        // Anonymous rows use SQL three-valued equality: a definite mismatch wins over an earlier NULL field. Native arrays and stored records instead use total element equality through the value owner below.
87        (Value::Row(xs), Value::Row(ys)) => {
88            if xs.len() != ys.len() {
89                return Ok(Some(false));
90            }
91            let mut unknown = false;
92            for (x, y) in xs.iter().zip(ys) {
93                match values_equal_nullable_with_control(x, y, control)? {
94                    Some(false) => return Ok(Some(false)),
95                    Some(true) => {}
96                    None => unknown = true,
97                }
98            }
99            if unknown {
100                None
101            } else {
102                Some(true)
103            }
104        }
105        _ => Some(equal_sql_values(a, b, control)?),
106    };
107    Ok(equal)
108}
109
110/// Compare with the existing two-valued selector convention that SQL UNKNOWN sorts as equal.
111pub fn compare_with_control(
112    a: &Value,
113    b: &Value,
114    control: &ProductionControl<'_>,
115) -> Result<Ordering> {
116    Ok(compare_nullable_with_control(a, b, control)?.unwrap_or(Ordering::Equal))
117}
118
119pub fn compare_nullable_with_control(
120    a: &Value,
121    b: &Value,
122    control: &ProductionControl<'_>,
123) -> Result<Option<Ordering>> {
124    control.check()?;
125    match (a, b) {
126        (Value::Null, _) | (_, Value::Null) => Ok(None),
127        (
128            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
129            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
130        )
131        | (Value::Bool(_), Value::Decimal(_))
132        | (Value::Decimal(_), Value::Bool(_))
133        | (Value::Str(_), Value::Str(_))
134        | (Value::FixedChar(_), Value::FixedChar(_))
135        | (Value::JsonB(_), Value::JsonB(_))
136        | (Value::Temporal(_), Value::Temporal(_))
137        | (Value::Bool(_), Value::Bool(_))
138        | (Value::Array(_), Value::Array(_))
139        | (Value::LegacyVector(_), Value::LegacyVector(_))
140        | (Value::List(_), Value::List(_))
141        | (Value::Record(_), Value::Record(_) | Value::Row(_))
142        | (Value::Row(_), Value::Record(_))
143        | (Value::Enum(_), Value::Enum(_)) => Ok(Some(compare_sql_values(a, b, control)?)),
144        (Value::FixedChar(x), Value::Str(y)) | (Value::Str(x), Value::FixedChar(y)) => {
145            Ok(Some(compare_fixed_text(x, y, control)?))
146        }
147        (Value::Temporal(x), Value::Str(y)) => x
148            .parse_same_kind_in_order_with_control(
149                y,
150                crate::expr::transaction_timestamp_or_clock(),
151                crate::expr::temporal_date_order(),
152                control,
153            )?
154            .map(|parsed| Some(x.cmp(&parsed)))
155            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot compare {a:?} with {b:?}"))),
156        (Value::Str(x), Value::Temporal(y)) => y
157            .parse_same_kind_in_order_with_control(
158                x,
159                crate::expr::transaction_timestamp_or_clock(),
160                crate::expr::temporal_date_order(),
161                control,
162            )?
163            .map(|parsed| Some(parsed.cmp(y)))
164            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot compare {a:?} with {b:?}"))),
165        // Ordering is lexicographic; reaching NULL before a definite comparison leaves it unknown.
166        (Value::Row(xs), Value::Row(ys)) => {
167            for (x, y) in xs.iter().zip(ys) {
168                match compare_nullable_with_control(x, y, control)? {
169                    Some(Ordering::Equal) => {}
170                    Some(other) => return Ok(Some(other)),
171                    None => return Ok(None),
172                }
173            }
174            Ok(Some(xs.len().cmp(&ys.len())))
175        }
176        (lhs, rhs) => Err(SQLError::TypeMismatch(format!(
177            "cannot compare {lhs:?} with {rhs:?}"
178        ))),
179    }
180}
181
182fn compare_sql_values(
183    left: &Value,
184    right: &Value,
185    control: &ProductionControl<'_>,
186) -> Result<Ordering> {
187    // Primitive mixed float/integer and float/numeric operators select float8 inputs. Physical keys and already-bound container elements keep their exact carrier order.
188    if matches!(
189        (left, right),
190        (Value::Float(_), Value::Int(_) | Value::Decimal(_))
191            | (Value::Int(_) | Value::Decimal(_), Value::Float(_))
192    ) {
193        let left =
194            super::super::cast_value_from_with_control(left, "double precision", None, control)?;
195        let right =
196            super::super::cast_value_from_with_control(right, "double precision", None, control)?;
197        return Ok(left.cmp(&right));
198    }
199    compare_typed_values_with_control(left, right, control)
200}
201
202/// Compare already-bound values, preserving type operator failures and total container NULL semantics. Callers supply top-level NULL placement and must apply operator-selected casts first.
203pub fn compare_typed_values_with_control(
204    left: &Value,
205    right: &Value,
206    control: &ProductionControl<'_>,
207) -> Result<Ordering> {
208    control.check()?;
209    match (left, right) {
210        (Value::Null, Value::Null) => return Ok(Ordering::Equal),
211        (Value::Null, _) => return Ok(Ordering::Greater),
212        (_, Value::Null) => return Ok(Ordering::Less),
213        (Value::Array(left), Value::Array(right)) => {
214            return left.cmp_by_with_control(right, control, compare_typed_values_with_control);
215        }
216        (Value::Record(left), Value::Record(right)) => {
217            return compare_sequence(
218                left.iter().map(|(_, v)| v),
219                right.iter().map(|(_, v)| v),
220                control,
221            );
222        }
223        // A composite value compared with an anonymous row uses the record operators, which order NULL fields after all others.
224        (Value::Record(left), Value::Row(right)) => {
225            return compare_sequence(left.iter().map(|(_, v)| v), right.iter(), control);
226        }
227        (Value::Row(left), Value::Record(right)) => {
228            return compare_sequence(left.iter(), right.iter().map(|(_, v)| v), control);
229        }
230        (Value::Row(left), Value::Row(right)) => {
231            return compare_sequence(left.iter(), right.iter(), control);
232        }
233        (Value::List(left), Value::List(right)) => {
234            return compare_sequence(left.iter(), right.iter(), control);
235        }
236        // Enum operators are declared on one enum type; binding coerces every other operand to it.
237        (Value::Enum(left), Value::Enum(right)) if left.type_oid() == right.type_oid() => {
238            return Ok(left.key().cmp(right.key()));
239        }
240        (Value::Enum(_), _) | (_, Value::Enum(_)) => {
241            return Err(SQLError::Internal(format!(
242                "enum comparison reached operands of different types: {} and {}",
243                comparison_operand_type(left),
244                comparison_operand_type(right)
245            )));
246        }
247        _ => {}
248    }
249    for value in [left, right] {
250        if let Value::LegacyVector(vector) = value {
251            validate_legacy_vector_comparison(vector)?;
252        }
253    }
254    left.cmp_with_control(right, control).map_err(Into::into)
255}
256
257fn comparison_operand_type(value: &Value) -> String {
258    match value {
259        Value::Enum(label) => format!("enum type OID {}", label.type_oid()),
260        other => super::super::diagnostics::value_type_name(other).to_owned(),
261    }
262}
263
264/// Validate the layout required by the `oidvector` scalar equality, ordering and hashing operators. Array operators on `int2vector` permit dimensionless arrays.
265pub fn validate_legacy_vector_comparison(vector: &uqa_core::LegacyVectorValue) -> Result<()> {
266    if vector.kind() == uqa_core::LegacyVectorKind::Oid && !vector.has_vector_layout() {
267        return Err(SQLError::Routine {
268            sqlstate: "42804".into(),
269            message: "array is not a valid oidvector".into(),
270        });
271    }
272    Ok(())
273}
274
275/// Declared SQL keys whose runtime values may require a fallible comparison operator.
276pub fn type_comparison_can_fail(ty: &crate::ast::ColumnType) -> bool {
277    use crate::ast::ColumnType;
278    match ty {
279        ColumnType::OidVector | ColumnType::Record => true,
280        ColumnType::Array(element) | ColumnType::Domain { base: element, .. } => {
281            type_comparison_can_fail(element)
282        }
283        _ => false,
284    }
285}
286
287/// Whether an opaque value key can suppress a SQL operator failure. A single such input remains legal until an operator compares it.
288pub fn value_comparison_can_fail(value: &Value) -> bool {
289    match value {
290        Value::LegacyVector(vector) => {
291            vector.kind() == uqa_core::LegacyVectorKind::Oid && !vector.has_vector_layout()
292        }
293        Value::Array(array) => array.elements().iter().any(value_comparison_can_fail),
294        Value::Row(values) => values.iter().any(value_comparison_can_fail),
295        Value::List(values) => values.iter().any(value_comparison_can_fail),
296        Value::Record(fields) => fields
297            .iter()
298            .any(|(_, value)| value_comparison_can_fail(value)),
299        _ => false,
300    }
301}
302
303fn equal_sql_values(left: &Value, right: &Value, control: &ProductionControl<'_>) -> Result<bool> {
304    control.check()?;
305    match (left, right) {
306        (Value::Array(left), Value::Array(right)) => {
307            left.eq_by_with_control(right, control, equal_sql_values)
308        }
309        (Value::Record(left), Value::Record(right)) => equal_sequence(
310            left.iter().map(|(_, v)| v),
311            right.iter().map(|(_, v)| v),
312            control,
313        ),
314        (Value::Record(left), Value::Row(right)) => {
315            equal_sequence(left.iter().map(|(_, v)| v), right.iter(), control)
316        }
317        (Value::Row(left), Value::Record(right)) => {
318            equal_sequence(left.iter(), right.iter().map(|(_, v)| v), control)
319        }
320        (Value::Row(left), Value::Row(right)) => equal_sequence(left.iter(), right.iter(), control),
321        (Value::List(left), Value::List(right)) => {
322            equal_sequence(left.iter(), right.iter(), control)
323        }
324        _ => Ok(compare_sql_values(left, right, control)?.is_eq()),
325    }
326}
327
328fn equal_sequence<'a>(
329    mut left: impl Iterator<Item = &'a Value>,
330    mut right: impl Iterator<Item = &'a Value>,
331    control: &ProductionControl<'_>,
332) -> Result<bool> {
333    loop {
334        control.check()?;
335        match (left.next(), right.next()) {
336            (Some(left), Some(right)) if equal_sql_values(left, right, control)? => {}
337            (None, None) => return Ok(true),
338            _ => return Ok(false),
339        }
340    }
341}
342
343fn compare_sequence<'a>(
344    mut left: impl Iterator<Item = &'a Value>,
345    mut right: impl Iterator<Item = &'a Value>,
346    control: &ProductionControl<'_>,
347) -> Result<Ordering> {
348    loop {
349        control.check()?;
350        let ordering = match (left.next(), right.next()) {
351            (Some(left), Some(right)) => compare_typed_values_with_control(left, right, control)?,
352            (Some(_), None) => Ordering::Greater,
353            (None, Some(_)) => Ordering::Less,
354            (None, None) => return Ok(Ordering::Equal),
355        };
356        if !ordering.is_eq() {
357            return Ok(ordering);
358        }
359    }
360}
361
362fn compare_fixed_text(
363    left: &str,
364    right: &str,
365    control: &ProductionControl<'_>,
366) -> Result<Ordering> {
367    fn trim<'a>(text: &'a str, control: &ProductionControl<'_>) -> Result<&'a [u8]> {
368        let mut bytes = text.as_bytes();
369        let mut checked = 0;
370        while bytes.last() == Some(&b' ') {
371            if checked % 4096 == 0 {
372                control.check()?;
373            }
374            bytes = &bytes[..bytes.len() - 1];
375            checked += 1;
376        }
377        Ok(bytes)
378    }
379    let left = trim(left, control)?;
380    let right = trim(right, control)?;
381    for (left, right) in left.chunks(4096).zip(right.chunks(4096)) {
382        control.check()?;
383        let ordering = left.cmp(right);
384        if !ordering.is_eq() {
385            return Ok(ordering);
386        }
387    }
388    control.check()?;
389    Ok(left.len().cmp(&right.len()))
390}
391
392#[cfg(test)]
393mod tests;