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::super::enums::{EnumComparisonState, EnumLabelCatalog};
10use super::{BinaryOp, Result, SQLError, Value};
11use std::cmp::Ordering;
12use uqa_core::memory::ProductionControl;
13
14mod records;
15mod support;
16pub(in crate::expr) use support::{require_array_element_support, ComparisonSupport};
17
18#[cfg(test)]
19pub(in crate::expr) fn eval_comparison_op(op: BinaryOp, l: &Value, r: &Value) -> Result<Value> {
20    Ok(eval_comparison_truth(op, l, r)?
21        .map(Value::Bool)
22        .unwrap_or(Value::Null))
23}
24
25/// Compare two values without allocating an intermediate [`Value::Bool`]. `None` represents SQL UNKNOWN, including an undecided anonymous row comparison.
26#[inline]
27pub fn eval_comparison_truth(op: BinaryOp, l: &Value, r: &Value) -> Result<Option<bool>> {
28    eval_comparison_truth_with_control(op, l, r, &ProductionControl::uncontrolled())
29}
30
31/// Evaluate the same three-valued comparison while admitting the native numeric, JSONB and array comparison workspace to the caller's allowance.
32pub fn eval_comparison_truth_with_control(
33    op: BinaryOp,
34    l: &Value,
35    r: &Value,
36    control: &ProductionControl<'_>,
37) -> Result<Option<bool>> {
38    control.check()?;
39    let out = match op {
40        BinaryOp::Equal => values_equal_nullable_with_control(l, r, control)?,
41        BinaryOp::NotEqual => values_equal_nullable_with_control(l, r, control)?.map(|v| !v),
42        BinaryOp::Less => compare_nullable_with_control(l, r, control)?.map(|v| v.is_lt()),
43        BinaryOp::LessEqual => compare_nullable_with_control(l, r, control)?.map(|v| v.is_le()),
44        BinaryOp::Greater => compare_nullable_with_control(l, r, control)?.map(|v| v.is_gt()),
45        BinaryOp::GreaterEqual => compare_nullable_with_control(l, r, control)?.map(|v| v.is_ge()),
46        _ => {
47            return Err(SQLError::Internal(format!(
48                "non-comparison operator {op:?} reached comparison evaluation"
49            )))
50        }
51    };
52    Ok(out)
53}
54
55/// Borrow physical type metadata from scalar hooks even when the embedder provides no enum capability.
56pub fn eval_comparison_truth_with_engine(
57    op: BinaryOp,
58    left: &Value,
59    right: &Value,
60    control: &ProductionControl<'_>,
61    engine: Option<&dyn crate::expr::EngineHook>,
62    state: Option<&EnumComparisonState>,
63) -> Result<Option<bool>> {
64    eval_comparison_truth_with_deferred_state(op, left, right, control, engine, || state)
65}
66
67/// Compare complete SQL values selected by a consumer such as CASE, NULLIF or ANY. Anonymous record carriers retain record operators; three-valued ROW constructor comparison is selected separately by expression syntax.
68pub fn eval_value_comparison_truth_with_engine(
69    op: BinaryOp,
70    left: &Value,
71    right: &Value,
72    control: &ProductionControl<'_>,
73    engine: Option<&dyn crate::expr::EngineHook>,
74    state: Option<&EnumComparisonState>,
75) -> Result<Option<bool>> {
76    if matches!((left, right), (Value::Row(_), Value::Row(_))) {
77        eval_record_comparison_truth_with_engine(op, left, right, control, engine)
78    } else {
79        eval_comparison_truth_with_engine(op, left, right, control, engine, state)
80    }
81}
82
83/// Compare record operands selected by SQL syntax. A record-valued column, cast or parameter retains record field operators even when its carrier is an anonymous `Row`; only two uncast ROW constructors select three-valued row comparison.
84pub fn eval_record_comparison_truth_with_engine(
85    op: BinaryOp,
86    left: &Value,
87    right: &Value,
88    control: &ProductionControl<'_>,
89    engine: Option<&dyn crate::expr::EngineHook>,
90) -> Result<Option<bool>> {
91    control.check()?;
92    if matches!(left, Value::Null) || matches!(right, Value::Null) {
93        return Ok(None);
94    }
95    let catalog = engine.map(crate::expr::EngineValueCatalog);
96    let catalog = catalog
97        .as_ref()
98        .map(|catalog| catalog as &dyn crate::expr::SQLValueCatalog);
99    if matches!(op, BinaryOp::Equal | BinaryOp::NotEqual) {
100        let equal = equal_typed_values_with_enum_catalog(left, right, control, catalog)?;
101        return Ok(Some(if op == BinaryOp::Equal { equal } else { !equal }));
102    }
103    let order = compare_typed_values_with_enum_catalog(left, right, control, catalog, None)?;
104    Ok(Some(match op {
105        BinaryOp::Less => order.is_lt(),
106        BinaryOp::LessEqual => order.is_le(),
107        BinaryOp::Greater => order.is_gt(),
108        BinaryOp::GreaterEqual => order.is_ge(),
109        _ => {
110            return Err(SQLError::Internal(format!(
111                "non-comparison operator {op:?} reached record comparison"
112            )))
113        }
114    }))
115}
116
117/// Primitive scalar comparisons need no catalog or private enum call state. Resolve that state only when the evaluated carriers require the general comparison path; operand evaluation and its SQL ordering remain the caller's responsibility.
118#[inline]
119pub fn eval_comparison_truth_with_deferred_state<'a>(
120    op: BinaryOp,
121    left: &Value,
122    right: &Value,
123    control: &ProductionControl<'_>,
124    engine: Option<&dyn crate::expr::EngineHook>,
125    state: impl FnOnce() -> Option<&'a EnumComparisonState>,
126) -> Result<Option<bool>> {
127    control.check()?;
128    let primitive = match (left, right) {
129        (Value::Null, _) | (_, Value::Null) => Some(None),
130        (Value::Int(_), Value::Int(_))
131        | (Value::Float(_), Value::Float(_))
132        | (Value::Bool(_), Value::Bool(_)) => Some(Some(left.cmp(right))),
133        _ => None,
134    };
135    if let Some(order) = primitive {
136        return match op {
137            BinaryOp::Equal => Ok(order.map(Ordering::is_eq)),
138            BinaryOp::NotEqual => Ok(order.map(|order| !order.is_eq())),
139            BinaryOp::Less => Ok(order.map(Ordering::is_lt)),
140            BinaryOp::LessEqual => Ok(order.map(Ordering::is_le)),
141            BinaryOp::Greater => Ok(order.map(Ordering::is_gt)),
142            BinaryOp::GreaterEqual => Ok(order.map(Ordering::is_ge)),
143            _ => Err(SQLError::Internal(format!(
144                "non-comparison operator {op:?} reached comparison evaluation"
145            ))),
146        };
147    }
148    let catalog = engine.map(crate::expr::value_catalog::EngineValueCatalog);
149    eval_comparison_truth_with_enum_catalog(
150        op,
151        left,
152        right,
153        control,
154        catalog
155            .as_ref()
156            .map(|catalog| catalog as &dyn crate::expr::SQLValueCatalog),
157        state(),
158    )
159}
160
161/// Observe scalar and nested enums through the caller's catalog, preserving private scalar/row call state, shared type support state and ordinary three-valued rules.
162pub fn eval_comparison_truth_with_enum_catalog(
163    op: BinaryOp,
164    left: &Value,
165    right: &Value,
166    control: &ProductionControl<'_>,
167    enums: Option<&dyn super::super::enums::EnumLabelCatalog>,
168    state: Option<&super::super::enums::EnumComparisonState>,
169) -> Result<Option<bool>> {
170    control.check()?;
171    if matches!(op, BinaryOp::Equal | BinaryOp::NotEqual) {
172        return values_equal_nullable_with_catalog(left, right, control, enums)
173            .map(|equal| equal.map(|equal| if op == BinaryOp::Equal { equal } else { !equal }));
174    }
175    if !matches!(
176        op,
177        BinaryOp::Less | BinaryOp::LessEqual | BinaryOp::Greater | BinaryOp::GreaterEqual
178    ) {
179        return Err(SQLError::Internal(format!(
180            "non-comparison operator {op:?} reached comparison evaluation"
181        )));
182    }
183    let order = compare_nullable_with_catalog(left, right, control, enums, state)?;
184    Ok(order.map(|order| match op {
185        BinaryOp::Less => order.is_lt(),
186        BinaryOp::LessEqual => order.is_le(),
187        BinaryOp::Greater => order.is_gt(),
188        BinaryOp::GreaterEqual => order.is_ge(),
189        _ => unreachable!("only comparison operators reach comparison evaluation"),
190    }))
191}
192
193/// Two-valued equality treats SQL UNKNOWN as no match for CASE, NULLIF and membership probes.
194#[cfg(test)]
195pub(in crate::expr) fn values_equal(a: &Value, b: &Value) -> Result<bool> {
196    Ok(values_equal_nullable(a, b)? == Some(true))
197}
198
199pub fn values_equal_with_control(
200    a: &Value,
201    b: &Value,
202    control: &ProductionControl<'_>,
203) -> Result<bool> {
204    Ok(values_equal_nullable_with_control(a, b, control)? == Some(true))
205}
206
207#[cfg(test)]
208pub(in crate::expr) fn values_equal_nullable(a: &Value, b: &Value) -> Result<Option<bool>> {
209    values_equal_nullable_with_control(a, b, &ProductionControl::uncontrolled())
210}
211
212pub fn values_equal_nullable_with_control(
213    a: &Value,
214    b: &Value,
215    control: &ProductionControl<'_>,
216) -> Result<Option<bool>> {
217    values_equal_nullable_with_catalog(a, b, control, None)
218}
219
220fn values_equal_nullable_with_catalog(
221    a: &Value,
222    b: &Value,
223    control: &ProductionControl<'_>,
224    enums: Option<&dyn super::super::enums::EnumLabelCatalog>,
225) -> Result<Option<bool>> {
226    control.check()?;
227    if let Some(value) = super::super::enums::eval_comparison(BinaryOp::Equal, a, b, enums, None)? {
228        return match value {
229            Value::Null => Ok(None),
230            Value::Bool(value) => Ok(Some(value)),
231            _ => unreachable!("equality returns boolean or NULL"),
232        };
233    }
234    if let Some(order) = super::super::datums::compare_jsonb_with_control(a, b, control)? {
235        return Ok(Some(order.is_eq()));
236    }
237    let equal = match (a, b) {
238        (Value::Null, _) | (_, Value::Null) => None,
239        (Value::Datum(datum), _) => {
240            return values_equal_nullable_with_catalog(
241                &*super::super::datums::read_with_value_catalog_and_control(datum, enums, control)?,
242                b,
243                control,
244                enums,
245            )
246        }
247        (_, Value::Datum(datum)) => {
248            return values_equal_nullable_with_catalog(
249                a,
250                &*super::super::datums::read_with_value_catalog_and_control(datum, enums, control)?,
251                control,
252                enums,
253            )
254        }
255        (Value::Temporal(x), Value::Str(y)) | (Value::Str(y), Value::Temporal(x)) => Some(
256            x.parse_same_kind_in_order_with_control(
257                y,
258                crate::expr::transaction_timestamp_or_clock(),
259                crate::expr::temporal_date_order(),
260                control,
261            )?
262            .is_some_and(|parsed| x.cmp(&parsed).is_eq()),
263        ),
264        (Value::FixedChar(x), Value::Str(y)) | (Value::Str(y), Value::FixedChar(x)) => {
265            Some(compare_fixed_text(x, y, control)?.is_eq())
266        }
267        // 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.
268        (Value::Row(xs), Value::Row(ys)) => {
269            if xs.len() != ys.len() {
270                return Ok(Some(false));
271            }
272            let mut unknown = false;
273            for (x, y) in xs.iter().zip(ys) {
274                let equal = if matches!((x, y), (Value::Row(_), Value::Row(_))) {
275                    Some(equal_values(x, y, control, enums, false)?)
276                } else {
277                    values_equal_nullable_with_catalog(x, y, control, enums)?
278                };
279                match equal {
280                    Some(false) => return Ok(Some(false)),
281                    Some(true) => {}
282                    None => unknown = true,
283                }
284            }
285            if unknown {
286                None
287            } else {
288                Some(true)
289            }
290        }
291        _ => Some(equal_values(a, b, control, enums, false)?),
292    };
293    Ok(equal)
294}
295
296/// Compare with the existing two-valued selector convention that SQL UNKNOWN sorts as equal.
297pub fn compare_with_control(
298    a: &Value,
299    b: &Value,
300    control: &ProductionControl<'_>,
301) -> Result<Ordering> {
302    Ok(compare_nullable_with_control(a, b, control)?.unwrap_or(Ordering::Equal))
303}
304
305pub fn compare_nullable_with_control(
306    a: &Value,
307    b: &Value,
308    control: &ProductionControl<'_>,
309) -> Result<Option<Ordering>> {
310    compare_nullable_with_catalog(a, b, control, None, None)
311}
312
313fn compare_nullable_with_catalog(
314    a: &Value,
315    b: &Value,
316    control: &ProductionControl<'_>,
317    enums: Option<&dyn EnumLabelCatalog>,
318    state: Option<&EnumComparisonState>,
319) -> Result<Option<Ordering>> {
320    control.check()?;
321    if matches!(a, Value::Null) || matches!(b, Value::Null) {
322        return Ok(None);
323    }
324    if let Some(order) = super::super::enums::comparison_order(a, b, enums, state, false)? {
325        return Ok(Some(order));
326    }
327    if let Some(order) = super::super::datums::compare_jsonb_with_control(a, b, control)? {
328        return Ok(Some(order));
329    }
330    match (a, b) {
331        (Value::Null, _) | (_, Value::Null) => Ok(None),
332        (Value::Datum(datum), _) => compare_nullable_with_catalog(
333            &*super::super::datums::read_with_value_catalog_and_control(datum, enums, control)?,
334            b,
335            control,
336            enums,
337            state,
338        ),
339        (_, Value::Datum(datum)) => compare_nullable_with_catalog(
340            a,
341            &*super::super::datums::read_with_value_catalog_and_control(datum, enums, control)?,
342            control,
343            enums,
344            state,
345        ),
346        (
347            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
348            Value::Int(_) | Value::Float(_) | Value::Decimal(_),
349        )
350        | (Value::Bool(_), Value::Decimal(_))
351        | (Value::Decimal(_), Value::Bool(_))
352        | (Value::Str(_), Value::Str(_))
353        | (Value::FixedChar(_), Value::FixedChar(_))
354        | (Value::JsonB(_), Value::JsonB(_))
355        | (Value::Temporal(_), Value::Temporal(_))
356        | (Value::Bool(_), Value::Bool(_))
357        | (Value::Array(_), Value::Array(_))
358        | (Value::LegacyVector(_), Value::LegacyVector(_))
359        | (Value::List(_), Value::List(_))
360        | (Value::Record(_), Value::Record(_) | Value::Row(_))
361        | (Value::Row(_), Value::Record(_))
362        | (Value::Enum(_), Value::Enum(_)) => Ok(Some(compare_sql_values(a, b, control, enums)?)),
363        (Value::FixedChar(x), Value::Str(y)) | (Value::Str(x), Value::FixedChar(y)) => {
364            Ok(Some(compare_fixed_text(x, y, control)?))
365        }
366        (Value::Temporal(x), Value::Str(y)) => x
367            .parse_same_kind_in_order_with_control(
368                y,
369                crate::expr::transaction_timestamp_or_clock(),
370                crate::expr::temporal_date_order(),
371                control,
372            )?
373            .map(|parsed| Some(x.cmp(&parsed)))
374            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot compare {a:?} with {b:?}"))),
375        (Value::Str(x), Value::Temporal(y)) => y
376            .parse_same_kind_in_order_with_control(
377                x,
378                crate::expr::transaction_timestamp_or_clock(),
379                crate::expr::temporal_date_order(),
380                control,
381            )?
382            .map(|parsed| Some(parsed.cmp(y)))
383            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot compare {a:?} with {b:?}"))),
384        // Ordering is lexicographic; reaching NULL before a definite comparison leaves it unknown.
385        (Value::Row(xs), Value::Row(ys)) => compare_anonymous_row(xs, ys, control, enums, state),
386        (lhs, rhs) => Err(SQLError::TypeMismatch(format!(
387            "cannot compare {lhs:?} with {rhs:?}"
388        ))),
389    }
390}
391
392fn compare_anonymous_row(
393    left: &[Value],
394    right: &[Value],
395    control: &ProductionControl<'_>,
396    enums: Option<&dyn EnumLabelCatalog>,
397    state: Option<&EnumComparisonState>,
398) -> Result<Option<Ordering>> {
399    for (index, (left, right)) in left.iter().zip(right).enumerate() {
400        let field =
401            if matches!(left, Value::Enum(_) | Value::Datum(_)) && !matches!(right, Value::Null) {
402                state.map(|state| state.field(index))
403            } else {
404                None
405            };
406        let order = if matches!((left, right), (Value::Row(_), Value::Row(_))) {
407            Some(compare_typed_values_with_catalog(
408                left, right, control, enums,
409            )?)
410        } else {
411            compare_nullable_with_catalog(left, right, control, enums, field.as_deref())?
412        };
413        match order {
414            Some(Ordering::Equal) => {}
415            other => return Ok(other),
416        }
417    }
418    Ok(Some(left.len().cmp(&right.len())))
419}
420
421fn compare_sql_values(
422    left: &Value,
423    right: &Value,
424    control: &ProductionControl<'_>,
425    enums: Option<&dyn EnumLabelCatalog>,
426) -> Result<Ordering> {
427    // Primitive mixed float/integer and float/numeric operators select float8 inputs. Physical keys and already-bound container elements keep their exact carrier order.
428    if matches!(
429        (left, right),
430        (Value::Float(_), Value::Int(_) | Value::Decimal(_))
431            | (Value::Int(_) | Value::Decimal(_), Value::Float(_))
432    ) {
433        let left =
434            super::super::cast_value_from_with_control(left, "double precision", None, control)?;
435        let right =
436            super::super::cast_value_from_with_control(right, "double precision", None, control)?;
437        return Ok(left.cmp(&right));
438    }
439    compare_typed_values_with_catalog(left, right, control, enums)
440}
441
442/// 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.
443pub fn compare_typed_values_with_control(
444    left: &Value,
445    right: &Value,
446    control: &ProductionControl<'_>,
447) -> Result<Ordering> {
448    compare_typed_values_with_catalog(left, right, control, None)
449}
450
451/// Compare one physical ordering key with its private scalar enum call state. Nested array/record fields use the catalog's shared type comparison state.
452pub fn compare_typed_values_with_enum_catalog(
453    left: &Value,
454    right: &Value,
455    control: &ProductionControl<'_>,
456    enums: Option<&dyn EnumLabelCatalog>,
457    state: Option<&EnumComparisonState>,
458) -> Result<Ordering> {
459    control.check()?;
460    if !matches!(left, Value::Null) && !matches!(right, Value::Null) {
461        if let Some(order) =
462            super::super::enums::comparison_order(left, right, enums, state, false)?
463        {
464            return Ok(order);
465        }
466    }
467    compare_typed_values_with_catalog(left, right, control, enums)
468}
469
470/// Order internal equality groups by physical enum identity, including nested keys. This ordering is not exposed as SQL ORDER BY and never observes an enum label merely to eliminate duplicates.
471pub fn compare_grouping_values_with_enum_catalog(
472    left: &Value,
473    right: &Value,
474    control: &ProductionControl<'_>,
475    enums: Option<&dyn EnumLabelCatalog>,
476) -> Result<Ordering> {
477    compare_typed_values_for_purpose(left, right, control, enums, EnumKeyOrder::Identity)
478}
479
480#[derive(Clone, Copy)]
481enum EnumKeyOrder {
482    Declaration,
483    Identity,
484}
485
486fn enum_key_order(
487    left: &Value,
488    right: &Value,
489    enums: Option<&dyn EnumLabelCatalog>,
490    order: EnumKeyOrder,
491) -> Result<Option<Ordering>> {
492    if matches!(order, EnumKeyOrder::Identity) {
493        if let Some(enums) = enums {
494            if let (Some(left), Some(right)) = (
495                super::super::enums::comparison_identity(enums, left)?,
496                super::super::enums::comparison_identity(enums, right)?,
497            ) {
498                return Ok(Some(left.cmp(&right)));
499            }
500        }
501        return Ok(None);
502    }
503    super::super::enums::comparison_order(left, right, enums, None, true)
504}
505
506fn compare_typed_values_with_catalog(
507    left: &Value,
508    right: &Value,
509    control: &ProductionControl<'_>,
510    enums: Option<&dyn EnumLabelCatalog>,
511) -> Result<Ordering> {
512    compare_typed_values_for_purpose(left, right, control, enums, EnumKeyOrder::Declaration)
513}
514
515fn compare_typed_values_for_purpose(
516    left: &Value,
517    right: &Value,
518    control: &ProductionControl<'_>,
519    enums: Option<&dyn EnumLabelCatalog>,
520    enum_order: EnumKeyOrder,
521) -> Result<Ordering> {
522    control.check()?;
523    if let Some(order) = super::super::datums::compare_jsonb_with_control(left, right, control)? {
524        return Ok(order);
525    }
526    match (left, right) {
527        (Value::Null, Value::Null) => return Ok(Ordering::Equal),
528        (Value::Null, _) => return Ok(Ordering::Greater),
529        (_, Value::Null) => return Ok(Ordering::Less),
530        _ => {}
531    }
532    if let Some(order) = enum_key_order(left, right, enums, enum_order)? {
533        return Ok(order);
534    }
535    match (left, right) {
536        (Value::Datum(datum), _) => {
537            return compare_typed_values_for_purpose(
538                &*super::super::datums::read_with_value_catalog_and_control(datum, enums, control)?,
539                right,
540                control,
541                enums,
542                enum_order,
543            )
544        }
545        (_, Value::Datum(datum)) => {
546            return compare_typed_values_for_purpose(
547                left,
548                &*super::super::datums::read_with_value_catalog_and_control(datum, enums, control)?,
549                control,
550                enums,
551                enum_order,
552            )
553        }
554        (Value::Array(left), Value::Array(right)) => {
555            validate_array_element_types(left, right)?;
556            return left.cmp_by_with_control(right, control, |left, right, control| {
557                compare_typed_values_for_purpose(left, right, control, enums, enum_order)
558            });
559        }
560        (Value::Record(_) | Value::Row(_), Value::Record(_) | Value::Row(_)) => {
561            return records::compare(
562                left,
563                right,
564                enums,
565                control,
566                match enum_order {
567                    EnumKeyOrder::Declaration => ComparisonSupport::Ordering,
568                    EnumKeyOrder::Identity => ComparisonSupport::Equality,
569                },
570                Ordering::Equal,
571                |left, right| {
572                    let order =
573                        compare_typed_values_for_purpose(left, right, control, enums, enum_order)?;
574                    Ok((!order.is_eq()).then_some(order))
575                },
576            );
577        }
578        (Value::List(left), Value::List(right)) => {
579            return compare_sequence(left.iter(), right.iter(), control, enums, enum_order);
580        }
581        // Enum operators are declared on one enum type; binding coerces every other operand to it.
582        (Value::Enum(left), Value::Enum(right)) if left.type_oid() == right.type_oid() => {
583            return Ok(left.key().cmp(right.key()));
584        }
585        (Value::Enum(_), _) | (_, Value::Enum(_)) => {
586            return Err(SQLError::Internal(format!(
587                "enum comparison reached operands of different types: {} and {}",
588                comparison_operand_type(left),
589                comparison_operand_type(right)
590            )));
591        }
592        (Value::Json(_), Value::Json(_)) => {
593            return Err(ComparisonSupport::Ordering.error("42883", "json".into()));
594        }
595        _ => {}
596    }
597    for value in [left, right] {
598        if let Value::LegacyVector(vector) = value {
599            validate_legacy_vector_comparison(vector)?;
600        }
601    }
602    left.cmp_with_control(right, control).map_err(Into::into)
603}
604
605fn comparison_operand_type(value: &Value) -> String {
606    match value {
607        Value::Enum(label) => format!("enum type OID {}", label.type_oid()),
608        other => super::super::diagnostics::value_type_name(other).to_owned(),
609    }
610}
611
612/// Validate the layout required by the `oidvector` scalar equality, ordering and hashing operators. Array operators on `int2vector` permit dimensionless arrays.
613pub fn validate_legacy_vector_comparison(vector: &uqa_core::LegacyVectorValue) -> Result<()> {
614    if vector.kind() == uqa_core::LegacyVectorKind::Oid && !vector.has_vector_layout() {
615        return Err(SQLError::Routine {
616            sqlstate: "42804".into(),
617            message: "array is not a valid oidvector".into(),
618        });
619    }
620    Ok(())
621}
622
623/// Declared SQL keys whose runtime values may require a fallible comparison operator.
624pub fn type_comparison_can_fail(ty: &crate::ast::ColumnType) -> bool {
625    use crate::ast::ColumnType;
626    match ty {
627        ColumnType::OidVector
628        | ColumnType::Record
629        | ColumnType::Composite(_)
630        | ColumnType::Array(_) => true,
631        ColumnType::Domain { base: element, .. } => type_comparison_can_fail(element),
632        _ => false,
633    }
634}
635
636/// Whether an opaque value key can suppress a SQL operator failure. A single such input remains legal until an operator compares it.
637pub fn value_comparison_can_fail(value: &Value) -> bool {
638    match value {
639        Value::Datum(_) => true,
640        Value::LegacyVector(vector) => {
641            vector.kind() == uqa_core::LegacyVectorKind::Oid && !vector.has_vector_layout()
642        }
643        Value::Array(array) => {
644            array.element_type_oid().is_some()
645                || array.elements().iter().any(value_comparison_can_fail)
646        }
647        Value::Row(values) => {
648            values.field_types().is_some() || values.iter().any(value_comparison_can_fail)
649        }
650        Value::List(values) => values.iter().any(value_comparison_can_fail),
651        Value::Record(fields) => {
652            fields.type_oid().is_some()
653                || fields
654                    .iter()
655                    .any(|(_, value)| value_comparison_can_fail(value))
656        }
657        _ => false,
658    }
659}
660
661/// Compare already-bound group/hash keys without enum output. Physical enum
662/// identities use the same equality as `enum_eq`, including inside containers.
663pub fn equal_typed_values_with_enum_catalog(
664    left: &Value,
665    right: &Value,
666    control: &ProductionControl<'_>,
667    enums: Option<&dyn super::super::enums::EnumLabelCatalog>,
668) -> Result<bool> {
669    equal_values(left, right, control, enums, true)
670}
671
672fn equal_values(
673    left: &Value,
674    right: &Value,
675    control: &ProductionControl<'_>,
676    enums: Option<&dyn super::super::enums::EnumLabelCatalog>,
677    typed: bool,
678) -> Result<bool> {
679    control.check()?;
680    if let Some(order) = super::super::datums::compare_jsonb_with_control(left, right, control)? {
681        return Ok(order.is_eq());
682    }
683    if matches!(left, Value::Null) || matches!(right, Value::Null) {
684        return Ok(matches!((left, right), (Value::Null, Value::Null)));
685    }
686    if let Some(enums) = enums {
687        if let (Some(left), Some(right)) = (
688            super::super::enums::comparison_identity(enums, left)?,
689            super::super::enums::comparison_identity(enums, right)?,
690        ) {
691            return Ok(left == right);
692        }
693    }
694    match (left, right) {
695        (Value::Datum(datum), _) => equal_values(
696            &*super::super::datums::read_with_value_catalog_and_control(datum, enums, control)?,
697            right,
698            control,
699            enums,
700            typed,
701        ),
702        (_, Value::Datum(datum)) => equal_values(
703            left,
704            &*super::super::datums::read_with_value_catalog_and_control(datum, enums, control)?,
705            control,
706            enums,
707            typed,
708        ),
709        (Value::Array(left), Value::Array(right)) => {
710            validate_array_element_types(left, right)?;
711            left.eq_by_with_control(right, control, |left, right, control| {
712                equal_values(left, right, control, enums, typed)
713            })
714        }
715        (Value::Record(_) | Value::Row(_), Value::Record(_) | Value::Row(_)) => records::compare(
716            left,
717            right,
718            enums,
719            control,
720            ComparisonSupport::Equality,
721            true,
722            |left, right| Ok((!equal_values(left, right, control, enums, typed)?).then_some(false)),
723        ),
724        (Value::List(left), Value::List(right)) => {
725            equal_sequence(left.iter(), right.iter(), control, enums, typed)
726        }
727        (Value::Json(_), Value::Json(_)) => {
728            Err(ComparisonSupport::Equality.error("42883", "json".into()))
729        }
730        _ if typed => Ok(compare_typed_values_with_control(left, right, control)?.is_eq()),
731        _ => Ok(compare_sql_values(left, right, control, enums)?.is_eq()),
732    }
733}
734
735fn validate_array_element_types(
736    left: &uqa_core::ArrayValue,
737    right: &uqa_core::ArrayValue,
738) -> Result<()> {
739    if matches!((left.element_type_oid(), right.element_type_oid()), (Some(left), Some(right)) if left != right)
740    {
741        return Err(SQLError::Routine {
742            sqlstate: "42804".into(),
743            message: "cannot compare arrays of different element types".into(),
744        });
745    }
746    Ok(())
747}
748
749fn equal_sequence<'a>(
750    mut left: impl Iterator<Item = &'a Value>,
751    mut right: impl Iterator<Item = &'a Value>,
752    control: &ProductionControl<'_>,
753    enums: Option<&dyn super::super::enums::EnumLabelCatalog>,
754    typed: bool,
755) -> Result<bool> {
756    loop {
757        control.check()?;
758        match (left.next(), right.next()) {
759            (Some(left), Some(right)) if equal_values(left, right, control, enums, typed)? => {}
760            (None, None) => return Ok(true),
761            _ => return Ok(false),
762        }
763    }
764}
765
766fn compare_sequence<'a>(
767    mut left: impl Iterator<Item = &'a Value>,
768    mut right: impl Iterator<Item = &'a Value>,
769    control: &ProductionControl<'_>,
770    enums: Option<&dyn EnumLabelCatalog>,
771    enum_order: EnumKeyOrder,
772) -> Result<Ordering> {
773    loop {
774        control.check()?;
775        let ordering = match (left.next(), right.next()) {
776            (Some(left), Some(right)) => {
777                compare_typed_values_for_purpose(left, right, control, enums, enum_order)?
778            }
779            (Some(_), None) => Ordering::Greater,
780            (None, Some(_)) => Ordering::Less,
781            (None, None) => return Ok(Ordering::Equal),
782        };
783        if !ordering.is_eq() {
784            return Ok(ordering);
785        }
786    }
787}
788
789fn compare_fixed_text(
790    left: &str,
791    right: &str,
792    control: &ProductionControl<'_>,
793) -> Result<Ordering> {
794    fn trim<'a>(text: &'a str, control: &ProductionControl<'_>) -> Result<&'a [u8]> {
795        let mut bytes = text.as_bytes();
796        let mut checked = 0;
797        while bytes.last() == Some(&b' ') {
798            if checked % 4096 == 0 {
799                control.check()?;
800            }
801            bytes = &bytes[..bytes.len() - 1];
802            checked += 1;
803        }
804        Ok(bytes)
805    }
806    let left = trim(left, control)?;
807    let right = trim(right, control)?;
808    for (left, right) in left.chunks(4096).zip(right.chunks(4096)) {
809        control.check()?;
810        let ordering = left.cmp(right);
811        if !ordering.is_eq() {
812            return Ok(ordering);
813        }
814    }
815    control.check()?;
816    Ok(left.len().cmp(&right.len()))
817}
818
819#[cfg(test)]
820mod tests;