Skip to main content

uqa_sql/catalog/
foreign_wrapper.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Foreign-wrapper catalog identity, separate from server names and source adapters.
8
9use super::roles::{identity::RoleSubject, RoleDefinition, RoleIdentity};
10use crate::{ast::FunctionBinding, SQLError};
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13
14pub type ForeignWrappers = BTreeMap<String, ForeignWrapperDefinition>;
15
16/// Implementations already provided by the host. A SQL name alone never selects one.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum NativeForeignWrapper {
19    Memory,
20    DuckDB,
21    Arrow,
22}
23
24impl NativeForeignWrapper {
25    pub const ALL: [Self; 3] = [Self::Memory, Self::DuckDB, Self::Arrow];
26
27    pub const fn handler_name(self) -> &'static str {
28        match self {
29            Self::Memory => "memory_fdw_handler",
30            Self::DuckDB => "duckdb_fdw_handler",
31            Self::Arrow => "arrow_fdw_handler",
32        }
33    }
34
35    pub fn handler_oid(self) -> i64 {
36        super::oids::stable_oid("native-fdw-handler", self.handler_name())
37    }
38
39    pub fn for_handler(oid: u32) -> Option<Self> {
40        Self::ALL
41            .into_iter()
42            .find(|native| native.handler_oid() == i64::from(oid))
43    }
44
45    pub const fn name(self) -> &'static str {
46        match self {
47            Self::Memory => "memory_fdw",
48            Self::DuckDB => "duckdb_fdw",
49            Self::Arrow => "arrow_fdw",
50        }
51    }
52
53    /// Native wrappers occupy reserved addresses in their own catalog class, below user OIDs.
54    pub const fn reference(self) -> ForeignWrapperReference {
55        let (oid, tag) = match self {
56            Self::Memory => (1, 1),
57            Self::DuckDB => (2, 2),
58            Self::Arrow => (3, 3),
59        };
60        let mut object_id = *b"UQA native FDW\0\0";
61        object_id[15] = tag;
62        ForeignWrapperReference { oid, object_id }
63    }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub enum ForeignWrapperHandler {
68    None,
69    Native(NativeForeignWrapper),
70    Function(ForeignWrapperFunction),
71}
72
73/// Keep `PostgreSQL`'s routine OID even when a validator removes itself during creation.
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct ForeignWrapperFunction {
77    pub oid: u32,
78    pub binding: FunctionBinding,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct ForeignWrapperReference {
84    pub oid: u32,
85    pub object_id: [u8; 16],
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(deny_unknown_fields)]
90pub struct ForeignWrapperDefinition {
91    pub name: String,
92    pub identity: ForeignWrapperReference,
93    pub owner: RoleIdentity,
94    pub handler: ForeignWrapperHandler,
95    pub validator: Option<ForeignWrapperFunction>,
96    /// Catalog option arrays retain declaration order, even when execution later needs a map.
97    pub options: Vec<(String, String)>,
98}
99
100impl ForeignWrapperDefinition {
101    pub fn require_handler(&self) -> Result<(), SQLError> {
102        if matches!(self.handler, ForeignWrapperHandler::None) {
103            return Err(SQLError::Routine {
104                sqlstate: "55000".into(),
105                message: format!("foreign-data wrapper \"{}\" has no handler", self.name),
106            });
107        }
108        Ok(())
109    }
110}
111
112pub fn native_wrappers() -> ForeignWrappers {
113    [
114        NativeForeignWrapper::Memory,
115        NativeForeignWrapper::DuckDB,
116        NativeForeignWrapper::Arrow,
117    ]
118    .into_iter()
119    .map(|native| {
120        let name = native.name().to_owned();
121        (
122            name.clone(),
123            ForeignWrapperDefinition {
124                name,
125                identity: native.reference(),
126                owner: RoleIdentity::BOOTSTRAP,
127                handler: ForeignWrapperHandler::Native(native),
128                validator: None,
129                options: Vec::new(),
130            },
131        )
132    })
133    .collect()
134}
135
136/// A missing callback is a valid retained reference; a live callback must agree with its saved OID and incarnation.
137pub fn validate_functions(
138    wrappers: &ForeignWrappers,
139    routines: &crate::routines::lifecycle::RoutineRegistry,
140) -> Result<(), SQLError> {
141    for wrapper in wrappers.values() {
142        let handler = match &wrapper.handler {
143            ForeignWrapperHandler::Function(function) => Some(function),
144            _ => None,
145        };
146        for reference in handler.into_iter().chain(wrapper.validator.as_ref()) {
147            if reference.binding.builtin {
148                continue;
149            }
150            let Some(function) = routines
151                .values()
152                .flatten()
153                .find(|function| function.def.object_id == reference.binding.object_id)
154            else {
155                continue;
156            };
157            if function.def.catalog_oid != Some(reference.oid)
158                || function.def.is_procedure
159                || crate::routines::routine_signature_types(&function.def)
160                    != reference.binding.argument_types
161            {
162                return Err(invalid(
163                    &wrapper.name,
164                    "live function disagrees with its retained identity",
165                ));
166            }
167        }
168    }
169    Ok(())
170}
171
172/// A stored reference must keep its selected incarnation when a wrapper name is reused.
173pub fn bound_wrapper<'a>(
174    wrappers: &'a ForeignWrappers,
175    name: &str,
176    reference: ForeignWrapperReference,
177) -> Result<&'a ForeignWrapperDefinition, SQLError> {
178    wrappers
179        .get(name)
180        .filter(|wrapper| wrapper.identity == reference)
181        .ok_or_else(|| SQLError::Routine {
182            sqlstate: "XX000".into(),
183            message: format!(
184                "cache lookup failed for foreign-data wrapper {}",
185                reference.oid
186            ),
187        })
188}
189
190/// Validate the complete candidate before publishing any restored wrapper.
191pub fn validate_wrappers(
192    wrappers: &ForeignWrappers,
193    roles: &BTreeMap<String, RoleDefinition>,
194) -> Result<(), SQLError> {
195    let mut oids = BTreeSet::new();
196    let mut identities = BTreeSet::new();
197    for (name, wrapper) in wrappers {
198        if name.is_empty() || name.contains('\0') || name != &wrapper.name {
199            return Err(invalid(name, "invalid catalog name"));
200        }
201        let identity = wrapper.identity;
202        let valid_identity = identity.oid >= super::oids::FIRST_NORMAL_OBJECT_ID
203            || matches!(wrapper.handler, ForeignWrapperHandler::Native(native) if identity == native.reference());
204        if !valid_identity
205            || !oids.insert(identity.oid)
206            || identity.object_id == [0; 16]
207            || !identities.insert(identity.object_id)
208        {
209            return Err(invalid(name, "invalid or duplicate catalog identity"));
210        }
211        if !wrapper.owner.is_valid() || wrapper.owner.role_definition(roles).is_none() {
212            return Err(invalid(name, "owner references a missing or replaced role"));
213        }
214        let mut options = BTreeSet::new();
215        for (option, _) in &wrapper.options {
216            if option.is_empty() || option.contains(['\0', '=']) || !options.insert(option) {
217                return Err(invalid(name, "invalid or duplicate option name"));
218            }
219        }
220        let handler = match &wrapper.handler {
221            ForeignWrapperHandler::Function(function) => Some(function),
222            _ => None,
223        };
224        for reference in handler.into_iter().chain(wrapper.validator.as_ref()) {
225            let function = &reference.binding;
226            if reference.oid == 0
227                || (!function.builtin && reference.oid < super::oids::FIRST_NORMAL_OBJECT_ID)
228                || function.name.is_empty()
229                || function.resolution_error.is_some()
230                || function.dispatch.is_some()
231                || (!function.builtin && function.object_id.is_none_or(|id| id == [0; 16]))
232            {
233                return Err(invalid(name, "invalid bound function identity"));
234            }
235        }
236    }
237    Ok(())
238}
239
240fn invalid(name: &str, reason: &str) -> SQLError {
241    SQLError::Internal(format!("foreign-data wrapper `{name}`: {reason}"))
242}
243
244#[cfg(test)]
245mod tests;