Skip to main content

postrust_core/schema_cache/
routine.rs

1//! Stored function/procedure types.
2
3use crate::api_request::QualifiedIdentifier;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// A stored function or procedure.
8#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct Routine {
10    /// Schema name
11    pub schema: String,
12    /// Function name
13    pub name: String,
14    /// Description from comment
15    pub description: Option<String>,
16    /// Function parameters
17    pub params: Vec<RoutineParam>,
18    /// Return type
19    pub return_type: RetType,
20    /// Whether the return type is composite (a row type or `record`), meaning
21    /// `SELECT * FROM fn()` expands to the type's own columns rather than a
22    /// single column named after the function.
23    #[serde(default)]
24    pub returns_composite: bool,
25    /// Function volatility
26    pub volatility: FuncVolatility,
27    /// Whether the function has VARIADIC parameters
28    pub has_variadic: bool,
29    /// Isolation level (if set by function)
30    pub isolation_level: Option<String>,
31    /// Function-level GUC settings
32    pub settings: Vec<(String, String)>,
33    /// Whether this is a procedure (vs function)
34    pub is_procedure: bool,
35    /// The media type this function's value already is.
36    ///
37    /// A domain named `image/png` is a declaration about the value, not a type
38    /// the API layer serialises: `returns "text/plain"` returns text, and
39    /// wrapping it in JSON would be answering a question nobody asked. `*/*`
40    /// is spelled that way in the catalogue and reaches a client as
41    /// `application/octet-stream`, which is what "some bytes" is called on the
42    /// wire.
43    #[serde(default)]
44    pub media_type: Option<String>,
45    /// The type that media-type domain is a domain over.
46    ///
47    /// A `bytea` renders as `\x` followed by hex when this side cannot name
48    /// its type, and that rendering is a description of the bytes rather than
49    /// the bytes. See [`Self::media_type`].
50    #[serde(default)]
51    pub media_base_type: Option<String>,
52    /// The columns a `RETURNS TABLE` function's rows have, as `(name, type)`.
53    ///
54    /// A function returning rows of a table is shaped by that table, whose
55    /// columns are known. One declaring its own columns is not, and this is
56    /// the only account of them -- without it the result can only be selected
57    /// with `*`, and a column of a type this process cannot decode comes back
58    /// null with nothing to say it should have been rendered by the database.
59    #[serde(default)]
60    pub output_columns: Vec<(String, String)>,
61}
62
63impl Routine {
64    /// The media type this function produces, as a client would name it.
65    pub fn produced_media_type(&self) -> Option<&str> {
66        match self.media_type.as_deref() {
67            Some("*/*") => Some("application/octet-stream"),
68            other => other,
69        }
70    }
71}
72
73impl Routine {
74    /// Get the qualified identifier for this routine.
75    pub fn qualified_identifier(&self) -> QualifiedIdentifier {
76        QualifiedIdentifier::new(&self.schema, &self.name)
77    }
78
79    /// Check if this function is safe for GET requests.
80    pub fn is_safe_for_get(&self) -> bool {
81        matches!(
82            self.volatility,
83            FuncVolatility::Immutable | FuncVolatility::Stable
84        )
85    }
86
87    /// Get required parameters (no default).
88    pub fn required_params(&self) -> impl Iterator<Item = &RoutineParam> {
89        self.params.iter().filter(|p| p.required)
90    }
91
92    /// Find a parameter by name.
93    pub fn find_param(&self, name: &str) -> Option<&RoutineParam> {
94        self.params.iter().find(|p| p.name == name)
95    }
96}
97
98/// A function parameter.
99#[derive(Clone, Debug, Serialize, Deserialize)]
100pub struct RoutineParam {
101    /// Parameter name
102    pub name: String,
103    /// PostgreSQL type
104    pub param_type: String,
105    /// Type with max length info
106    pub type_max_length: String,
107    /// Whether this parameter is required
108    pub required: bool,
109    /// Whether this is a VARIADIC parameter
110    pub variadic: bool,
111}
112
113/// Function return type.
114#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
115pub enum RetType {
116    /// Returns a single value
117    Single(String),
118    /// Returns a set of values (SETOF)
119    SetOf(String),
120    /// Returns a table (RETURNS TABLE)
121    Table(Vec<(String, String)>),
122    /// Returns void
123    Void,
124}
125
126impl RetType {
127    /// Check if this returns multiple rows.
128    pub fn is_set_returning(&self) -> bool {
129        matches!(self, Self::SetOf(_) | Self::Table(_))
130    }
131
132    /// Get the base type name.
133    pub fn type_name(&self) -> Option<&str> {
134        match self {
135            Self::Single(t) => Some(t),
136            Self::SetOf(t) => Some(t),
137            Self::Table(_) => None,
138            Self::Void => None,
139        }
140    }
141}
142
143/// Function volatility category.
144#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
145pub enum FuncVolatility {
146    /// Function cannot modify database and always returns same result for same inputs
147    Immutable,
148    /// Function cannot modify database but result may change across queries
149    Stable,
150    /// Function can modify database
151    Volatile,
152}
153
154impl FuncVolatility {
155    /// Read from `pg_proc.provolatile`, which is `i`, `s` or `v`.
156    ///
157    /// Anything else is treated as volatile: that is the assumption which
158    /// forbids the most, so an unrecognised value cannot make a function look
159    /// safer to call than it is.
160    pub fn from_char(c: char) -> Self {
161        match c {
162            'i' => Self::Immutable,
163            's' => Self::Stable,
164            _ => Self::Volatile,
165        }
166    }
167}
168
169/// Map of qualified identifier to routines (overloaded functions share name).
170pub type RoutineMap = HashMap<QualifiedIdentifier, Vec<Routine>>;
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_routine_is_safe_for_get() {
178        let mut routine = Routine {
179            output_columns: Vec::new(),
180            media_type: None,
181            media_base_type: None,
182            schema: "public".into(),
183            name: "get_users".into(),
184            description: None,
185            params: vec![],
186            return_type: RetType::SetOf("users".into()),
187            returns_composite: true,
188            volatility: FuncVolatility::Stable,
189            has_variadic: false,
190            isolation_level: None,
191            settings: vec![],
192            is_procedure: false,
193        };
194
195        assert!(routine.is_safe_for_get());
196
197        routine.volatility = FuncVolatility::Volatile;
198        assert!(!routine.is_safe_for_get());
199    }
200
201    #[test]
202    fn test_ret_type_is_set_returning() {
203        assert!(!RetType::Single("text".into()).is_set_returning());
204        assert!(RetType::SetOf("users".into()).is_set_returning());
205        assert!(RetType::Table(vec![("id".into(), "int".into())]).is_set_returning());
206        assert!(!RetType::Void.is_set_returning());
207    }
208
209    #[test]
210    fn test_func_volatility_from_char() {
211        assert_eq!(FuncVolatility::from_char('i'), FuncVolatility::Immutable);
212        assert_eq!(FuncVolatility::from_char('s'), FuncVolatility::Stable);
213        assert_eq!(FuncVolatility::from_char('v'), FuncVolatility::Volatile);
214        assert_eq!(FuncVolatility::from_char('x'), FuncVolatility::Volatile);
215    }
216}