Skip to main content

uqa_sql/routines/lifecycle/
names.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Search-path and namespace authorization rules for routine names.
8
9use crate::catalog::roles::RoleReference;
10use crate::{catalog::security::BoundSchemaSecurity, SQLError};
11use uqa_core::RelationIdentity;
12
13/// Immutable routine address and declared input types, independent of invocation coercions.
14pub struct RoutineCatalogIdentity {
15    pub oid: u32,
16    pub relation: RelationIdentity,
17    pub argument_types: Vec<i64>,
18    pub kind: char,
19}
20
21pub trait RoutineNameCatalog {
22    fn schema_security(&self, schema: &str) -> Option<BoundSchemaSecurity>;
23    fn current_role(&self) -> RoleReference;
24    fn search_path(&self) -> Vec<String>;
25    fn require_schema_usage(&self, schema: &str, role: &RoleReference) -> Result<(), SQLError>;
26    fn schema_has_usage(&self, schema: &str, role: &RoleReference) -> bool;
27    /// `format_type_be` of a routine argument type: a user-defined type, recorded by identity, is spelled by its current name.
28    fn routine_type_display(&self, type_name: &str) -> String;
29    /// `format_procedure` of an existing routine, including search-path qualification and its current input type names. Call before retaining the routine registry's write guard.
30    fn routine_identity_display(&self, oid: u32) -> Result<String, SQLError>;
31}
32
33pub fn routine_lookup_keys(
34    catalog: &dyn RoutineNameCatalog,
35    name: &str,
36) -> Result<Vec<String>, SQLError> {
37    let (schema, local_name) =
38        RelationIdentity::parse_reference(name).map_err(|error| SQLError::Routine {
39            sqlstate: "42602".into(),
40            message: format!("invalid routine name `{name}`: {error}"),
41        })?;
42    if let Some(schema) = schema {
43        if catalog.schema_security(&schema).is_none() {
44            return Err(SQLError::Routine {
45                sqlstate: "3F000".into(),
46                message: format!("schema \"{schema}\" does not exist"),
47            });
48        }
49        catalog.require_schema_usage(&schema, &catalog.current_role())?;
50        return Ok(vec![
51            RelationIdentity::new(schema, local_name).qualified_name()
52        ]);
53    }
54    let current_user = catalog.current_role();
55    let search_path = catalog.search_path();
56    Ok(search_path
57        .into_iter()
58        .filter(|schema| {
59            catalog.schema_security(schema).is_some()
60                && catalog.schema_has_usage(schema, &current_user)
61        })
62        .map(|schema| RelationIdentity::new(schema, &local_name).qualified_name())
63        .collect())
64}
65
66/// Catalog command lookup includes implicit `pg_catalog` in `PostgreSQL`'s search-path position.
67pub fn routine_lookup_keys_with_builtins(
68    catalog: &dyn RoutineNameCatalog,
69    name: &str,
70) -> Result<Vec<String>, SQLError> {
71    let mut keys = routine_lookup_keys(catalog, name)?;
72    let (schema, local) = RelationIdentity::parse_reference(name).map_err(SQLError::Internal)?;
73    if schema.is_none()
74        && catalog.schema_has_usage("pg_catalog", &catalog.current_role())
75        && !catalog
76            .search_path()
77            .iter()
78            .any(|schema| schema == "pg_catalog")
79    {
80        keys.insert(
81            0,
82            RelationIdentity::new("pg_catalog", local).qualified_name(),
83        );
84    }
85    Ok(keys)
86}
87
88/// `LookupFuncName` matches declared input OIDs exactly, ignores procedures, and does not expand defaults or variadics.
89pub fn exact_function(
90    catalog: &dyn RoutineNameCatalog,
91    identities: &[RoutineCatalogIdentity],
92    name: &str,
93    argument_types: &[i64],
94    display_types: &[String],
95) -> Result<usize, SQLError> {
96    for key in routine_lookup_keys_with_builtins(catalog, name)? {
97        if let Some(index) = identities.iter().position(|identity| {
98            identity.relation.qualified_name() == key
99                && identity.kind != 'p'
100                && identity.argument_types == argument_types
101        }) {
102            return Ok(index);
103        }
104    }
105    Err(SQLError::Routine {
106        sqlstate: "42883".into(),
107        message: format!(
108            "function {} does not exist",
109            super::routine_signature_display(catalog, name, display_types)
110        ),
111    })
112}
113
114/// Defer namespace errors during recursive argument analysis; definitive binding checks again.
115pub fn routine_lookup_keys_for_analysis(
116    catalog: &dyn RoutineNameCatalog,
117    name: &str,
118) -> Result<Option<Vec<String>>, SQLError> {
119    match routine_lookup_keys(catalog, name) {
120        Err(error) if crate::routines::is_routine_namespace_lookup_error(&error) => Ok(None),
121        result => result.map(Some),
122    }
123}
124
125#[cfg(test)]
126mod tests;