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                    composite_source: None,
195                    value,
196                    ty: constant_type(alias, array).catalog_name(),
197                };
198                Ok(())
199            }
200            Err(error) => {
201                failure = Some(error);
202                Err(String::new())
203            }
204        }
205    });
206    match (outcome, failure) {
207        (Ok(()), _) => Ok(()),
208        (Err(_), Some(error)) => Err(error),
209        (Err(message), None) => Err(SQLError::Internal(message)),
210    }
211}
212
213/// 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`.
214fn sequence_argument_mut(expression: &mut ScalarExpr) -> Option<&mut ScalarExpr> {
215    let ScalarExpr::Func { name, args, .. } = expression else {
216        return None;
217    };
218    if !is_sequence_function(name) {
219        return None;
220    }
221    args.first_mut()
222        .filter(|argument| matches!(argument, ScalarExpr::Literal(Value::Str(_))))
223}
224
225fn sequence_argument(expression: &ScalarExpr) -> Option<&str> {
226    let ScalarExpr::Func { name, args, .. } = expression else {
227        return None;
228    };
229    if !is_sequence_function(name) {
230        return None;
231    }
232    match args.first() {
233        Some(ScalarExpr::Literal(Value::Str(text))) => Some(text),
234        _ => None,
235    }
236}
237
238fn is_sequence_function(name: &str) -> bool {
239    let lower = name.to_ascii_lowercase();
240    let local = lower.strip_prefix("pg_catalog.").unwrap_or(&lower);
241    matches!(local, "nextval" | "currval" | "setval")
242        && (!lower.contains('.') || lower.starts_with("pg_catalog."))
243}
244
245/// [`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.
246fn read_scalar_constant<C: OidAliasInput + ?Sized>(
247    catalog: &C,
248    expression: &mut ScalarExpr,
249    keep_relations: bool,
250    failure: &mut Option<SQLError>,
251) {
252    if failure.is_some() {
253        return;
254    }
255    if let Some(argument) = sequence_argument_mut(expression) {
256        let ScalarExpr::Literal(Value::Str(text)) = &*argument else {
257            unreachable!("sequence argument is an unknown literal");
258        };
259        match read_constant(catalog, &ColumnType::Regclass, text, false) {
260            Ok(value) => {
261                if !keep_relations {
262                    *argument = ScalarExpr::TypedLiteral {
263                        composite_source: None,
264                        value,
265                        ty: ColumnType::Regclass.catalog_name(),
266                        bound_type: Some(ColumnType::Regclass),
267                        parameter_index: None,
268                    };
269                }
270            }
271            Err(error) => *failure = Some(error),
272        }
273        return;
274    }
275    let ScalarExpr::Cast { expr, ty, .. } = expression else {
276        return;
277    };
278    let Some((alias, array)) = alias_type(ty) else {
279        return;
280    };
281    let ScalarExpr::Literal(Value::Str(text)) = expr.as_ref() else {
282        return;
283    };
284    match read_constant(catalog, &alias, text, array) {
285        Ok(value) => {
286            if keep_relations && matches!(alias, ColumnType::Regclass) {
287                return;
288            }
289            let bound_type = constant_type(alias, array);
290            **expr = ScalarExpr::TypedLiteral {
291                composite_source: None,
292                value,
293                ty: bound_type.catalog_name(),
294                bound_type: Some(bound_type),
295                parameter_index: None,
296            };
297        }
298        Err(error) => *failure = Some(error),
299    }
300}
301
302/// [`read_oid_alias_constants`] over every scalar expression of a stored query, nested ones included: a view stores the OIDs its `reg*` constants name.
303pub fn read_oid_alias_constants_in_plan<C: OidAliasInput + ?Sized>(
304    catalog: &C,
305    plan: &mut QueryPlan,
306) -> Result<(), SQLError> {
307    let mut failure = None;
308    plan.rewrite_scalar_expressions(&mut |root| {
309        root.visit_mut(&mut |expression| {
310            read_scalar_constant(catalog, expression, false, &mut failure);
311        });
312    });
313    failure.map_or(Ok(()), Err)
314}
315
316/// 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.
317pub fn read_prepared_oid_alias_constants<C: OidAliasInput + ?Sized>(
318    catalog: &C,
319    plan: &mut UnifiedPlan,
320) -> Result<(), SQLError> {
321    let mut failure = None;
322    plan.rewrite_scalar_expressions(&mut |root| {
323        root.visit_mut(&mut |expression| {
324            read_scalar_constant(catalog, expression, false, &mut failure);
325        });
326    });
327    failure.map_or(Ok(()), Err)
328}
329
330/// 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.
331pub fn check_statement_oid_alias_constants<C: OidAliasInput + ?Sized>(
332    catalog: &C,
333    plan: &UnifiedPlan,
334) -> Result<(), SQLError> {
335    let mut failure = None;
336    plan.visit_scalar_expressions(&mut |root| {
337        root.visit(&mut |expression| {
338            if failure.is_some() {
339                return;
340            }
341            if let Some(text) = sequence_argument(expression) {
342                if let Err(error) = read_constant(catalog, &ColumnType::Regclass, text, false) {
343                    failure = Some(error);
344                }
345                return;
346            }
347            let ScalarExpr::Cast { expr, ty, .. } = expression else {
348                return;
349            };
350            let Some((alias, array)) = alias_type(ty) else {
351                return;
352            };
353            let ScalarExpr::Literal(Value::Str(text)) = expr.as_ref() else {
354                return;
355            };
356            if let Err(error) = read_constant(catalog, &alias, text, array) {
357                failure = Some(error);
358            }
359        });
360    });
361    failure.map_or(Ok(()), Err)
362}