Skip to main content

postrust_core/query/
mod.rs

1//! SQL query generation from execution plans.
2//!
3//! This module converts execution plans into parameterized SQL queries.
4
5mod builder;
6
7pub use builder::{QueryBuilder, INSERTED_COLUMN, PARENT_ROW_COLUMN};
8
9use crate::error::Result;
10use crate::plan::{ActionPlan, DbActionPlan};
11use postrust_sql::{SqlFragment, SqlParam};
12
13/// Build SQL from an action plan.
14///
15/// The SQL only: who it runs as is not part of it. The role, the claims and
16/// the rest of what makes a transaction the caller's are a
17/// [`Session`](crate::Session)'s, applied to the transaction before this
18/// runs on it.
19pub fn build_query(plan: &ActionPlan) -> Result<MainQuery> {
20    match plan {
21        ActionPlan::Db(db_plan) => build_db_query(db_plan),
22        ActionPlan::Info(_) => Ok(MainQuery::empty()),
23    }
24}
25
26/// The name the rows a mutation affected are read from.
27pub const MUTATION_RESULT: &str = "pgrst_mutation_result";
28
29/// Build SQL from a database action plan.
30fn build_db_query(plan: &DbActionPlan) -> Result<MainQuery> {
31    let mut query = MainQuery::new();
32
33    match plan {
34        DbActionPlan::Read(read_tree) => {
35            query.main = QueryBuilder::build_read(read_tree)?;
36        }
37        DbActionPlan::MutateRead { mutate, read } => {
38            let mutation = QueryBuilder::build_mutate(mutate)?;
39
40            // The rows a mutation affected are a relation like any other, so
41            // `?select=` is answered by reading from them -- which is also
42            // what lets a mutation embed, alias, cast and compute exactly as a
43            // read does. Without the wrapper the `RETURNING` list was the
44            // whole answer, and none of that reached it.
45            query.main = match read {
46                Some(read_tree) => {
47                    let read_sql = QueryBuilder::build_read(read_tree)?;
48                    let mut frag = SqlFragment::new();
49                    frag.push("WITH ");
50                    frag.push(MUTATION_RESULT);
51                    frag.push(" AS (");
52                    frag.append(mutation);
53                    frag.push(") ");
54                    frag.append(read_sql);
55                    frag
56                }
57                None => mutation,
58            };
59        }
60        DbActionPlan::Call { call, read } => {
61            query.main = QueryBuilder::build_call(call, read.as_ref())?;
62        }
63    }
64
65    Ok(query)
66}
67
68/// A complete query with setup and main statement.
69#[derive(Clone, Debug, Default)]
70pub struct MainQuery {
71    /// Main query
72    pub main: SqlFragment,
73    /// Read query (for mutations with RETURNING)
74    pub read: Option<SqlFragment>,
75    /// Count query (for pagination)
76    pub count: Option<SqlFragment>,
77}
78
79impl MainQuery {
80    /// Create a new empty query.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Create an empty query (for info-only plans).
86    pub fn empty() -> Self {
87        Self::default()
88    }
89
90    /// Check if this query has a main statement.
91    pub fn has_main(&self) -> bool {
92        !self.main.is_empty()
93    }
94
95    /// Get the main SQL and parameters.
96    pub fn build_main(self) -> (String, Vec<SqlParam>) {
97        self.main.build()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::api_request::{QualifiedIdentifier, Range};
105    use crate::plan::{CoercibleField, CoercibleSelectField, MutatePlan, ReadPlan, ReadPlanTree};
106
107    /// The whole statement, parentheses included.
108    ///
109    /// A missing one here is a syntax error on every write with a body, and
110    /// nothing short of executing it against PostgreSQL -- or reading it --
111    /// will say so, since the fragments that build it are each well-formed.
112    #[test]
113    fn a_write_reads_its_body_and_is_read_from() {
114        let mutate = MutatePlan::Insert {
115            target: QualifiedIdentifier::new("test", "items"),
116            columns: vec![CoercibleField::simple("id", "int8")],
117            body: Some(bytes::Bytes::from(r#"{"id":9001}"#)),
118            on_conflict: None,
119            where_clauses: vec![],
120            returning: vec!["id".into()],
121            pk_cols: vec!["id".into()],
122            apply_defaults: true,
123            reports_inserted: false,
124        };
125        let read = ReadPlan {
126            select: vec![CoercibleSelectField::simple("id", "bigint")],
127            from: QualifiedIdentifier::unqualified(MUTATION_RESULT),
128            from_alias: None,
129            where_clauses: vec![],
130            order: vec![],
131            range: Range::default(),
132            rel_name: String::new(),
133            rel_to_parent: None,
134            rel_join_conds: vec![],
135            rel_join_type: None,
136            rel_select: vec![],
137            depth: 0,
138        };
139
140        let plan = ActionPlan::Db(DbActionPlan::MutateRead {
141            mutate,
142            read: Some(ReadPlanTree::leaf(read)),
143        });
144        let (sql, _) = build_query(&plan).unwrap().build_main();
145
146        assert_eq!(
147            sql,
148            "WITH pgrst_mutation_result AS (\
149             INSERT INTO \"test\".\"items\" (\"id\") \
150             SELECT pgrst_body.\"id\" \
151             FROM (SELECT $1::json AS json_data) pgrst_payload, \
152             LATERAL (SELECT \"id\" FROM json_to_record(pgrst_payload.json_data) \
153             AS _(\"id\" int8)) pgrst_body \
154             RETURNING \"items\".\"id\") \
155             SELECT \"id\" FROM \"pgrst_mutation_result\""
156        );
157    }
158}