Skip to main content

postrust_core/plan/
mod.rs

1//! Query planning module.
2//!
3//! Converts parsed API requests into execution plans that can be
4//! translated to SQL queries.
5
6mod call_plan;
7mod mutate_plan;
8mod read_plan;
9mod types;
10
11pub use call_plan::{CallParams, CallPlan};
12pub use mutate_plan::MutatePlan;
13pub use read_plan::{resolve_top_level_range, ReadPlan, ReadPlanTree};
14pub use types::*;
15
16use crate::api_request::{Action, ApiRequest, DbAction, QualifiedIdentifier};
17use crate::error::{Error, Result};
18use crate::schema_cache::{Routine, SchemaCache};
19use std::collections::HashSet;
20
21/// The execution plan for an API request.
22#[derive(Clone, Debug)]
23pub enum ActionPlan {
24    /// Plan that requires database access
25    Db(DbActionPlan),
26    /// Plan that doesn't need database (OPTIONS, OpenAPI)
27    Info(InfoPlan),
28}
29
30/// Database action plan.
31#[derive(Clone, Debug)]
32pub enum DbActionPlan {
33    /// Read operation (SELECT)
34    Read(ReadPlanTree),
35    /// Mutation operation (INSERT/UPDATE/DELETE)
36    MutateRead {
37        /// The write to perform.
38        mutate: MutatePlan,
39        /// What to read back afterwards. `None` when the client asked for no
40        /// representation, in which case nothing is selected.
41        read: Option<ReadPlanTree>,
42    },
43    /// RPC call
44    Call {
45        /// The function call.
46        call: CallPlan,
47        /// How to shape what the function returned, when it returns rows.
48        read: Option<ReadPlanTree>,
49    },
50}
51
52/// Info-only plan (no database access needed).
53#[derive(Clone, Debug)]
54pub enum InfoPlan {
55    /// OPTIONS on a table
56    RelationInfo(QualifiedIdentifier),
57    /// OPTIONS on a function
58    RoutineInfo(QualifiedIdentifier),
59    /// OpenAPI spec
60    OpenApiSpec,
61}
62
63/// Create an action plan from an API request.
64pub fn create_action_plan(request: &ApiRequest, schema_cache: &SchemaCache) -> Result<ActionPlan> {
65    match &request.action {
66        Action::Db(db_action) => {
67            // SchemaRead is a special case - it returns OpenAPI spec, not a DB query
68            if matches!(db_action, DbAction::SchemaRead { .. }) {
69                return Ok(ActionPlan::Info(InfoPlan::OpenApiSpec));
70            }
71            validate_embed_paths(request)?;
72            let plan = create_db_plan(request, db_action, schema_cache)?;
73            Ok(ActionPlan::Db(plan))
74        }
75        Action::RelationInfo(qi) => Ok(ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()))),
76        Action::RoutineInfo { qi, .. } => Ok(ActionPlan::Info(InfoPlan::RoutineInfo(qi.clone()))),
77        Action::SchemaInfo => Ok(ActionPlan::Info(InfoPlan::OpenApiSpec)),
78    }
79}
80
81/// The overload whose parameters most resemble the ones supplied.
82///
83/// Compared as one string of sorted names -- `a, b, c` -- because what a
84/// client gets wrong is usually one name out of several, and comparing the
85/// lists as a whole is what notices that. `None` when nothing is close enough
86/// to be worth saying; a bad guess is worse than silence.
87fn nearest_overload(
88    qi: &QualifiedIdentifier,
89    routines: &[crate::schema_cache::Routine],
90    supplied: &[String],
91) -> Option<String> {
92    // No floor at all, unlike a name: the nearest signature that shares any
93    // character sequence with what was asked for is offered. A client that got
94    // one parameter of three wrong is not close by any similarity measure --
95    // `(a, b)` against `(a, b, smthelse)` scores a third -- and naming a
96    // parameter list discloses nothing, since the client is trying to write
97    // one. A name is different: there a bad guess names an object the client
98    // did not know about, which is why that hint keeps its floor.
99    //
100    // Fitted against every hint the reference gives rather than reasoned from:
101    // a floor of a third answers seven of the eight correctly, which is
102    // exactly the kind of nearly-right that looks settled.
103    const MIN_SIMILARITY: f64 = 0.0;
104
105    // Compared as the parameter list is written -- parentheses included --
106    // because that is what the client sees and what it would have to change.
107    let listed = |names: &mut Vec<String>| -> String {
108        names.sort();
109        format!("({})", names.join(", "))
110    };
111    let asked = listed(&mut supplied.to_vec());
112
113    let candidates: Vec<String> = routines
114        .iter()
115        .map(|routine| listed(&mut routine.params.iter().map(|p| p.name.clone()).collect()))
116        .collect();
117
118    crate::schema_cache::closest(
119        candidates.iter().map(String::as_str),
120        &asked,
121        MIN_SIMILARITY,
122    )
123    .map(|params| format!("{}{}", qi, params))
124}
125
126/// Check that every dotted filter, order or range names a resource that was
127/// embedded.
128///
129/// `?non_existent_projects.name=like.*x*` looks like a filter on an embedded
130/// resource, and answering it as though the prefix meant nothing would return
131/// the whole table with no sign that the filter was dropped. PostgREST reports
132/// the first segment it cannot account for, which is the one the client got
133/// wrong.
134fn validate_embed_paths(request: &ApiRequest) -> Result<()> {
135    use crate::api_request::SelectItem;
136
137    // An embed answers to the alias the request gave it and to the relation's
138    // own name: `the_tasks:tasks(*)` is addressed by either.
139    fn descend<'a>(items: &'a [SelectItem], segment: &str) -> Option<&'a [SelectItem]> {
140        items.iter().find_map(|item| match item {
141            SelectItem::Relation {
142                relation,
143                alias,
144                select,
145                ..
146            } if relation == segment || alias.as_deref() == Some(segment) => {
147                Some(select.as_slice())
148            }
149            SelectItem::SpreadRelation {
150                relation, select, ..
151            } if relation == segment => Some(select.as_slice()),
152            _ => None,
153        })
154    }
155
156    let check = |path: &[String]| -> Result<()> {
157        let mut items = request.query_params.select.as_slice();
158        for segment in path {
159            items = descend(items, segment)
160                .ok_or_else(|| Error::NotAnEmbeddedResource(segment.clone()))?;
161        }
162        Ok(())
163    };
164
165    for (path, _) in &request.query_params.filters {
166        check(path)?;
167    }
168    for (path, _) in &request.query_params.order {
169        check(path)?;
170    }
171    for (path, _) in &request.query_params.logic {
172        check(path)?;
173    }
174    for path in request.query_params.ranges.keys() {
175        if !path.is_empty() {
176            check(&path.split('.').map(String::from).collect::<Vec<_>>())?;
177        }
178    }
179
180    Ok(())
181}
182
183/// The arguments a request supplies to a function.
184///
185/// On a function call every query parameter is a candidate argument, but a
186/// value carrying an operator -- `id=gt.1` -- is a filter over the result
187/// instead. That is what lets `/rpc/getallprojects?id=gt.1` call a function
188/// taking nothing at all, while `/rpc/add_them?a=1&b=2&smthelse=x` is a call
189/// with an argument no signature declares.
190fn supplied_arguments(request: &ApiRequest) -> Vec<String> {
191    use crate::api_request::Payload;
192
193    // Over POST the arguments are the body's keys, and the query string holds
194    // filters over the result. Reading only the query string there picked the
195    // signature that takes nothing at all.
196    //
197    // `?columns=` overrides them: it says which of the body's keys are
198    // arguments, so a body carrying more than the call needs still names one
199    // signature rather than none.
200    let is_post = matches!(
201        request.action,
202        Action::Db(DbAction::Routine {
203            invoke_method: crate::api_request::InvokeMethod::Inv,
204            ..
205        })
206    );
207    if is_post {
208        if let Some(columns) = &request.query_params.columns {
209            let mut names: Vec<String> = columns.iter().cloned().collect();
210            names.sort();
211            return names;
212        }
213    }
214
215    // A name is reported and matched as a set, so the order the body or the
216    // query string happened to use is not part of it.
217    let sorted = |mut names: Vec<String>| -> Vec<String> {
218        names.sort();
219        names.dedup();
220        names
221    };
222
223    match &request.payload {
224        Some(Payload::ProcessedJson { keys, .. }) => return sorted(keys.iter().cloned().collect()),
225        Some(Payload::ProcessedUrlEncoded { data, .. }) => {
226            return sorted(data.iter().map(|(name, _)| name.clone()).collect())
227        }
228        // A raw body is one unnamed argument, so it names no parameter at all.
229        Some(Payload::RawJson(_)) | Some(Payload::RawPayload(_)) => return Vec::new(),
230        None if is_post => return Vec::new(),
231        _ => {}
232    }
233
234    // A filter on an embedded resource is keyed by its path -- `clients.id`
235    // -- and is no more an argument than a filter on the result itself.
236    let embedded: HashSet<String> = request
237        .query_params
238        .filters
239        .iter()
240        .map(|(path, filter)| {
241            let mut key = path.join(".");
242            key.push('.');
243            key.push_str(&filter.field.name);
244            key
245        })
246        .collect();
247
248    let mut names: Vec<String> = Vec::new();
249    for (name, value) in &request.query_params.params {
250        if crate::api_request::value_is_filter(value) || embedded.contains(name) {
251            continue;
252        }
253        if !names.contains(name) {
254            names.push(name.clone());
255        }
256    }
257    names.sort();
258    names
259}
260
261/// Choose which signature of a function the supplied arguments call.
262///
263/// A name may carry several, and taking whichever happened to be loaded first
264/// calls the wrong one -- or calls one the arguments do not fit, which
265/// PostgreSQL then rejects with a message about a function that does not
266/// exist, where the truth is that none of them matched.
267///
268/// Two kinds of signature can answer a call. One names its parameters, and
269/// fits when the arguments supplied are exactly its required ones plus any of
270/// its optional ones. The other has a single parameter with no name, and takes
271/// the whole request body as that one argument -- so it fits whatever the body
272/// says, and only when the body's media type matches the parameter's type.
273///
274/// A named match always wins; the unnamed one is what a call falls back to. If
275/// nothing is left the function was not found, and if more than one remains at
276/// the same level the call is ambiguous -- saying so is more use than picking
277/// one.
278fn select_overload<'a>(
279    routines: &'a [Routine],
280    request: &ApiRequest,
281) -> Result<Option<&'a Routine>> {
282    let supplied = supplied_arguments(request);
283    let is_post = matches!(
284        request.action,
285        Action::Db(DbAction::Routine {
286            invoke_method: crate::api_request::InvokeMethod::Inv,
287            ..
288        })
289    );
290
291    let (named, unnamed): (Vec<&Routine>, Vec<&Routine>) =
292        routines
293            .iter()
294            .fold((Vec::new(), Vec::new()), |mut acc, r| {
295                if matches_params(r, &supplied, is_post, &request.content_media_type) {
296                    acc.0.push(r);
297                } else if takes_whole_body(r, is_post, &request.content_media_type) {
298                    acc.1.push(r);
299                }
300                acc
301            });
302
303    let candidates = match named.is_empty() {
304        true => unnamed,
305        false => named,
306    };
307
308    match candidates.len() {
309        0 => Ok(None),
310        1 => Ok(candidates.first().copied()),
311        _ => Err(Error::AmbiguousFunction {
312            candidates: candidates.iter().map(|r| signature_of(r)).collect(),
313        }),
314    }
315}
316
317/// Whether a signature's parameters are exactly what the request supplied.
318///
319/// Required parameters must all be given and optional ones may be; an argument
320/// naming no parameter at all disqualifies the signature, which is what makes
321/// `add_them(a, b)` not answer a call passing `a`, `b` and `smthelse`.
322fn matches_params(
323    routine: &Routine,
324    supplied: &[String],
325    is_post: bool,
326    content: &crate::api_request::MediaType,
327) -> bool {
328    if routine.params.is_empty() {
329        // A body posted as text, xml or bytes is one unnamed argument. A
330        // signature taking nothing cannot receive it, however empty the
331        // argument list looks.
332        return supplied.is_empty() && !(is_post && is_raw_body_type(content));
333    }
334
335    let declares = |name: &String, required: bool| {
336        routine
337            .params
338            .iter()
339            .any(|p| &p.name == name && p.required == required)
340    };
341
342    routine
343        .params
344        .iter()
345        .filter(|p| p.required)
346        .all(|p| supplied.contains(&p.name))
347        && supplied
348            .iter()
349            .all(|name| declares(name, true) || declares(name, false))
350}
351
352/// Whether a signature is the single unnamed parameter that takes the body.
353///
354/// Only over `POST`, and only where the body's media type is the one that
355/// parameter's type can receive: a `json` parameter takes a JSON body, a
356/// `text` one a plain-text body, and so on. A GET has no body to pass.
357fn takes_whole_body(
358    routine: &Routine,
359    is_post: bool,
360    content: &crate::api_request::MediaType,
361) -> bool {
362    use crate::api_request::MediaType;
363
364    if !is_post {
365        return false;
366    }
367    let [param] = routine.params.as_slice() else {
368        return false;
369    };
370    if !param.name.is_empty() {
371        return false;
372    }
373    matches!(
374        (content, param.param_type.as_str()),
375        (MediaType::ApplicationJson, "json" | "jsonb")
376            | (MediaType::TextPlain, "text")
377            | (MediaType::TextXml, "xml")
378            | (MediaType::OctetStream, "bytea")
379    )
380}
381
382/// The type a single unnamed parameter would have, for a body of this type.
383///
384/// `None` where no such parameter could take the body at all, which is every
385/// media type that is neither JSON nor one of the three that carry a single
386/// value.
387fn single_param_type(content: &crate::api_request::MediaType) -> Option<String> {
388    use crate::api_request::MediaType;
389    match content {
390        MediaType::ApplicationJson => Some(crate::error::JSON_PARAM.to_string()),
391        MediaType::TextPlain => Some("text".to_string()),
392        MediaType::TextXml => Some("xml".to_string()),
393        MediaType::OctetStream => Some("bytea".to_string()),
394        _ => None,
395    }
396}
397
398/// Media types whose body is one value rather than a set of named arguments.
399fn is_raw_body_type(content: &crate::api_request::MediaType) -> bool {
400    use crate::api_request::MediaType;
401    matches!(
402        content,
403        MediaType::TextPlain | MediaType::TextXml | MediaType::OctetStream
404    )
405}
406
407/// A function's signature as PostgREST prints it when naming candidates.
408fn signature_of(routine: &Routine) -> String {
409    format!(
410        "{}.{}({})",
411        routine.schema,
412        routine.name,
413        routine
414            .params
415            .iter()
416            .map(|p| format!("{} => {}", p.name, p.param_type))
417            .collect::<Vec<_>>()
418            .join(", ")
419    )
420}
421
422/// Create a database action plan.
423fn create_db_plan(
424    request: &ApiRequest,
425    action: &DbAction,
426    schema_cache: &SchemaCache,
427) -> Result<DbActionPlan> {
428    match action {
429        DbAction::RelationRead { qi, .. } => {
430            let table = schema_cache.require_table(qi)?;
431            let read_plan = ReadPlan::from_request(request, table, schema_cache)?;
432            Ok(DbActionPlan::Read(ReadPlanTree::leaf(read_plan)))
433        }
434
435        DbAction::RelationMut { qi, mutation } => {
436            let table = schema_cache.require_table(qi)?;
437            let mutate_plan = MutatePlan::from_request(request, table, mutation, schema_cache)?;
438
439            let read_plan = if request.preferences.representation.needs_body() {
440                let rp = ReadPlan::for_mutation(request, table, schema_cache)?;
441                Some(ReadPlanTree::leaf(rp))
442            } else {
443                None
444            };
445
446            Ok(DbActionPlan::MutateRead {
447                mutate: mutate_plan,
448                read: read_plan,
449            })
450        }
451
452        DbAction::Routine { qi, invoke_method } => {
453            let supplied = supplied_arguments(request);
454
455            // Over POST the whole body can be one unnamed argument, and the
456            // body's media type decides which type that parameter could be.
457            // The message says which signatures were looked for, so it says
458            // so too.
459            let single_param = match invoke_method {
460                crate::api_request::InvokeMethod::Inv => {
461                    single_param_type(&request.content_media_type)
462                }
463                _ => None,
464            };
465
466            // The name is reported as it was called, arguments and all, so the
467            // client can see which signature was looked for.
468            let not_found = |candidate: Option<String>| Error::FunctionNotFound {
469                name: qi.to_string(),
470                params: supplied.clone(),
471                candidate,
472                single_param: single_param.clone(),
473            };
474
475            // Nothing of that name: the client most likely misspelled it, so
476            // the nearest name in the schema is worth offering.
477            let routines = schema_cache
478                .get_routines(qi)
479                .ok_or_else(|| not_found(schema_cache.similar_routine(qi)))?;
480
481            let routine = select_overload(routines, request)?.ok_or_else(|| {
482                // The name exists but nothing takes these arguments, so the
483                // overload whose parameters most resemble the ones supplied is
484                // the one to offer -- not simply the first, which says nothing
485                // about what the client asked for.
486                not_found(nearest_overload(qi, routines, &supplied))
487            })?;
488
489            let call_plan = CallPlan::from_request(request, routine)?;
490
491            // The rows a function returns are shaped like any other read:
492            // selected, filtered, ordered, paged and embedded on. That needs a
493            // table to resolve columns against, so it applies exactly when the
494            // function returns rows of one this cache knows.
495            //
496            // The table is the one *this* overload returns: a name may carry
497            // several signatures returning different things, and taking the
498            // first would shape the result of one call by the columns of
499            // another.
500            let read = schema_cache
501                .returned_table(routine)
502                .map(|table| ReadPlan::from_request(request, table, schema_cache))
503                .transpose()?
504                .map(ReadPlanTree::leaf);
505
506            Ok(DbActionPlan::Call {
507                call: call_plan,
508                read,
509            })
510        }
511
512        DbAction::SchemaRead { .. } => {
513            // This case is handled in create_action_plan before calling create_db_plan
514            unreachable!("SchemaRead should be handled in create_action_plan")
515        }
516    }
517}
518
519impl crate::api_request::PreferRepresentation {
520    /// Check if response body is needed.
521    pub fn needs_body(&self) -> bool {
522        matches!(self, Self::Full)
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn test_info_plan() {
532        let qi = QualifiedIdentifier::new("public", "users");
533        let plan = ActionPlan::Info(InfoPlan::RelationInfo(qi.clone()));
534
535        match plan {
536            ActionPlan::Info(InfoPlan::RelationInfo(q)) => {
537                assert_eq!(q.name, "users");
538            }
539            _ => panic!("Expected RelationInfo"),
540        }
541    }
542}