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}