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. Comparisons use the key order and
11//! need no catalog access.
12
13use std::sync::Arc;
14
15use uqa_core::{ArrayValue, EnumLabelKey, EnumValue, Value};
16
17use super::{Result, SQLError};
18use crate::ast::ColumnType;
19
20mod functions;
21pub use functions::enum_function_value;
22
23/// One label of an enum type.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct EnumTypeLabel {
26    pub oid: u32,
27    pub key: EnumLabelKey,
28    pub label: String,
29}
30
31/// The labels of one enum type in ascending key order, which is declaration order.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct EnumTypeLabels {
34    pub type_oid: u32,
35    pub labels: Vec<EnumTypeLabel>,
36}
37
38impl EnumTypeLabels {
39    pub fn by_key(&self, key: &EnumLabelKey) -> Option<&EnumTypeLabel> {
40        self.labels
41            .binary_search_by(|label| label.key.cmp(key))
42            .ok()
43            .map(|index| &self.labels[index])
44    }
45
46    pub fn by_text(&self, text: &str) -> Option<&EnumTypeLabel> {
47        self.labels.iter().find(|label| label.label == text)
48    }
49
50    fn value(&self, label: &EnumTypeLabel) -> Value {
51        Value::Enum(EnumValue::new(self.type_oid, label.key.clone()))
52    }
53}
54
55/// Catalog access for enum label input and output, supplied by the statement's execution context and by catalog-aware binding.
56pub trait EnumLabelCatalog {
57    /// The labels of one enum type in the statement's catalog generation, or `None` when the catalog has no such type.
58    fn enum_type_labels(&self, type_oid: u32) -> Result<Option<Arc<EnumTypeLabels>>>;
59
60    /// Whether the current transaction added this label to a type that it did not create. `PostgreSQL` rejects such a label until the transaction commits.
61    fn enum_label_uncommitted(&self, label_oid: u32) -> bool;
62
63    /// `format_type_be` of the type, which qualifies a type hidden by the search path.
64    fn enum_type_name(&self, type_oid: u32) -> Result<Option<String>>;
65
66    /// Whether the statement catalog defines any enum type; binding skips enum literal validation otherwise.
67    fn has_enum_types(&self) -> bool;
68}
69
70fn catalog_unavailable(type_oid: u32) -> SQLError {
71    SQLError::Internal(format!(
72        "enum type OID {type_oid} is not available in the statement catalog"
73    ))
74}
75
76pub(crate) fn labels(
77    catalog: Option<&dyn EnumLabelCatalog>,
78    type_oid: u32,
79) -> Result<Arc<EnumTypeLabels>> {
80    catalog
81        .ok_or_else(|| catalog_unavailable(type_oid))?
82        .enum_type_labels(type_oid)?
83        .ok_or_else(|| catalog_unavailable(type_oid))
84}
85
86pub(crate) fn type_name(catalog: Option<&dyn EnumLabelCatalog>, type_oid: u32) -> Result<String> {
87    catalog
88        .ok_or_else(|| catalog_unavailable(type_oid))?
89        .enum_type_name(type_oid)?
90        .ok_or_else(|| catalog_unavailable(type_oid))
91}
92
93/// `check_safe_enum_use`: a label added by the current transaction to a type it did not create is unusable until commit.
94fn check_safe(
95    catalog: Option<&dyn EnumLabelCatalog>,
96    labels: &EnumTypeLabels,
97    label: &EnumTypeLabel,
98) -> Result<()> {
99    if catalog.is_some_and(|catalog| catalog.enum_label_uncommitted(label.oid)) {
100        return Err(SQLError::Diagnostic {
101            sqlstate: "55P04".into(),
102            message: format!(
103                "unsafe use of new value \"{}\" of enum type {}",
104                label.label,
105                type_name(catalog, labels.type_oid)?
106            ),
107            detail: None,
108            hint: Some("New enum values must be committed before they can be used.".into()),
109        });
110    }
111    Ok(())
112}
113
114fn invalid_internal_value(value: &EnumValue) -> SQLError {
115    SQLError::Internal(format!(
116        "invalid internal value for enum type OID {}",
117        value.type_oid()
118    ))
119}
120
121/// The catalog label of a value, which carries the label OID and current text.
122pub(crate) fn enum_label(
123    catalog: Option<&dyn EnumLabelCatalog>,
124    value: &EnumValue,
125) -> Result<EnumTypeLabel> {
126    labels(catalog, value.type_oid())?
127        .by_key(value.key())
128        .cloned()
129        .ok_or_else(|| invalid_internal_value(value))
130}
131
132/// `enum_out`: the current label of a value.
133pub fn enum_label_text(
134    catalog: Option<&dyn EnumLabelCatalog>,
135    value: &EnumValue,
136) -> Result<String> {
137    enum_label(catalog, value).map(|label| label.label)
138}
139
140/// Catalog OID of an enum value's label, the datum `PostgreSQL` stores for the value.
141pub fn enum_label_oid(catalog: Option<&dyn EnumLabelCatalog>, value: &EnumValue) -> Result<u32> {
142    enum_label(catalog, value).map(|label| label.oid)
143}
144
145/// `enum_in`: the label with exactly this text. Case and surrounding spaces are significant.
146pub fn enum_value_from_text(
147    catalog: Option<&dyn EnumLabelCatalog>,
148    type_oid: u32,
149    text: &str,
150) -> Result<Value> {
151    let labels = labels(catalog, type_oid)?;
152    let Some(label) = labels.by_text(text) else {
153        return Err(SQLError::Routine {
154            sqlstate: "22P02".into(),
155            message: format!(
156                "invalid input value for enum {}: \"{text}\"",
157                type_name(catalog, type_oid)?
158            ),
159        });
160    };
161    check_safe(catalog, &labels, label)?;
162    Ok(labels.value(label))
163}
164
165/// `enum_first` and `enum_last` read only the argument's type; the returned label must be usable.
166pub fn enum_endpoint(
167    catalog: Option<&dyn EnumLabelCatalog>,
168    type_oid: u32,
169    last: bool,
170) -> Result<Value> {
171    let labels = labels(catalog, type_oid)?;
172    let label = if last {
173        labels.labels.last()
174    } else {
175        labels.labels.first()
176    };
177    let Some(label) = label else {
178        return Err(SQLError::Routine {
179            sqlstate: "55000".into(),
180            message: format!("enum {} contains no values", type_name(catalog, type_oid)?),
181        });
182    };
183    check_safe(catalog, &labels, label)?;
184    Ok(labels.value(label))
185}
186
187/// `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.
188pub fn enum_range(
189    catalog: Option<&dyn EnumLabelCatalog>,
190    type_oid: u32,
191    lower: Option<&EnumValue>,
192    upper: Option<&EnumValue>,
193) -> Result<Value> {
194    let labels = labels(catalog, type_oid)?;
195    let mut elements = Vec::new();
196    for label in &labels.labels {
197        if lower.is_some_and(|lower| &label.key < lower.key()) {
198            continue;
199        }
200        if upper.is_some_and(|upper| &label.key > upper.key()) {
201            break;
202        }
203        check_safe(catalog, &labels, label)?;
204        elements.push(labels.value(label));
205    }
206    ArrayValue::try_new(elements)
207        .map(Value::Array)
208        .ok_or_else(|| SQLError::Internal("enum range array has invalid dimensions".into()))
209}
210
211/// Replace every enum carrier inside a value by its label text, as the output functions of containers do.
212pub fn render_enum_labels(catalog: Option<&dyn EnumLabelCatalog>, value: &Value) -> Result<Value> {
213    Ok(match value {
214        Value::Enum(label) => Value::Str(enum_label_text(catalog, label)?),
215        Value::Array(array) => Value::Array(map_array(array, |element| {
216            render_enum_labels(catalog, element)
217        })?),
218        Value::List(values) => Value::List(
219            values
220                .iter()
221                .map(|element| render_enum_labels(catalog, element))
222                .collect::<Result<_>>()?,
223        ),
224        Value::Row(values) => Value::Row(
225            values.clone().with_values(
226                values
227                    .iter()
228                    .map(|element| render_enum_labels(catalog, element))
229                    .collect::<Result<_>>()?,
230            )?,
231        ),
232        Value::Record(fields) => Value::Record(
233            fields
234                .iter()
235                .map(|(name, element)| Ok((name.clone(), render_enum_labels(catalog, element)?)))
236                .collect::<Result<_>>()?,
237        ),
238        Value::Map(fields) => Value::Map(
239            fields
240                .iter()
241                .map(|(name, element)| Ok((name.clone(), render_enum_labels(catalog, element)?)))
242                .collect::<Result<_>>()?,
243        ),
244        other => other.clone(),
245    })
246}
247
248/// Rebuild an array with the same bounds after converting every leaf element; nested dimensions are `List` elements.
249fn map_array(
250    array: &ArrayValue,
251    mut convert: impl FnMut(&Value) -> Result<Value>,
252) -> Result<ArrayValue> {
253    fn leaves(
254        values: &[Value],
255        convert: &mut dyn FnMut(&Value) -> Result<Value>,
256    ) -> Result<Vec<Value>> {
257        values
258            .iter()
259            .map(|value| match value {
260                Value::List(values) => leaves(values, convert).map(Value::List),
261                value => convert(value),
262            })
263            .collect()
264    }
265    let elements = leaves(array.elements(), &mut convert)?;
266    ArrayValue::with_lower_bounds(elements, array.lower_bounds().to_vec())
267        .filter(|converted| converted.dimensions() == array.dimensions())
268        .ok_or_else(|| SQLError::Internal("enum array conversion changed array dimensions".into()))
269}
270
271/// Whether a value contains an enum carrier anywhere.
272pub fn contains_enum_carrier(value: &Value) -> bool {
273    match value {
274        Value::Enum(_) => true,
275        Value::Array(array) => array.elements().iter().any(contains_enum_carrier),
276        Value::List(values) => values.iter().any(contains_enum_carrier),
277        Value::Row(values) => values.iter().any(contains_enum_carrier),
278        Value::Record(fields) => fields.iter().any(|(_, value)| contains_enum_carrier(value)),
279        Value::Map(fields) => fields.values().any(contains_enum_carrier),
280        _ => false,
281    }
282}
283
284/// 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.
285#[must_use]
286pub fn call_applies_output_functions(name: &str, arguments: &[(Option<String>, Value)]) -> bool {
287    match name {
288        "concat_op" => !arguments
289            .iter()
290            .any(|(_, value)| matches!(value, Value::Array(_) | Value::List(_))),
291        "concat" | "concat_ws" | "format" | "quote_literal" | "quote_nullable" | "to_json"
292        | "to_jsonb" | "row_to_json" | "array_to_json" | "json_build_object"
293        | "json_build_array" | "jsonb_build_object" | "jsonb_build_array" | "array_to_string" => {
294            true
295        }
296        _ => false,
297    }
298}
299
300/// Render the enum carriers of an output-function call's arguments as their labels.
301pub fn render_call_arguments(
302    catalog: Option<&dyn EnumLabelCatalog>,
303    arguments: Vec<(Option<String>, Value)>,
304) -> Result<Vec<(Option<String>, Value)>> {
305    arguments
306        .into_iter()
307        .map(|(name, value)| {
308            if contains_enum_carrier(&value) {
309                Ok((name, render_enum_labels(catalog, &value)?))
310            } else {
311                Ok((name, value))
312            }
313        })
314        .collect()
315}
316
317/// Arguments handed to host-registered functions: enum values become their labels, as procedural languages receive them through the type's output function.
318pub fn render_host_arguments(
319    catalog: Option<&dyn EnumLabelCatalog>,
320    arguments: &[Value],
321) -> Result<Vec<Value>> {
322    arguments
323        .iter()
324        .map(|value| {
325            if contains_enum_carrier(value) {
326                render_enum_labels(catalog, value)
327            } else {
328                Ok(value.clone())
329            }
330        })
331        .collect()
332}
333
334/// Whether a declared type is an enum or an array whose leaf is an enum.
335#[must_use]
336pub fn is_enum_bearing(ty: &ColumnType) -> bool {
337    match ty {
338        ColumnType::Enum(_) => true,
339        ColumnType::Array(element) => is_enum_bearing(element),
340        _ => false,
341    }
342}
343
344/// 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.
345pub fn fold_unknown_literal(
346    catalog: Option<&dyn EnumLabelCatalog>,
347    value: &Value,
348    target: &ColumnType,
349) -> Result<Option<Value>> {
350    match (target, value) {
351        (ColumnType::Enum(_) | ColumnType::Array(_), Value::Null) if is_enum_bearing(target) => {
352            Ok(Some(Value::Null))
353        }
354        (ColumnType::Enum(reference), Value::Str(text)) => {
355            enum_value_from_text(catalog, reference.oid, text).map(Some)
356        }
357        (ColumnType::Array(element), Value::Str(text)) if is_enum_bearing(element) => {
358            let mut leaf = element.as_ref();
359            while let ColumnType::Array(inner) = leaf {
360                leaf = inner;
361            }
362            let ColumnType::Enum(reference) = leaf else {
363                return Ok(None);
364            };
365            let parsed = super::casting::parse_pg_array_literal(text)?;
366            map_array(&parsed, |element| match element {
367                Value::Null => Ok(Value::Null),
368                Value::Str(text) => enum_value_from_text(catalog, reference.oid, text),
369                other => Err(SQLError::Internal(format!(
370                    "array literal parsing produced a non-text element {other:?}"
371                ))),
372            })
373            .map(|array| Some(Value::Array(array)))
374        }
375        _ => Ok(None),
376    }
377}
378
379/// The string category accepted by automatic I/O conversion casts.
380fn string_category(ty: &ColumnType) -> bool {
381    match ty {
382        ColumnType::Text
383        | ColumnType::Varchar(_)
384        | ColumnType::Bpchar
385        | ColumnType::Character(_)
386        | ColumnType::Name => true,
387        ColumnType::Domain { base, .. } => string_category(base),
388        _ => false,
389    }
390}
391
392fn cannot_cast(source: &str, target: &str) -> SQLError {
393    SQLError::Routine {
394        sqlstate: "42846".into(),
395        message: format!("cannot cast type {source} to {target}"),
396    }
397}
398
399/// 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.
400pub fn cast_to_enum(
401    catalog: Option<&dyn EnumLabelCatalog>,
402    value: &Value,
403    source: Option<&ColumnType>,
404    target: &ColumnType,
405) -> Result<Option<Value>> {
406    let ColumnType::Enum(reference) = target else {
407        return Ok(None);
408    };
409    let source_name = |source: Option<&ColumnType>| -> Result<String> {
410        Ok(match (value, source) {
411            (Value::Enum(label), _) => type_name(catalog, label.type_oid())?,
412            (_, Some(ColumnType::Enum(source))) => type_name(catalog, source.oid)?,
413            (_, Some(source)) => source.sql_name(),
414            (_, None) => "unknown".into(),
415        })
416    };
417    match value {
418        Value::Null => Ok(Some(Value::Null)),
419        Value::Enum(label) if label.type_oid() == reference.oid => Ok(Some(value.clone())),
420        Value::Str(text) | Value::FixedChar(text) if source.is_none_or(string_category) => {
421            enum_value_from_text(catalog, reference.oid, text).map(Some)
422        }
423        _ => Err(cannot_cast(
424            &source_name(source)?,
425            &type_name(catalog, reference.oid)?,
426        )),
427    }
428}
429
430/// Cast an enum value to a string-category type through its label; any other target has no cast.
431pub fn enum_output_for_cast(
432    catalog: Option<&dyn EnumLabelCatalog>,
433    label: &EnumValue,
434    target: &ColumnType,
435) -> Result<Value> {
436    if string_category(target) {
437        return enum_label_text(catalog, label).map(Value::Str);
438    }
439    Err(cannot_cast(
440        &type_name(catalog, label.type_oid())?,
441        &match target {
442            ColumnType::Enum(reference) => type_name(catalog, reference.oid)?,
443            other => other.sql_name(),
444        },
445    ))
446}