Skip to main content

uqa_sql/expr/
enums.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Enum label input and output, ordered label functions and I/O casts through the statement's catalog.
8//!
9//! Values carry an immutable label key; the catalog supplies the current label text, so
10//! `ALTER TYPE ... RENAME VALUE` never rewrites stored values. Native comparisons use key order;
11//! physical enum support functions observe raw OIDs and consult catalog order only when needed.
12
13use std::sync::Arc;
14
15use uqa_core::memory::{Produced, ProductionControl, ProductionVec};
16use uqa_core::{ArrayValue, EnumLabelKey, EnumValue, Value};
17
18use super::{Result, SQLError};
19use crate::ast::ColumnType;
20
21mod functions;
22mod physical;
23pub use functions::{
24    enum_function_value, enum_function_value_with_control, enum_function_value_with_state,
25};
26pub(in crate::expr) use physical::comparison_order;
27pub use physical::{
28    comparison_identity, eval_comparison, EnumComparisonState, EnumTypeComparisonStates,
29};
30
31/// One label of an enum type.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct EnumTypeLabel {
34    pub oid: u32,
35    pub key: EnumLabelKey,
36    pub label: String,
37}
38
39/// The labels of one enum type in ascending key order, which is declaration order.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct EnumTypeLabels {
42    pub type_oid: u32,
43    pub labels: Vec<EnumTypeLabel>,
44}
45
46impl EnumTypeLabels {
47    pub fn by_key(&self, key: &EnumLabelKey) -> Option<&EnumTypeLabel> {
48        self.labels
49            .binary_search_by(|label| label.key.cmp(key))
50            .ok()
51            .map(|index| &self.labels[index])
52    }
53
54    pub fn by_text(&self, text: &str) -> Option<&EnumTypeLabel> {
55        self.labels.iter().find(|label| label.label == text)
56    }
57
58    fn value(&self, label: &EnumTypeLabel) -> Value {
59        Value::Enum(
60            EnumValue::new(self.type_oid, label.key.clone()).with_label_oid(Some(label.oid)),
61        )
62    }
63
64    fn value_with_control(
65        &self,
66        label: &EnumTypeLabel,
67        control: &ProductionControl<'_>,
68    ) -> Result<Produced<Value>> {
69        let memory = control.reserve(label.key.as_bytes().len())?;
70        Ok(control.finish(self.value(label), memory)?)
71    }
72}
73
74/// The enum-facing name of the shared SQL value catalog.
75pub use super::SQLValueCatalog as EnumLabelCatalog;
76
77pub(crate) fn enum_value_from_oid(
78    catalog: Option<&dyn EnumLabelCatalog>,
79    label_oid: u32,
80) -> Result<Value> {
81    catalog
82        .map(|catalog| catalog.enum_value_by_oid(label_oid))
83        .transpose()?
84        .flatten()
85        .map(Value::Enum)
86        .ok_or_else(|| SQLError::Routine {
87            sqlstate: "22P03".into(),
88            message: format!("invalid internal value for enum: {label_oid}"),
89        })
90}
91
92fn catalog_unavailable(type_oid: u32) -> SQLError {
93    SQLError::Internal(format!(
94        "enum type OID {type_oid} is not available in the statement catalog"
95    ))
96}
97
98pub(crate) fn labels(
99    catalog: Option<&dyn EnumLabelCatalog>,
100    type_oid: u32,
101) -> Result<Arc<EnumTypeLabels>> {
102    catalog
103        .ok_or_else(|| catalog_unavailable(type_oid))?
104        .enum_type_labels(type_oid)?
105        .ok_or_else(|| catalog_unavailable(type_oid))
106}
107
108pub(crate) fn type_name(catalog: Option<&dyn EnumLabelCatalog>, type_oid: u32) -> Result<String> {
109    catalog
110        .ok_or_else(|| catalog_unavailable(type_oid))?
111        .enum_type_name(type_oid)?
112        .ok_or_else(|| catalog_unavailable(type_oid))
113}
114
115/// `check_safe_enum_use`: a label added by the current transaction to a type it did not create is unusable until commit.
116fn check_safe(
117    catalog: Option<&dyn EnumLabelCatalog>,
118    labels: &EnumTypeLabels,
119    label: &EnumTypeLabel,
120) -> Result<()> {
121    if catalog.is_some_and(|catalog| catalog.enum_label_uncommitted(label.oid)) {
122        return Err(SQLError::Diagnostic {
123            sqlstate: "55P04".into(),
124            message: format!(
125                "unsafe use of new value \"{}\" of enum type {}",
126                label.label,
127                type_name(catalog, labels.type_oid)?
128            ),
129            detail: None,
130            hint: Some("New enum values must be committed before they can be used.".into()),
131        });
132    }
133    Ok(())
134}
135
136fn invalid_internal_value(value: &EnumValue) -> SQLError {
137    SQLError::Internal(format!(
138        "invalid internal value for enum type OID {}",
139        value.type_oid()
140    ))
141}
142
143/// The catalog label of a value, which carries the label OID and current text.
144pub(crate) fn enum_label(
145    catalog: Option<&dyn EnumLabelCatalog>,
146    value: &EnumValue,
147) -> Result<EnumTypeLabel> {
148    labels(catalog, value.type_oid())?
149        .by_key(value.key())
150        .cloned()
151        .ok_or_else(|| invalid_internal_value(value))
152}
153
154/// `enum_out`: the current label of a value.
155pub fn enum_label_text(
156    catalog: Option<&dyn EnumLabelCatalog>,
157    value: &EnumValue,
158) -> Result<String> {
159    enum_label(catalog, value).map(|label| label.label)
160}
161
162/// Catalog OID of an enum value's label, the datum `PostgreSQL` stores for the value.
163pub fn enum_label_oid(catalog: Option<&dyn EnumLabelCatalog>, value: &EnumValue) -> Result<u32> {
164    enum_label(catalog, value).map(|label| label.oid)
165}
166
167/// `enum_in`: the label with exactly this text. Case and surrounding spaces are significant.
168pub fn enum_value_from_text(
169    catalog: Option<&dyn EnumLabelCatalog>,
170    type_oid: u32,
171    text: &str,
172) -> Result<Value> {
173    let labels = labels(catalog, type_oid)?;
174    let Some(label) = labels.by_text(text) else {
175        return Err(SQLError::Routine {
176            sqlstate: "22P02".into(),
177            message: format!(
178                "invalid input value for enum {}: \"{text}\"",
179                type_name(catalog, type_oid)?
180            ),
181        });
182    };
183    check_safe(catalog, &labels, label)?;
184    Ok(labels.value(label))
185}
186
187/// `enum_first` and `enum_last` read only the argument's type; the returned label must be usable.
188pub fn enum_endpoint(
189    catalog: Option<&dyn EnumLabelCatalog>,
190    type_oid: u32,
191    last: bool,
192) -> Result<Value> {
193    enum_endpoint_with_control(catalog, type_oid, last, &ProductionControl::uncontrolled())
194        .map(|value| value.into_uncontrolled().expect("ordinary enum endpoint"))
195}
196
197fn enum_endpoint_with_control(
198    catalog: Option<&dyn EnumLabelCatalog>,
199    type_oid: u32,
200    last: bool,
201    control: &ProductionControl<'_>,
202) -> Result<Produced<Value>> {
203    control.check()?;
204    let labels = labels(catalog, type_oid)?;
205    let label = if last {
206        labels.labels.last()
207    } else {
208        labels.labels.first()
209    };
210    let Some(label) = label else {
211        return Err(SQLError::Routine {
212            sqlstate: "55000".into(),
213            message: format!("enum {} contains no values", type_name(catalog, type_oid)?),
214        });
215    };
216    check_safe(catalog, &labels, label)?;
217    labels.value_with_control(label, control)
218}
219
220/// `enum_range(lower, upper)`: labels from `lower` through `upper` inclusive, where a missing bound is open. A lower bound after the upper bound yields an empty array. Every returned label must be usable.
221pub fn enum_range(
222    catalog: Option<&dyn EnumLabelCatalog>,
223    type_oid: u32,
224    lower: Option<&EnumValue>,
225    upper: Option<&EnumValue>,
226) -> Result<Value> {
227    let lower = lower
228        .map(|value| enum_label_oid(catalog, value))
229        .transpose()?;
230    let upper = upper
231        .map(|value| enum_label_oid(catalog, value))
232        .transpose()?;
233    range_by_oid(catalog, type_oid, lower, upper)
234}
235
236fn range_by_oid(
237    catalog: Option<&dyn EnumLabelCatalog>,
238    type_oid: u32,
239    lower: Option<u32>,
240    upper: Option<u32>,
241) -> Result<Value> {
242    range_by_oid_with_control(
243        catalog,
244        type_oid,
245        lower,
246        upper,
247        &ProductionControl::uncontrolled(),
248    )
249    .map(|value| value.into_uncontrolled().expect("ordinary enum range"))
250}
251
252fn range_by_oid_with_control(
253    catalog: Option<&dyn EnumLabelCatalog>,
254    type_oid: u32,
255    lower: Option<u32>,
256    upper: Option<u32>,
257    control: &ProductionControl<'_>,
258) -> Result<Produced<Value>> {
259    control.check()?;
260    let labels = labels(catalog, type_oid)?;
261    let mut elements = ProductionVec::new(*control);
262    let mut include = lower.is_none_or(|oid| oid == 0);
263    for label in &labels.labels {
264        control.check()?;
265        if lower == Some(label.oid) {
266            include = true;
267        }
268        if include {
269            check_safe(catalog, &labels, label)?;
270            elements.push_produced(labels.value_with_control(label, control)?)?;
271        }
272        if upper == Some(label.oid) {
273            break;
274        }
275    }
276    let array = ArrayValue::try_new_with_control(elements.finish()?, control)?
277        .ok_or_else(|| SQLError::Internal("enum range array has invalid dimensions".into()))?;
278    let (array, memory) = array.into_parts();
279    Ok(control.finish(
280        Value::Array(array.with_element_type_oid(Some(type_oid))),
281        memory,
282    )?)
283}
284
285/// Replace every enum carrier inside a value by its label text, as the output functions of containers do.
286pub fn render_enum_labels(catalog: Option<&dyn EnumLabelCatalog>, value: &Value) -> Result<Value> {
287    map_enum_values(value, &mut |label| {
288        enum_label_text(catalog, label).map(Value::Str)
289    })
290}
291
292/// Retain physical identities without repeating enum input checks or changing admitted values. Legacy carriers resolve their missing OID once; existing identities remain usable after catalog changes.
293pub(crate) fn retain_enum_oids(
294    catalog: Option<&dyn EnumLabelCatalog>,
295    value: &Value,
296) -> Result<Value> {
297    map_enum_values(value, &mut |label| {
298        let oid = match label.label_oid() {
299            Some(oid) => oid,
300            None => enum_label_oid(catalog, label)?,
301        };
302        Ok(Value::Enum(label.clone().with_label_oid(Some(oid))))
303    })
304}
305
306fn map_enum_values(
307    value: &Value,
308    convert: &mut dyn FnMut(&EnumValue) -> Result<Value>,
309) -> Result<Value> {
310    Ok(match value {
311        Value::Enum(label) => convert(label)?,
312        Value::Array(array) => Value::Array(map_array(array, |element| {
313            map_enum_values(element, convert)
314        })?),
315        Value::List(values) => Value::List(
316            values
317                .iter()
318                .map(|element| map_enum_values(element, convert))
319                .collect::<Result<_>>()?,
320        ),
321        Value::Row(values) => Value::Row(
322            values.clone().with_values(
323                values
324                    .iter()
325                    .map(|element| map_enum_values(element, convert))
326                    .collect::<Result<_>>()?,
327            )?,
328        ),
329        Value::Record(fields) => Value::Record(uqa_core::RecordValue::from_parts(
330            fields
331                .iter()
332                .map(|(name, element)| Ok((name.clone(), map_enum_values(element, convert)?)))
333                .collect::<Result<_>>()?,
334            fields.type_oid(),
335        )),
336        Value::Map(fields) => Value::Map(
337            fields
338                .iter()
339                .map(|(name, element)| Ok((name.clone(), map_enum_values(element, convert)?)))
340                .collect::<Result<_>>()?,
341        ),
342        other => other.clone(),
343    })
344}
345
346/// Rebuild an array with the same bounds after converting every leaf element; nested dimensions are `List` elements.
347fn map_array(
348    array: &ArrayValue,
349    mut convert: impl FnMut(&Value) -> Result<Value>,
350) -> Result<ArrayValue> {
351    fn leaves(
352        values: &[Value],
353        convert: &mut dyn FnMut(&Value) -> Result<Value>,
354    ) -> Result<Vec<Value>> {
355        values
356            .iter()
357            .map(|value| match value {
358                Value::List(values) => leaves(values, convert).map(Value::List),
359                value => convert(value),
360            })
361            .collect()
362    }
363    let elements = leaves(array.elements(), &mut convert)?;
364    ArrayValue::with_lower_bounds(elements, array.lower_bounds().to_vec())
365        .filter(|converted| converted.dimensions() == array.dimensions())
366        .map(|converted| converted.with_element_type_oid(array.element_type_oid()))
367        .ok_or_else(|| SQLError::Internal("enum array conversion changed array dimensions".into()))
368}
369
370/// Whether a value contains an enum carrier anywhere.
371pub fn contains_enum_carrier(value: &Value) -> bool {
372    enum_carrier_matches(value, &|_| true)
373}
374
375pub(crate) fn has_missing_enum_oid(value: &Value) -> bool {
376    enum_carrier_matches(value, &|label| label.label_oid().is_none())
377}
378
379fn enum_carrier_matches(value: &Value, predicate: &dyn Fn(&EnumValue) -> bool) -> bool {
380    match value {
381        Value::Enum(label) => predicate(label),
382        Value::Array(array) => array
383            .elements()
384            .iter()
385            .any(|value| enum_carrier_matches(value, predicate)),
386        Value::List(values) => values
387            .iter()
388            .any(|value| enum_carrier_matches(value, predicate)),
389        Value::Row(values) => values
390            .iter()
391            .any(|value| enum_carrier_matches(value, predicate)),
392        Value::Record(fields) => fields
393            .iter()
394            .any(|(_, value)| enum_carrier_matches(value, predicate)),
395        Value::Map(fields) => fields
396            .values()
397            .any(|value| enum_carrier_matches(value, predicate)),
398        _ => false,
399    }
400}
401
402/// Whether a built-in call converts its arguments with their types' output functions, so enum arguments contribute their current labels. `||` concatenates text only when neither operand is an array; with an array operand it appends or prepends the enum value itself.
403#[must_use]
404pub fn call_applies_output_functions(name: &str, arguments: &[(Option<String>, Value)]) -> bool {
405    match name {
406        "concat_op" => !arguments
407            .iter()
408            .any(|(_, value)| matches!(value, Value::Array(_) | Value::List(_))),
409        "concat" | "concat_ws" | "format" | "quote_literal" | "quote_nullable" | "to_json"
410        | "to_jsonb" | "row_to_json" | "array_to_json" | "json_build_object"
411        | "json_build_array" | "jsonb_build_object" | "jsonb_build_array" | "array_to_string" => {
412            true
413        }
414        _ => false,
415    }
416}
417
418/// Render the enum carriers of an output-function call's arguments as their labels.
419pub fn render_call_arguments(
420    catalog: Option<&dyn EnumLabelCatalog>,
421    arguments: Vec<(Option<String>, Value)>,
422) -> Result<Vec<(Option<String>, Value)>> {
423    arguments
424        .into_iter()
425        .map(|(name, value)| {
426            if contains_enum_carrier(&value) {
427                Ok((name, render_enum_labels(catalog, &value)?))
428            } else {
429                Ok((name, value))
430            }
431        })
432        .collect()
433}
434
435/// Arguments handed to host-registered functions: enum values become their labels, as procedural languages receive them through the type's output function.
436pub fn render_host_arguments(
437    catalog: Option<&dyn EnumLabelCatalog>,
438    arguments: &[Value],
439) -> Result<Vec<Value>> {
440    arguments
441        .iter()
442        .map(|value| {
443            if contains_enum_carrier(value) {
444                render_enum_labels(catalog, value)
445            } else {
446                Ok(value.clone())
447            }
448        })
449        .collect()
450}
451
452/// Whether a declared type is an enum or an array whose leaf is an enum.
453#[must_use]
454pub fn is_enum_bearing(ty: &ColumnType) -> bool {
455    match ty {
456        ColumnType::Enum(_) => true,
457        ColumnType::Array(element) => is_enum_bearing(element),
458        _ => false,
459    }
460}
461
462/// Convert an `unknown` literal to an enum or enum-array type with the type's input function, as `PostgreSQL` does when it coerces an untyped constant during parse analysis. Returns `None` for any other target.
463pub fn fold_unknown_literal(
464    catalog: Option<&dyn EnumLabelCatalog>,
465    value: &Value,
466    target: &ColumnType,
467) -> Result<Option<Value>> {
468    match (target, value) {
469        (ColumnType::Enum(_) | ColumnType::Array(_), Value::Null) if is_enum_bearing(target) => {
470            Ok(Some(Value::Null))
471        }
472        (ColumnType::Enum(reference), Value::Str(text)) => {
473            enum_value_from_text(catalog, reference.oid, text).map(Some)
474        }
475        (ColumnType::Array(element), Value::Str(text)) if is_enum_bearing(element) => {
476            let mut leaf = element.as_ref();
477            while let ColumnType::Array(inner) = leaf {
478                leaf = inner;
479            }
480            let ColumnType::Enum(reference) = leaf else {
481                return Ok(None);
482            };
483            let parsed = super::casting::parse_pg_array_literal(text)?;
484            map_array(&parsed, |element| match element {
485                Value::Null => Ok(Value::Null),
486                Value::Str(text) => enum_value_from_text(catalog, reference.oid, text),
487                other => Err(SQLError::Internal(format!(
488                    "array literal parsing produced a non-text element {other:?}"
489                ))),
490            })
491            .map(|array| {
492                Some(Value::Array(
493                    array.with_element_type_oid(Some(reference.oid)),
494                ))
495            })
496        }
497        _ => Ok(None),
498    }
499}
500
501/// The string category accepted by automatic I/O conversion casts.
502fn string_category(ty: &ColumnType) -> bool {
503    match ty {
504        ColumnType::Text
505        | ColumnType::Varchar(_)
506        | ColumnType::Bpchar
507        | ColumnType::Character(_)
508        | ColumnType::Name => true,
509        ColumnType::Domain { base, .. } => string_category(base),
510        _ => false,
511    }
512}
513
514fn cannot_cast(source: &str, target: &str) -> SQLError {
515    SQLError::Routine {
516        sqlstate: "42846".into(),
517        message: format!("cannot cast type {source} to {target}"),
518    }
519}
520
521/// Cast a value to an enum type: identity for the same type, `enum_in` for unknown literals and string-category sources, and 42846 otherwise. Returns `None` when the target is not an enum.
522pub fn cast_to_enum(
523    catalog: Option<&dyn EnumLabelCatalog>,
524    value: &Value,
525    source: Option<&ColumnType>,
526    target: &ColumnType,
527) -> Result<Option<Value>> {
528    let ColumnType::Enum(reference) = target else {
529        return Ok(None);
530    };
531    let source_name = |source: Option<&ColumnType>| -> Result<String> {
532        Ok(match (value, source) {
533            (Value::Enum(label), _) => type_name(catalog, label.type_oid())?,
534            (_, Some(ColumnType::Enum(source))) => type_name(catalog, source.oid)?,
535            (_, Some(source)) => source.sql_name(),
536            (_, None) => "unknown".into(),
537        })
538    };
539    match value {
540        Value::Null => Ok(Some(Value::Null)),
541        Value::Enum(label) if label.type_oid() == reference.oid => Ok(Some(value.clone())),
542        Value::Datum(datum) if datum.type_oid() == reference.oid => Ok(Some(value.clone())),
543        Value::Str(text) | Value::FixedChar(text) if source.is_none_or(string_category) => {
544            enum_value_from_text(catalog, reference.oid, text).map(Some)
545        }
546        _ => Err(cannot_cast(
547            &source_name(source)?,
548            &type_name(catalog, reference.oid)?,
549        )),
550    }
551}
552
553/// Cast an enum value to a string-category type through its label; any other target has no cast.
554pub fn enum_output_for_cast(
555    catalog: Option<&dyn EnumLabelCatalog>,
556    label: &EnumValue,
557    target: &ColumnType,
558) -> Result<Value> {
559    if string_category(target) {
560        return enum_label_text(catalog, label).map(Value::Str);
561    }
562    Err(cannot_cast(
563        &type_name(catalog, label.type_oid())?,
564        &match target {
565            ColumnType::Enum(reference) => type_name(catalog, reference.oid)?,
566            other => other.sql_name(),
567        },
568    ))
569}