Skip to main content

uqa_sql/catalog/collations/
expression.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Collation-clause identity and type validation shared by expression analysis and catalog encoding.
8
9use crate::ast::{Expr, FunctionDispatch};
10use crate::{ColumnType, FunctionTypeResolver, SQLError, ScalarExpr};
11use uqa_core::{memory::ProductionControl, Value};
12
13pub struct Clause<'a, T> {
14    pub argument: &'a T,
15    pub name: &'a str,
16    pub oid: Option<u32>,
17}
18
19impl<T> Clause<'_, T> {
20    /// Executable SQL retains a selected identity instead of resolving its former spelling again.
21    pub fn executable_name(&self) -> Result<std::borrow::Cow<'_, str>, SQLError> {
22        let Some(oid) = self.oid else {
23            return Ok(std::borrow::Cow::Borrowed(self.name));
24        };
25        let name = super::builtin_collation_name(i64::from(oid))
26            .ok_or_else(|| SQLError::Internal("unknown collation identity".into()))?;
27        Ok(std::borrow::Cow::Owned(format!(
28            "pg_catalog.{}",
29            crate::expr::quote_ident(name)
30        )))
31    }
32}
33
34/// Retained constants may carry an indeterminate collatable result, but cannot attach a collation to a noncollatable or unresolved type.
35pub(crate) fn validate_literal_collation(
36    ty: Option<&ColumnType>,
37    oid: Option<u32>,
38) -> Result<(), SQLError> {
39    if oid.is_some_and(|oid| oid != 0)
40        && ty.is_none_or(|ty| !crate::schema::composites::type_is_collatable(ty))
41    {
42        return Err(SQLError::Internal(
43            "constant collation does not match its declared type".into(),
44        ));
45    }
46    Ok(())
47}
48
49pub fn scalar_clause(expression: &ScalarExpr) -> Result<Option<Clause<'_, ScalarExpr>>, SQLError> {
50    let ScalarExpr::Func {
51        binding: Some(binding),
52        args,
53        ..
54    } = expression
55    else {
56        return Ok(None);
57    };
58    let Some(FunctionDispatch::Collate { oid }) = binding.dispatch else {
59        return Ok(None);
60    };
61    let [argument, ScalarExpr::Literal(Value::Str(name))] = args.as_slice() else {
62        return Err(SQLError::Internal("invalid COLLATE expression".into()));
63    };
64    Ok(Some(Clause {
65        argument,
66        name,
67        oid,
68    }))
69}
70
71pub fn ast_clause(expression: &Expr) -> Result<Option<Clause<'_, Expr>>, SQLError> {
72    let Expr::Func {
73        binding: Some(binding),
74        args,
75        ..
76    } = expression
77    else {
78        return Ok(None);
79    };
80    let Some(FunctionDispatch::Collate { oid }) = binding.dispatch else {
81        return Ok(None);
82    };
83    let [argument, Expr::Literal(Value::Str(name))] = args.as_slice() else {
84        return Err(SQLError::Internal("invalid COLLATE expression".into()));
85    };
86    Ok(Some(Clause {
87        argument,
88        name,
89        oid,
90    }))
91}
92
93pub(crate) fn discard_scalar_clause(expression: &mut ScalarExpr) -> Result<bool, SQLError> {
94    if scalar_clause(expression)?.is_none() {
95        return Ok(false);
96    }
97    let ScalarExpr::Func { args, .. } =
98        std::mem::replace(expression, ScalarExpr::Literal(Value::Null))
99    else {
100        unreachable!("validated collation clause");
101    };
102    *expression = args
103        .into_iter()
104        .next()
105        .expect("validated collation argument");
106    Ok(true)
107}
108
109pub(crate) fn discard_ast_clause(expression: &mut Expr) -> Result<bool, SQLError> {
110    if ast_clause(expression)?.is_none() {
111        return Ok(false);
112    }
113    let Expr::Func { args, .. } = std::mem::replace(expression, Expr::Literal(Value::Null)) else {
114        unreachable!("validated collation clause");
115    };
116    *expression = args
117        .into_iter()
118        .next()
119        .expect("validated collation argument");
120    Ok(true)
121}
122
123pub fn select(
124    name: &str,
125    selected: Option<u32>,
126    ty: Option<&ColumnType>,
127    catalog: Option<&dyn FunctionTypeResolver>,
128) -> Result<u32, SQLError> {
129    select_with_control(
130        name,
131        selected,
132        ty,
133        catalog,
134        &ProductionControl::uncontrolled(),
135    )
136}
137
138pub(crate) fn select_with_control(
139    name: &str,
140    selected: Option<u32>,
141    ty: Option<&ColumnType>,
142    catalog: Option<&dyn FunctionTypeResolver>,
143    control: &ProductionControl<'_>,
144) -> Result<u32, SQLError> {
145    control.check()?;
146    assert!(
147        catalog.is_none() || control.budget().is_none(),
148        "catalog callbacks are resolved outside retained generated inference"
149    );
150    if let Some(ty) = ty.filter(|ty| !crate::schema::composites::type_is_collatable(ty)) {
151        let ty = ty.without_type_modifiers_with_control(control)?;
152        let name = match catalog {
153            Some(catalog) => control.finish(catalog.format_type_name(&ty)?, None)?,
154            None => ty.display_name_with_control(control)?,
155        };
156        return Err(diagnostic(
157            control,
158            "42804",
159            format_args!("collations are not supported by type {}", name.as_str()),
160        )?);
161    }
162    if let Some(oid) = selected {
163        return valid_oid(oid);
164    }
165    if let Some(oid) = catalog
166        .map(|catalog| catalog.resolve_collation_name(name))
167        .transpose()?
168        .flatten()
169    {
170        return valid_oid(oid);
171    }
172    builtin_name(name, control)
173}
174
175fn valid_oid(oid: u32) -> Result<u32, SQLError> {
176    if oid == 0 {
177        Err(SQLError::Internal("invalid collation identity".into()))
178    } else {
179        Ok(oid)
180    }
181}
182
183/// Catalog-free analysis has the predefined namespace and collation identities.
184fn builtin_name(name: &str, control: &ProductionControl<'_>) -> Result<u32, SQLError> {
185    let names = crate::compiler::parse_regobject_name_with_control(name, control)?
186        .ok_or_else(|| SQLError::Internal("malformed COLLATE name".into()))?;
187    let (schema, local) = match names.as_slice() {
188        [local] => (None, local),
189        [schema, local] => (Some(schema.as_str()), local),
190        [database, schema, local] if database == "uqa" => (Some(schema.as_str()), local),
191        [_, _, _] => {
192            return Err(diagnostic(
193                control,
194                "0A000",
195                format_args!(
196                    "cross-database references are not implemented: {}",
197                    QualifiedName(&names)
198                ),
199            )?)
200        }
201        _ => {
202            return Err(diagnostic(
203                control,
204                "42601",
205                format_args!(
206                    "improper qualified name (too many dotted names): {}",
207                    QualifiedName(&names)
208                ),
209            )?)
210        }
211    };
212    if let Some(schema) = schema.filter(|schema| !matches!(*schema, "pg_catalog" | "public")) {
213        return Err(diagnostic(
214            control,
215            "3F000",
216            format_args!("schema \"{schema}\" does not exist"),
217        )?);
218    }
219    if schema.is_none_or(|schema| schema == "pg_catalog") {
220        if let Some(oid) = super::builtin_collation_oid(local) {
221            return u32::try_from(oid)
222                .map_err(|_| SQLError::Internal("invalid collation identity".into()));
223        }
224    }
225    Err(diagnostic(
226        control,
227        "42704",
228        format_args!(
229            "collation \"{}\" for encoding \"UTF8\" does not exist",
230            QualifiedName(&names)
231        ),
232    )?)
233}
234
235struct QualifiedName<'a>(&'a [String]);
236
237impl std::fmt::Display for QualifiedName<'_> {
238    fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        for (position, component) in self.0.iter().enumerate() {
240            if position > 0 {
241                output.write_str(".")?;
242            }
243            output.write_str(component)?;
244        }
245        Ok(())
246    }
247}
248
249fn diagnostic(
250    control: &ProductionControl<'_>,
251    sqlstate: &str,
252    message: std::fmt::Arguments<'_>,
253) -> Result<SQLError, SQLError> {
254    // Error payloads leave this producer through the existing unreserved SQLError boundary after admitted construction.
255    let (message, _memory) = control.format(message)?.into_parts();
256    Ok(SQLError::Routine {
257        sqlstate: sqlstate.into(),
258        message,
259    })
260}
261
262#[cfg(test)]
263mod tests;