Skip to main content

uqa_sql/catalog/
foreign_server.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Foreign-server catalog identity and ownership, independent of a transport implementation.
8
9use super::roles::{identity::RoleSubject, RoleDefinition, RoleIdentity};
10use crate::SQLError;
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct ForeignServerMetadata {
17    pub oid: u32,
18    pub object_id: [u8; 16],
19    pub owner: RoleIdentity,
20    pub server_type: Option<String>,
21    pub version: Option<String>,
22    /// Written option names, absent only in preceding map-only records.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub option_order: Option<Vec<String>>,
25    /// Missing only while restoring a catalog that predates wrapper identities.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub wrapper_reference: Option<super::foreign_wrapper::ForeignWrapperReference>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ForeignServerDefinition {
32    pub name: String,
33    pub fdw_type: String,
34    pub options: BTreeMap<String, String>,
35    pub metadata: ForeignServerMetadata,
36}
37
38impl ForeignServerDefinition {
39    pub fn bound_wrapper<'a>(
40        &self,
41        wrappers: &'a super::foreign_wrapper::ForeignWrappers,
42    ) -> Result<&'a super::foreign_wrapper::ForeignWrapperDefinition, SQLError> {
43        let reference = self.metadata.wrapper_reference.ok_or_else(|| {
44            SQLError::Internal(format!(
45                "foreign server `{}` has no wrapper identity",
46                self.name
47            ))
48        })?;
49        super::foreign_wrapper::bound_wrapper(wrappers, &self.fdw_type, reference)
50    }
51}
52
53/// Validate identities before publishing any restored definition. A name, OID or incarnation cannot alias another server, and an owner must still be the role incarnation that created it.
54pub fn validate_foreign_servers(
55    servers: &BTreeMap<String, ForeignServerDefinition>,
56    roles: &BTreeMap<String, RoleDefinition>,
57) -> Result<(), SQLError> {
58    let mut oids = BTreeSet::new();
59    let mut identities = BTreeSet::new();
60    for (name, server) in servers {
61        let metadata = &server.metadata;
62        validate_foreign_server_name(name)?;
63        if name != &server.name {
64            return Err(invalid(name, "invalid catalog name"));
65        }
66        if metadata.oid < super::oids::FIRST_NORMAL_OBJECT_ID || !oids.insert(metadata.oid) {
67            return Err(invalid(name, "invalid or duplicate catalog OID"));
68        }
69        if metadata.object_id == [0; 16] || !identities.insert(metadata.object_id) {
70            return Err(invalid(name, "invalid or duplicate object identity"));
71        }
72        if let Some(order) = &metadata.option_order {
73            let names = order.iter().collect::<BTreeSet<_>>();
74            if names.len() != order.len()
75                || order.len() != server.options.len()
76                || order.iter().any(|name| !server.options.contains_key(name))
77            {
78                return Err(invalid(name, "option order disagrees with stored options"));
79            }
80        }
81        if !metadata.owner.is_valid() || metadata.owner.role_definition(roles).is_none() {
82            return Err(invalid(name, "owner references a missing or replaced role"));
83        }
84    }
85    Ok(())
86}
87
88/// SQL rejects an empty identifier during parsing; direct callers must reject it before a durable definition is written.
89pub fn validate_foreign_server_name(name: &str) -> Result<(), SQLError> {
90    if name.is_empty() {
91        return Err(invalid(name, "invalid catalog name"));
92    }
93    Ok(())
94}
95
96fn invalid(name: &str, reason: &str) -> SQLError {
97    SQLError::Internal(format!("foreign server `{name}`: {reason}"))
98}
99
100#[cfg(test)]
101mod tests;