Skip to main content

uqa_sql/schema/dependencies/
oid_alias.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Read OID alias constants at analysis as the `reg*` input functions read them: a `regclass`, `regtype`, `regproc`, `regprocedure` or `regnamespace` cast of a string literal, or a cast to an array of one of them, stores the OIDs the names resolve to, as parse analysis stores the constant, so a stored expression follows the objects through renames and a name no object has is reported where the expression is analyzed.
8
9use super::walk_schema_expr_mut;
10use crate::ast::{ColumnType, Expr};
11use crate::expr::EngineHook;
12use crate::plan::{QueryPlan, UnifiedPlan};
13use crate::{SQLError, ScalarExpr};
14use uqa_core::{ArrayValue, Value};
15
16/// A catalog that reads OID alias input: the object a name denotes for one alias type, resolved as the type's input function resolves it through the search path, with the input function's errors. `None` says the catalog has no such object; the reader then reports the input function's missing-object error.
17pub trait OidAliasInput {
18    fn resolve_oid_alias_input(&self, ty: &ColumnType, name: &str)
19        -> Result<Option<i64>, SQLError>;
20}
21
22impl<T: EngineHook + ?Sized> OidAliasInput for T {
23    fn resolve_oid_alias_input(
24        &self,
25        ty: &ColumnType,
26        name: &str,
27    ) -> Result<Option<i64>, SQLError> {
28        match ty {
29            ColumnType::Regclass => EngineHook::resolve_regclass_input(self, name),
30            ColumnType::Regtype => EngineHook::resolve_regtype_input(self, name),
31            ColumnType::Regproc => EngineHook::resolve_regproc(self, name),
32            ColumnType::Regprocedure => EngineHook::resolve_regprocedure_input(self, name),
33            ColumnType::Regnamespace => EngineHook::resolve_regnamespace(self, name),
34            ColumnType::Regcollation => EngineHook::resolve_regcollation(self, name),
35            ColumnType::Regrole => EngineHook::resolve_regrole(self, name),
36            other => Err(SQLError::Internal(format!(
37                "{} is not an OID alias type read at analysis",
38                other.sql_name()
39            ))),
40        }
41    }
42}
43
44/// Whether `ty` is an OID alias type whose input function reads a catalog name at analysis.
45fn is_alias(ty: &ColumnType) -> bool {
46    matches!(
47        ty,
48        ColumnType::Regclass
49            | ColumnType::Regtype
50            | ColumnType::Regproc
51            | ColumnType::Regprocedure
52            | ColumnType::Regcollation
53            | ColumnType::Regnamespace
54    )
55}
56
57/// The OID alias type a cast's type name denotes, and whether the cast is to an array of it.
58fn alias_type(ty: &str) -> Option<(ColumnType, bool)> {
59    if !ty
60        .as_bytes()
61        .windows(3)
62        .any(|window| window.eq_ignore_ascii_case(b"reg"))
63    {
64        return None;
65    }
66    match ColumnType::from_sql_name(ty).ok()? {
67        ColumnType::Array(element) if is_alias(&element) => Some((*element, true)),
68        element if is_alias(&element) => Some((element, false)),
69        _ => None,
70    }
71}
72
73/// The error the alias type's input function reports for a name no object has.
74fn missing_object(ty: &ColumnType, name: &str) -> SQLError {
75    if matches!(ty, ColumnType::Regcollation) {
76        return SQLError::Routine {
77            sqlstate: "42704".into(),
78            message: format!("collation \"{name}\" for encoding \"UTF8\" does not exist"),
79        };
80    }
81    let (sqlstate, object) = match ty {
82        ColumnType::Regclass => ("42P01", "relation"),
83        ColumnType::Regtype => ("42704", "type"),
84        ColumnType::Regrole => ("42704", "role"),
85        ColumnType::Regproc | ColumnType::Regprocedure => ("42883", "function"),
86        _ => ("3F000", "schema"),
87    };
88    SQLError::Routine {
89        sqlstate: sqlstate.into(),
90        message: format!("{object} \"{name}\" does not exist"),
91    }
92}
93
94/// The OID the alias type's input function gives `name`.
95fn read_name<C: OidAliasInput + ?Sized>(
96    catalog: &C,
97    ty: &ColumnType,
98    name: &str,
99) -> Result<i64, SQLError> {
100    catalog
101        .resolve_oid_alias_input(ty, name)?
102        .ok_or_else(|| missing_object(ty, name))
103}
104
105/// The constant parse analysis stores for a cast of the literal `text` to the alias type `ty`, or to an array of it: the object's OID, or the OIDs of the array's elements.
106fn read_constant<C: OidAliasInput + ?Sized>(
107    catalog: &C,
108    ty: &ColumnType,
109    text: &str,
110    array: bool,
111) -> Result<Value, SQLError> {
112    if !array {
113        return read_name(catalog, ty, text).map(Value::Int);
114    }
115    let array = crate::expr::parse_pg_array_literal(text)?;
116    let lower_bounds = array.lower_bounds().to_vec();
117    let mut elements = array.into_elements();
118    read_array_elements(catalog, ty, &mut elements)?;
119    ArrayValue::with_lower_bounds(elements, lower_bounds)
120        .map(Value::Array)
121        .ok_or_else(|| {
122            SQLError::Internal(format!("{} array literal lost its shape", ty.sql_name()))
123        })
124}
125
126fn read_array_elements<C: OidAliasInput + ?Sized>(
127    catalog: &C,
128    ty: &ColumnType,
129    elements: &mut [Value],
130) -> Result<(), SQLError> {
131    for element in elements {
132        match element {
133            Value::Null => {}
134            Value::Str(name) => *element = Value::Int(read_name(catalog, ty, name)?),
135            Value::List(nested) => read_array_elements(catalog, ty, nested)?,
136            other => {
137                return Err(SQLError::TypeMismatch(format!(
138                    "cannot read {other:?} as {}",
139                    ty.sql_name(),
140                )))
141            }
142        }
143    }
144    Ok(())
145}
146
147/// Read an unknown input after ordered analysis selects an OID alias type. Other catalog-dependent input types remain the responsibility of their own binders.
148pub(crate) fn read_unknown_constant(
149    catalog: &dyn OidAliasInput,
150    ty: &ColumnType,
151    text: &str,
152) -> Result<Option<Value>, SQLError> {
153    match ty {
154        ColumnType::Array(element)
155            if is_alias(element) || matches!(element.as_ref(), ColumnType::Regrole) =>
156        {
157            read_constant(catalog, element, text, true).map(Some)
158        }
159        scalar if is_alias(scalar) || matches!(scalar, ColumnType::Regrole) => {
160            read_constant(catalog, scalar, text, false).map(Some)
161        }
162        _ => Ok(None),
163    }
164}
165
166/// The type of the stored constant: the alias type, or an array of it.
167fn constant_type(ty: ColumnType, array: bool) -> ColumnType {
168    if array {
169        ColumnType::Array(Box::new(ty))
170    } else {
171        ty
172    }
173}
174
175/// Read the OID alias constants of a stored schema expression: each `reg*` cast of a string literal becomes the typed constant of the OIDs the names resolve to, so the expression follows the objects through renames, and a name no object has reports the input function's error.
176pub fn read_oid_alias_constants<C: OidAliasInput + ?Sized>(
177    catalog: &C,
178    expression: &mut Expr,
179) -> Result<(), SQLError> {
180    let mut failure = None;
181    let outcome = walk_schema_expr_mut(expression, &mut |node| {
182        let Expr::Cast { expr, ty, .. } = node else {
183            return Ok(());
184        };
185        let Some((alias, array)) = alias_type(ty) else {
186            return Ok(());
187        };
188        let Expr::Literal(Value::Str(text)) = expr.as_ref() else {
189            return Ok(());
190        };
191        match read_constant(catalog, &alias, text, array) {
192            Ok(value) => {
193                **expr = Expr::TypedLiteral {
194                    collation_oid: None,
195                    composite_source: None,
196                    value,
197                    ty: constant_type(alias, array).catalog_name(),
198                };
199                Ok(())
200            }
201            Err(error) => {
202                failure = Some(error);
203                Err(String::new())
204            }
205        }
206    });
207    match (outcome, failure) {
208        (Ok(()), _) => Ok(()),
209        (Err(_), Some(error)) => Err(error),
210        (Err(message), None) => Err(SQLError::Internal(message)),
211    }
212}
213
214/// The first argument of `nextval`, `currval` or `setval` when it is an `unknown` literal: `coerce_type` reads it with `regclassin` for the `regclass` parameter, so the stored constant is the sequence's OID and prints as `'name'::regclass`.
215fn sequence_argument_mut(expression: &mut ScalarExpr) -> Option<&mut ScalarExpr> {
216    let ScalarExpr::Func { name, args, .. } = expression else {
217        return None;
218    };
219    if !is_sequence_function(name) {
220        return None;
221    }
222    args.first_mut()
223        .filter(|argument| matches!(argument, ScalarExpr::Literal(Value::Str(_))))
224}
225
226fn sequence_argument(expression: &ScalarExpr) -> Option<&str> {
227    let ScalarExpr::Func { name, args, .. } = expression else {
228        return None;
229    };
230    if !is_sequence_function(name) {
231        return None;
232    }
233    match args.first() {
234        Some(ScalarExpr::Literal(Value::Str(text))) => Some(text),
235        _ => None,
236    }
237}
238
239fn is_sequence_function(name: &str) -> bool {
240    let lower = name.to_ascii_lowercase();
241    let local = lower.strip_prefix("pg_catalog.").unwrap_or(&lower);
242    matches!(local, "nextval" | "currval" | "setval")
243        && (!lower.contains('.') || lower.starts_with("pg_catalog."))
244}
245
246/// [`read_oid_alias_constants`] for one scalar node. The first failure is kept and stops further reads. With `keep_relations`, a `regclass` cast is checked and left as written.
247fn read_scalar_constant<C: OidAliasInput + ?Sized>(
248    catalog: &C,
249    expression: &mut ScalarExpr,
250    keep_relations: bool,
251    failure: &mut Option<SQLError>,
252) {
253    if failure.is_some() {
254        return;
255    }
256    if let Some(argument) = sequence_argument_mut(expression) {
257        let ScalarExpr::Literal(Value::Str(text)) = &*argument else {
258            unreachable!("sequence argument is an unknown literal");
259        };
260        match read_constant(catalog, &ColumnType::Regclass, text, false) {
261            Ok(value) => {
262                if !keep_relations {
263                    *argument = ScalarExpr::TypedLiteral {
264                        collation_oid: None,
265                        composite_source: None,
266                        value,
267                        ty: ColumnType::Regclass.catalog_name(),
268                        bound_type: Some(ColumnType::Regclass),
269                        parameter_index: None,
270                    };
271                }
272            }
273            Err(error) => *failure = Some(error),
274        }
275        return;
276    }
277    let ScalarExpr::Cast { expr, ty, .. } = expression else {
278        return;
279    };
280    let Some((alias, array)) = alias_type(ty) else {
281        return;
282    };
283    let ScalarExpr::Literal(Value::Str(text)) = expr.as_ref() else {
284        return;
285    };
286    match read_constant(catalog, &alias, text, array) {
287        Ok(value) => {
288            if keep_relations && matches!(alias, ColumnType::Regclass) {
289                return;
290            }
291            let bound_type = constant_type(alias, array);
292            **expr = ScalarExpr::TypedLiteral {
293                collation_oid: None,
294                composite_source: None,
295                value,
296                ty: bound_type.catalog_name(),
297                bound_type: Some(bound_type),
298                parameter_index: None,
299            };
300        }
301        Err(error) => *failure = Some(error),
302    }
303}
304
305/// [`read_oid_alias_constants`] over every scalar expression of a stored query, nested ones included: a view stores the OIDs its `reg*` constants name.
306pub fn read_oid_alias_constants_in_plan<C: OidAliasInput + ?Sized>(
307    catalog: &C,
308    plan: &mut QueryPlan,
309) -> Result<(), SQLError> {
310    let mut failure = None;
311    plan.rewrite_scalar_expressions(&mut |root| {
312        root.visit_mut(&mut |expression| {
313            read_scalar_constant(catalog, expression, false, &mut failure);
314        });
315    });
316    failure.map_or(Ok(()), Err)
317}
318
319/// Read a prepared statement's OID alias inputs once. Scalar regclass constants contribute relation dependencies separately; invalidation reads the retained original syntax again. A whole array constant retains its OIDs without becoming scalar relation dependencies.
320pub fn read_prepared_oid_alias_constants<C: OidAliasInput + ?Sized>(
321    catalog: &C,
322    plan: &mut UnifiedPlan,
323) -> Result<(), SQLError> {
324    let mut failure = None;
325    plan.rewrite_scalar_expressions(&mut |root| {
326        root.visit_mut(&mut |expression| {
327            read_scalar_constant(catalog, expression, false, &mut failure);
328        });
329    });
330    failure.map_or(Ok(()), Err)
331}
332
333/// The alias input functions at analysis for a statement that runs at once: every `reg*` cast of a string literal, nested ones included, must name an object before the statement runs, as parse analysis reads the constant before planning and execution. The plan keeps the written names, which the cast resolves again when it is evaluated, since a statement's text is analyzed anew each time it runs.
334pub fn check_statement_oid_alias_constants<C: OidAliasInput + ?Sized>(
335    catalog: &C,
336    plan: &UnifiedPlan,
337) -> Result<(), SQLError> {
338    let mut failure = None;
339    plan.visit_scalar_expressions(&mut |root| {
340        root.visit(&mut |expression| {
341            if failure.is_some() {
342                return;
343            }
344            if let Some(text) = sequence_argument(expression) {
345                if let Err(error) = read_constant(catalog, &ColumnType::Regclass, text, false) {
346                    failure = Some(error);
347                }
348                return;
349            }
350            let ScalarExpr::Cast { expr, ty, .. } = expression else {
351                return;
352            };
353            let Some((alias, array)) = alias_type(ty) else {
354                return;
355            };
356            let ScalarExpr::Literal(Value::Str(text)) = expr.as_ref() else {
357                return;
358            };
359            if let Err(error) = read_constant(catalog, &alias, text, array) {
360                failure = Some(error);
361            }
362        });
363    });
364    failure.map_or(Ok(()), Err)
365}