Skip to main content

uqa_sql/expr/enums/
physical.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Physical enum operations read admitted OIDs without repeating enum input or output.
8
9use std::cmp::Ordering;
10
11use uqa_core::Value;
12
13use super::{enum_label_oid, type_name, EnumLabelCatalog};
14use crate::ast::BinaryOp;
15use crate::error::{Result, SQLError};
16
17mod state;
18#[cfg(test)]
19mod tests;
20pub use state::{EnumComparisonState, EnumTypeComparisonStates};
21
22pub(in crate::expr) fn comparison_order(
23    left: &Value,
24    right: &Value,
25    catalog: Option<&dyn EnumLabelCatalog>,
26    state: Option<&EnumComparisonState>,
27    container: bool,
28) -> Result<Option<Ordering>> {
29    let Some(catalog) = catalog else {
30        return Ok(None);
31    };
32    let (Some(left_oid), Some(right_oid)) = (
33        comparison_identity(catalog, left)?,
34        comparison_identity(catalog, right)?,
35    ) else {
36        return Ok(None);
37    };
38    let shared = if container {
39        let type_oid = match left {
40            Value::Enum(label) => label.type_oid(),
41            Value::Datum(datum) => datum.type_oid(),
42            _ => unreachable!("enum identity has a declared type"),
43        };
44        catalog
45            .enum_type_comparison_states()
46            .map(|states| states.get(type_oid))
47    } else {
48        None
49    };
50    compare_oids(
51        Some(catalog),
52        left_oid,
53        right_oid,
54        shared.as_deref().or(state),
55    )
56    .map(Some)
57}
58
59/// Classify an already-bound enum value and read its physical equality/hash
60/// identity. A retained label need not exist: these operators never call enum output.
61pub fn comparison_identity(catalog: &dyn EnumLabelCatalog, value: &Value) -> Result<Option<u32>> {
62    match value {
63        Value::Enum(_) => oid(Some(catalog), value),
64        Value::Datum(datum) if catalog.enum_type_labels(datum.type_oid())?.is_some() => {
65            super::super::datums::enum_label_oid(datum).map(Some)
66        }
67        _ => Ok(None),
68    }
69}
70
71/// Evaluate a scalar enum operator against physical identities. Other operand
72/// types remain with the ordinary SQL comparison owner; no label output occurs.
73pub fn eval_comparison(
74    op: BinaryOp,
75    left: &Value,
76    right: &Value,
77    catalog: Option<&dyn EnumLabelCatalog>,
78    state: Option<&EnumComparisonState>,
79) -> Result<Option<Value>> {
80    if matches!(
81        op,
82        BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide
83    ) {
84        return Ok(None);
85    }
86    if matches!(left, Value::Null) || matches!(right, Value::Null) {
87        return Ok(Some(Value::Null));
88    }
89    let Some(catalog) = catalog else {
90        return Ok(None);
91    };
92    let (Some(left), Some(right)) = (
93        comparison_identity(catalog, left)?,
94        comparison_identity(catalog, right)?,
95    ) else {
96        return Ok(None);
97    };
98    let value = match op {
99        BinaryOp::Equal => left == right,
100        BinaryOp::NotEqual => left != right,
101        _ => {
102            let order = compare_oids(Some(catalog), left, right, state)?;
103            match op {
104                BinaryOp::Less => order.is_lt(),
105                BinaryOp::LessEqual => order.is_le(),
106                BinaryOp::Greater => order.is_gt(),
107                BinaryOp::GreaterEqual => order.is_ge(),
108                _ => unreachable!("arithmetic has no enum comparison"),
109            }
110        }
111    };
112    Ok(Some(Value::Bool(value)))
113}
114
115pub(super) fn oid(catalog: Option<&dyn EnumLabelCatalog>, value: &Value) -> Result<Option<u32>> {
116    match value {
117        Value::Null => Ok(None),
118        Value::Enum(label) => label
119            .label_oid()
120            .map(Ok)
121            .unwrap_or_else(|| enum_label_oid(catalog, label))
122            .map(Some),
123        Value::Datum(datum) => super::super::datums::enum_label_oid(datum).map(Some),
124        other => Err(SQLError::Internal(format!(
125            "enum support function received {other:?}"
126        ))),
127    }
128}
129
130pub(super) fn equal(
131    catalog: Option<&dyn EnumLabelCatalog>,
132    left: &Value,
133    right: &Value,
134) -> Result<bool> {
135    if let Some(order) = native_order(left, right) {
136        return Ok(order.is_eq());
137    }
138    Ok(oid(catalog, left)? == oid(catalog, right)?)
139}
140
141fn native_order(left: &Value, right: &Value) -> Option<Ordering> {
142    match (left, right) {
143        (Value::Enum(left), Value::Enum(right)) if left.type_oid() == right.type_oid() => {
144            Some(left.key().cmp(right.key()))
145        }
146        _ => None,
147    }
148}
149
150pub(super) fn compare(
151    catalog: Option<&dyn EnumLabelCatalog>,
152    left: &Value,
153    right: &Value,
154    state: Option<&EnumComparisonState>,
155) -> Result<Ordering> {
156    let legacy_keys = catalog.is_none()
157        && matches!((left, right), (Value::Enum(left), Value::Enum(right)) if left.label_oid().is_none() && right.label_oid().is_none());
158    if state.is_none() || legacy_keys {
159        if let Some(order) = native_order(left, right) {
160            return Ok(order);
161        }
162    }
163    let (Some(left), Some(right)) = (oid(catalog, left)?, oid(catalog, right)?) else {
164        return Err(SQLError::Internal(
165            "enum comparison lost a strict argument".into(),
166        ));
167    };
168    compare_oids(catalog, left, right, state)
169}
170
171fn compare_oids(
172    catalog: Option<&dyn EnumLabelCatalog>,
173    left: u32,
174    right: u32,
175    state: Option<&EnumComparisonState>,
176) -> Result<Ordering> {
177    // Equal and even OIDs take PostgreSQL's catalog-free paths, including
178    // retained values whose output would report an invalid label identity.
179    if left == right || (left & 1 == 0 && right & 1 == 0) {
180        return Ok(left.cmp(&right));
181    }
182    let left_entry = catalog
183        .map(|catalog| catalog.enum_label_position(left))
184        .transpose()?
185        .flatten();
186    let type_oid = if let Some(oid) = state.and_then(EnumComparisonState::cached_type) {
187        oid
188    } else {
189        let (oid, _) = left_entry.ok_or_else(|| SQLError::Routine {
190            sqlstate: "22P03".into(),
191            message: format!("invalid internal value for enum: {left}"),
192        })?;
193        state.map_or(oid, |state| state.remember_type(oid))
194    };
195    let Some((_, left_position)) = left_entry.filter(|(oid, _)| *oid == type_oid) else {
196        return Err(missing_cached_label(catalog, type_oid, left)?);
197    };
198    let Some((_, right_position)) = catalog
199        .map(|catalog| catalog.enum_label_position(right))
200        .transpose()?
201        .flatten()
202        .filter(|(right_type, _)| *right_type == type_oid)
203    else {
204        return Err(missing_cached_label(catalog, type_oid, right)?);
205    };
206    Ok(left_position.cmp(&right_position))
207}
208
209fn missing_cached_label(
210    catalog: Option<&dyn EnumLabelCatalog>,
211    type_oid: u32,
212    label_oid: u32,
213) -> Result<SQLError> {
214    Ok(SQLError::Routine {
215        sqlstate: "XX000".into(),
216        message: format!(
217            "enum value {label_oid} not found in cache for enum {}",
218            type_name(catalog, type_oid)?
219        ),
220    })
221}