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    pub fn from_char(c: char) -> Self {
156        match c {
157            'i' => Self::Immutable,
158            's' => Self::Stable,
159            _ => Self::Volatile,
160        }
161    }
162}
163
164/// Map of qualified identifier to routines (overloaded functions share name).
165pub type RoutineMap = HashMap<QualifiedIdentifier, Vec<Routine>>;
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_routine_is_safe_for_get() {
173        let mut routine = Routine {
174            output_columns: Vec::new(),
175            media_type: None,
176            media_base_type: None,
177            schema: "public".into(),
178            name: "get_users".into(),
179            description: None,
180            params: vec![],
181            return_type: RetType::SetOf("users".into()),
182            returns_composite: true,
183            volatility: FuncVolatility::Stable,
184            has_variadic: false,
185            isolation_level: None,
186            settings: vec![],
187            is_procedure: false,
188        };
189
190        assert!(routine.is_safe_for_get());
191
192        routine.volatility = FuncVolatility::Volatile;
193        assert!(!routine.is_safe_for_get());
194    }
195
196    #[test]
197    fn test_ret_type_is_set_returning() {
198        assert!(!RetType::Single("text".into()).is_set_returning());
199        assert!(RetType::SetOf("users".into()).is_set_returning());
200        assert!(RetType::Table(vec![("id".into(), "int".into())]).is_set_returning());
201        assert!(!RetType::Void.is_set_returning());
202    }
203
204    #[test]
205    fn test_func_volatility_from_char() {
206        assert_eq!(FuncVolatility::from_char('i'), FuncVolatility::Immutable);
207        assert_eq!(FuncVolatility::from_char('s'), FuncVolatility::Stable);
208        assert_eq!(FuncVolatility::from_char('v'), FuncVolatility::Volatile);
209        assert_eq!(FuncVolatility::from_char('x'), FuncVolatility::Volatile);
210    }
211}