Skip to main content

uqa_sql/schema/
foreign_wrappers.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` wrapper declaration checks over exact catalog routine identities.
8
9use crate::{
10    ast::{ForeignWrapperFunctionOption, FunctionBinding},
11    catalog::roles::{memberships::role_is_superuser, RoleDefinition, RoleReference},
12    SQLError,
13};
14use std::collections::BTreeMap;
15
16#[derive(Clone)]
17pub struct ForeignWrapperRoutine {
18    pub binding: FunctionBinding,
19    pub oid: u32,
20    pub return_oid: i64,
21}
22
23#[derive(Default)]
24pub struct ForeignWrapperFunctions {
25    pub handler: Option<ForeignWrapperRoutine>,
26    pub validator: Option<ForeignWrapperRoutine>,
27}
28
29pub fn ensure_create_authority(
30    name: &str,
31    role: &RoleReference,
32    roles: &BTreeMap<String, RoleDefinition>,
33) -> Result<(), SQLError> {
34    if role_is_superuser(roles, role) {
35        return Ok(());
36    }
37    Err(SQLError::Diagnostic {
38        sqlstate: "42501".into(),
39        message: format!("permission denied to create foreign-data wrapper \"{name}\""),
40        detail: None,
41        hint: Some("Must be superuser to create a foreign-data wrapper.".into()),
42    })
43}
44
45pub fn duplicate_wrapper(name: &str) -> SQLError {
46    SQLError::Routine {
47        sqlstate: "42710".into(),
48        message: format!("foreign-data wrapper \"{name}\" already exists"),
49    }
50}
51
52pub fn ensure_drop_authority(
53    wrapper: &crate::catalog::foreign_wrapper::ForeignWrapperDefinition,
54    session: &dyn crate::catalog::roles::RoleReferenceNames,
55    catalog: &dyn crate::catalog::roles::guards::RoleCatalogGuards,
56) -> Result<(), SQLError> {
57    if crate::catalog::roles::role_inherits(
58        &catalog.role_definitions(),
59        &catalog.role_memberships(),
60        &session.current_role(),
61        &wrapper.owner,
62    ) {
63        return Ok(());
64    }
65    Err(SQLError::Routine {
66        sqlstate: "42501".into(),
67        message: format!("must be owner of foreign-data wrapper {}", wrapper.name),
68    })
69}
70
71pub fn missing_wrapper(name: &str) -> SQLError {
72    SQLError::Routine {
73        sqlstate: "42704".into(),
74        message: format!("foreign-data wrapper \"{name}\" does not exist"),
75    }
76}
77
78pub fn missing_wrapper_notice(name: &str) -> crate::SQLNotice {
79    crate::SQLNotice::notice(format!(
80        "foreign-data wrapper \"{name}\" does not exist, skipping"
81    ))
82}
83
84pub fn ensure_wrapper_usage(
85    wrapper: &crate::catalog::foreign_wrapper::ForeignWrapperDefinition,
86    role: &RoleReference,
87    catalog: &dyn crate::catalog::roles::guards::RoleCatalogGuards,
88) -> Result<(), SQLError> {
89    // Host-provided native wrappers retain their existing public registration contract.
90    if matches!(wrapper.handler, crate::catalog::foreign_wrapper::ForeignWrapperHandler::Native(native) if wrapper.identity == native.reference())
91        || crate::catalog::roles::role_inherits(
92            &catalog.role_definitions(),
93            &catalog.role_memberships(),
94            role,
95            &wrapper.owner,
96        )
97    {
98        return Ok(());
99    }
100    Err(SQLError::Routine {
101        sqlstate: "42501".into(),
102        message: format!(
103            "permission denied for foreign-data wrapper {}",
104            wrapper.name
105        ),
106    })
107}
108
109/// Keep each function lookup at its written position, including before a later duplicate clause.
110pub fn bind_functions(
111    options: &[ForeignWrapperFunctionOption],
112    mut lookup: impl FnMut(&str, &[i64], &[String]) -> Result<ForeignWrapperRoutine, SQLError>,
113) -> Result<ForeignWrapperFunctions, SQLError> {
114    let mut result = ForeignWrapperFunctions::default();
115    let (mut handler_given, mut validator_given) = (false, false);
116    for option in options {
117        let (given, name, handler) = match option {
118            ForeignWrapperFunctionOption::Handler(name) => (&mut handler_given, name, true),
119            ForeignWrapperFunctionOption::Validator(name) => (&mut validator_given, name, false),
120        };
121        if *given {
122            return Err(SQLError::Routine {
123                sqlstate: "42601".into(),
124                message: "conflicting or redundant options".into(),
125            });
126        }
127        *given = true;
128        let Some(parts) = name else {
129            continue;
130        };
131        let name = function_name(parts)?;
132        if handler {
133            let function = lookup(&name, &[], &[])?;
134            if function.return_oid != 3115 {
135                return Err(SQLError::Routine {
136                    sqlstate: "42809".into(),
137                    message: format!("function {} must return type fdw_handler", parts.join(".")),
138                });
139            }
140            result.handler = Some(function);
141        } else {
142            result.validator = Some(lookup(
143                &name,
144                &[1009, 26],
145                &["text[]".into(), "oid".into()],
146            )?);
147        }
148    }
149    Ok(result)
150}
151
152fn function_name(parts: &[String]) -> Result<String, SQLError> {
153    let local = match parts {
154        [_] | [_, _] => parts,
155        [database, _, _] if database == crate::catalog::DATABASE_NAME => &parts[1..],
156        [_, _, _] => {
157            return Err(SQLError::Routine {
158                sqlstate: "0A000".into(),
159                message: format!(
160                    "cross-database references are not implemented: {}",
161                    parts.join(".")
162                ),
163            })
164        }
165        _ => {
166            return Err(SQLError::Routine {
167                sqlstate: "42601".into(),
168                message: format!(
169                    "improper qualified name (too many dotted names): {}",
170                    parts.join(".")
171                ),
172            })
173        }
174    };
175    Ok(local
176        .iter()
177        .map(|part| crate::compiler::render_relation_component(part))
178        .collect::<Vec<_>>()
179        .join("."))
180}
181
182pub fn creation_options(options: &[(String, String)]) -> Result<Vec<(String, String)>, SQLError> {
183    super::foreign_servers::creation_options(options)?;
184    Ok(options.to_vec())
185}
186
187#[cfg(test)]
188mod tests;