Skip to main content

postrust_core/plan/
call_plan.rs

1//! RPC (stored function) call planning.
2
3use crate::api_request::{ApiRequest, Payload, QualifiedIdentifier};
4use crate::error::{Error, Result};
5use crate::schema_cache::Routine;
6use serde::{Deserialize, Serialize};
7
8/// A plan for calling a stored function.
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct CallPlan {
11    /// Function identifier
12    pub function: QualifiedIdentifier,
13    /// Call parameters
14    pub params: CallParams,
15    /// Whether to return a scalar result
16    pub returns_scalar: bool,
17    /// The declared return type, as PostgreSQL spells it.
18    ///
19    /// Used to decide whether this process can decode the result or whether
20    /// the database should render it, exactly as for a table's columns.
21    #[serde(default)]
22    pub return_type: Option<String>,
23    /// Whether the function is set-returning
24    pub returns_set: bool,
25    /// Whether the function returns nothing at all.
26    ///
27    /// A `void` function has no result to report, which is 204 rather than a
28    /// body of `null` -- and `RetType::Void` has no type name, so asking for
29    /// one would never find it.
30    #[serde(default)]
31    pub returns_void: bool,
32    /// Whether the return type is composite (row type or `record`): its
33    /// columns are real output columns, never the function-name wrapper.
34    #[serde(default)]
35    pub returns_composite: bool,
36    /// Function volatility (for transaction handling)
37    pub volatility: String,
38    /// Declared parameter types, in the routine's own order, as
39    /// `(name, pg_type)`.
40    ///
41    /// Arguments arrive from the URL or a JSON body as strings. Binding them
42    /// as `text` leaves PostgreSQL unable to resolve any signature that isn't
43    /// text -- `add_them(a => text, b => text) does not exist` -- so each one
44    /// is cast to its declared type at call time.
45    ///
46    /// The type is the one an argument can be cast to without losing part of
47    /// itself: a `char(4)` parameter is stored as bare `character`, whose
48    /// length is one, and a parameter's length is not enforced anyway.
49    #[serde(default)]
50    pub param_types: Vec<(String, String)>,
51    /// The media type the function's value already is, where its return type
52    /// is a domain that says so.
53    ///
54    /// The result is that media type rather than something to serialise as
55    /// JSON, so it is returned verbatim -- but only where the client asked for
56    /// it, since a function whose value is a PNG is still a value a client may
57    /// want as JSON.
58    #[serde(default)]
59    pub media_type: Option<String>,
60    /// The type that domain is a domain over -- what says whether the value
61    /// is bytes or text.
62    #[serde(default)]
63    pub media_base_type: Option<String>,
64    /// The columns the result has, where the function declares its own.
65    ///
66    /// `RETURNS TABLE(a text, b colour)` has no table to read the result's
67    /// shape from, so `SELECT *` is all that can be asked for -- and a column
68    /// of a type this process cannot decode comes back null. Knowing the
69    /// columns is what lets the database render those instead.
70    #[serde(default)]
71    pub output_columns: Vec<(String, String)>,
72    /// The names of the parameters declared `VARIADIC`.
73    ///
74    /// Named notation spells one `VARIADIC v := ...`; `v => ...` finds no
75    /// function at all, because the array is the whole of the variadic tail
76    /// rather than one value of it.
77    #[serde(default)]
78    pub variadic_params: Vec<String>,
79}
80
81/// How parameters are passed to the function.
82#[derive(Clone, Debug, Serialize, Deserialize)]
83pub enum CallParams {
84    /// Named parameters from URL query or JSON body
85    Named(Vec<(String, String)>),
86    /// Positional parameters (from JSON array)
87    Positional(Vec<String>),
88    /// Single JSON object passed as first argument
89    SingleObject(bytes::Bytes),
90    /// The whole request body, as the one parameter the function does not name.
91    ///
92    /// `unnamed_json_param(json)` declares a parameter with no name, so no
93    /// argument can be addressed to it; what it takes is the body itself,
94    /// cast to the type it declares. An absent body is that parameter's
95    /// `NULL`, not a call with no arguments -- the function has one either
96    /// way.
97    SingleUnnamed {
98        /// The raw body, absent when the request had none.
99        body: Option<bytes::Bytes>,
100        /// The type the parameter declares, which the body is cast to.
101        pg_type: String,
102    },
103    /// No parameters
104    None,
105}
106
107impl CallPlan {
108    /// Create a call plan from an API request.
109    pub fn from_request(request: &ApiRequest, routine: &Routine) -> Result<Self> {
110        let qi = routine.qualified_identifier();
111
112        let params = extract_call_params(request, routine)?;
113
114        let returns_scalar = !routine.return_type.is_set_returning()
115            && routine
116                .return_type
117                .type_name()
118                .map(|t| !t.contains("record"))
119                .unwrap_or(true);
120
121        Ok(Self {
122            function: qi,
123            params,
124            returns_scalar,
125            return_type: routine.return_type.type_name().map(str::to_string),
126            returns_set: routine.return_type.is_set_returning(),
127            returns_void: matches!(routine.return_type, crate::schema_cache::RetType::Void),
128            returns_composite: routine.returns_composite,
129            volatility: format!("{:?}", routine.volatility),
130            param_types: routine
131                .params
132                .iter()
133                .map(|p| (p.name.clone(), p.type_max_length.clone()))
134                .collect(),
135            media_type: routine.media_type.clone(),
136            media_base_type: routine.media_base_type.clone(),
137            output_columns: routine.output_columns.clone(),
138            variadic_params: routine
139                .params
140                .iter()
141                .filter(|p| p.variadic)
142                .map(|p| p.name.clone())
143                .collect(),
144        })
145    }
146
147    /// Check if this call has parameters.
148    pub fn has_params(&self) -> bool {
149        !matches!(self.params, CallParams::None)
150    }
151}
152
153/// Extract call parameters from request.
154fn extract_call_params(request: &ApiRequest, routine: &Routine) -> Result<CallParams> {
155    // A parameter with no name cannot be given an argument by name. What such
156    // a function takes is the body itself, whatever the body says.
157    if let [param] = routine.params.as_slice() {
158        if param.name.is_empty() {
159            let body = match &request.payload {
160                Some(Payload::ProcessedJson { raw, .. })
161                | Some(Payload::RawJson(raw))
162                | Some(Payload::RawPayload(raw)) => Some(raw.clone()),
163                Some(Payload::ProcessedUrlEncoded { .. }) | None => None,
164            };
165            return Ok(CallParams::SingleUnnamed {
166                body,
167                pg_type: param.param_type.clone(),
168            });
169        }
170    }
171
172    // Check for JSON body first
173    if let Some(payload) = &request.payload {
174        match payload {
175            Payload::ProcessedJson { raw, .. } => {
176                // Check if it's an object or array
177                let value: serde_json::Value =
178                    serde_json::from_slice(raw).map_err(|e| Error::InvalidBody(e.to_string()))?;
179
180                // A call takes one set of arguments. A body written as an
181                // array offers several, and PostgREST reads the first --
182                // `LIMIT 1` over the record set it makes of the body.
183                let value = match value {
184                    serde_json::Value::Array(rows)
185                        if rows.first().is_some_and(serde_json::Value::is_object) =>
186                    {
187                        rows.into_iter().next().unwrap_or(serde_json::Value::Null)
188                    }
189                    other => other,
190                };
191
192                match value {
193                    serde_json::Value::Object(map) => {
194                        // `?columns=` names which of the body's keys are
195                        // arguments, and a key the function does not declare
196                        // is not one -- the body may carry more than the call
197                        // needs.
198                        let map: serde_json::Map<String, serde_json::Value> = map
199                            .into_iter()
200                            .filter(|(name, _)| {
201                                request
202                                    .query_params
203                                    .columns
204                                    .as_ref()
205                                    .map(|columns| columns.contains(name))
206                                    .unwrap_or(true)
207                                    && routine.params.iter().any(|p| &p.name == name)
208                            })
209                            .collect();
210
211                        // Named parameters from JSON object
212                        let params: Vec<(String, String)> = map
213                            .into_iter()
214                            .map(|(k, v)| {
215                                // A `json` parameter takes the body's value as
216                                // the body wrote it: a JSON string is a JSON
217                                // string, and unquoting it first turns
218                                // `"{\"key\": 3}"` into an object the caller
219                                // did not pass -- or, for `"text"`, into
220                                // something that is not JSON at all.
221                                let takes_json = routine.params.iter().any(|p| {
222                                    p.name == k && matches!(p.param_type.as_str(), "json" | "jsonb")
223                                });
224                                // Extract string values without JSON quotes
225                                let value = match v {
226                                    ref value if takes_json => value.to_string(),
227                                    serde_json::Value::String(s) => s,
228                                    serde_json::Value::Null => String::new(),
229                                    // A JSON array for a parameter the function
230                                    // declares as an array is that array, not a
231                                    // string spelled in JSON: `{"v":["a","b"]}`
232                                    // passes two values to a variadic. PostgreSQL
233                                    // reads `{...}`, not `[...]`.
234                                    serde_json::Value::Array(items)
235                                        if routine.params.iter().any(|p| {
236                                            p.name == k && p.param_type.ends_with("[]")
237                                        }) =>
238                                    {
239                                        array_literal(
240                                            &items
241                                                .iter()
242                                                .map(|item| match item {
243                                                    serde_json::Value::String(s) => s.clone(),
244                                                    other => other.to_string(),
245                                                })
246                                                .collect::<Vec<_>>(),
247                                        )
248                                    }
249                                    other => other.to_string(),
250                                };
251                                (k, value)
252                            })
253                            .collect();
254                        return Ok(CallParams::Named(params));
255                    }
256                    serde_json::Value::Array(_) => {
257                        // Pass entire JSON as single argument
258                        return Ok(CallParams::SingleObject(raw.clone()));
259                    }
260                    _ => {
261                        // Scalar value - pass as single argument
262                        return Ok(CallParams::SingleObject(raw.clone()));
263                    }
264                }
265            }
266            Payload::ProcessedUrlEncoded { data, .. } => {
267                // A form body is named arguments like any other, repeats and
268                // all: the same name twice is one variadic argument or the
269                // last of them, never two arguments.
270                return Ok(named_arguments(
271                    data.iter()
272                        .map(|(name, value)| (name.clone(), value.clone())),
273                    routine,
274                ));
275            }
276            Payload::RawJson(raw) | Payload::RawPayload(raw) => {
277                return Ok(CallParams::SingleObject(raw.clone()));
278            }
279        }
280    }
281
282    // Fall back to query parameters. Only those naming a declared parameter
283    // are arguments -- the rest are filters on the function's result, which
284    // is how `/rpc/getallprojects?id=eq.1` filters rather than failing.
285    if !request.query_params.params.is_empty() {
286        let mut args: Vec<(String, Vec<String>)> = Vec::new();
287        for (name, value) in &request.query_params.params {
288            // A value carrying an operator filters the result; only a bare
289            // one is an argument. `?id=5&id=gt.2` is both at once.
290            if crate::api_request::value_is_filter(value) {
291                continue;
292            }
293            if !routine.params.iter().any(|p| &p.name == name) {
294                continue;
295            }
296            match args.iter_mut().find(|(existing, _)| existing == name) {
297                Some((_, values)) => values.push(value.clone()),
298                None => args.push((name.clone(), vec![value.clone()])),
299            }
300        }
301
302        // A name given more than once is one argument, not two: for a
303        // variadic parameter every value goes into the array it takes, and
304        // for any other the last one wins -- repeating it cannot mean asking
305        // for two values where the signature has room for one.
306        let args: Vec<(String, String)> = args
307            .into_iter()
308            .map(|(name, values)| {
309                let variadic = routine.params.iter().any(|p| p.name == name && p.variadic);
310                let value = match variadic {
311                    true => array_literal(&values),
312                    false => values.last().cloned().unwrap_or_default(),
313                };
314                (name, value)
315            })
316            .collect();
317
318        if !args.is_empty() {
319            return Ok(CallParams::Named(args));
320        }
321    }
322
323    // No parameters
324    Ok(CallParams::None)
325}
326
327/// Group named arguments, collapsing repeats the way a call must.
328///
329/// A name given more than once is one argument, not two: for a variadic
330/// parameter every value goes into the array it takes, and for any other the
331/// last one wins -- repeating it cannot mean asking for two values where the
332/// signature has room for one. Names the function does not declare are left
333/// out; the body may carry more than the call needs.
334fn named_arguments(
335    supplied: impl IntoIterator<Item = (String, String)>,
336    routine: &Routine,
337) -> CallParams {
338    let mut grouped: Vec<(String, Vec<String>)> = Vec::new();
339    for (name, value) in supplied {
340        if !routine.params.iter().any(|p| p.name == name) {
341            continue;
342        }
343        match grouped.iter_mut().find(|(existing, _)| *existing == name) {
344            Some((_, values)) => values.push(value),
345            None => grouped.push((name, vec![value])),
346        }
347    }
348
349    let args: Vec<(String, String)> = grouped
350        .into_iter()
351        .map(|(name, values)| {
352            let variadic = routine.params.iter().any(|p| p.name == name && p.variadic);
353            let value = match variadic {
354                true => array_literal(&values),
355                false => values.last().cloned().unwrap_or_default(),
356            };
357            (name, value)
358        })
359        .collect();
360
361    match args.is_empty() {
362        true => CallParams::None,
363        false => CallParams::Named(args),
364    }
365}
366
367/// Render values as a PostgreSQL array literal.
368///
369/// Every element is quoted, which is always valid and saves deciding which
370/// ones would otherwise need it -- an element containing a comma, a brace or a
371/// quote of its own would silently change the array's shape.
372fn array_literal(values: &[String]) -> String {
373    let elements: Vec<String> = values
374        .iter()
375        .map(|value| format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\"")))
376        .collect();
377    format!("{{{}}}", elements.join(","))
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use crate::schema_cache::{FuncVolatility, RetType};
384
385    fn make_routine() -> Routine {
386        Routine {
387            output_columns: Vec::new(),
388            media_type: None,
389            media_base_type: None,
390            schema: "public".into(),
391            name: "get_users".into(),
392            description: None,
393            params: vec![],
394            return_type: RetType::SetOf("users".into()),
395            returns_composite: true,
396            volatility: FuncVolatility::Stable,
397            has_variadic: false,
398            isolation_level: None,
399            settings: vec![],
400            is_procedure: false,
401        }
402    }
403
404    #[test]
405    fn test_call_plan_basic() {
406        let request = ApiRequest::default();
407        let routine = make_routine();
408
409        let plan = CallPlan::from_request(&request, &routine).unwrap();
410
411        assert_eq!(plan.function.name, "get_users");
412        assert!(plan.returns_set);
413        assert!(!plan.returns_scalar);
414        assert!(plan.returns_composite);
415    }
416
417    #[test]
418    fn test_call_plan_scalar_is_not_composite() {
419        let request = ApiRequest::default();
420        let routine = Routine {
421            return_type: RetType::Single("integer".into()),
422            returns_composite: false,
423            ..make_routine()
424        };
425
426        let plan = CallPlan::from_request(&request, &routine).unwrap();
427
428        assert!(plan.returns_scalar);
429        assert!(!plan.returns_set);
430        assert!(!plan.returns_composite);
431    }
432
433    #[test]
434    fn test_call_params_none() {
435        let request = ApiRequest::default();
436        let routine = make_routine();
437
438        let plan = CallPlan::from_request(&request, &routine).unwrap();
439        assert!(!plan.has_params());
440    }
441}