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