Skip to main content

postrust_core/plan/
mutate_plan.rs

1//! Mutation (INSERT/UPDATE/DELETE) query planning.
2
3use super::types::*;
4use crate::api_request::{ApiRequest, Mutation, Payload, PreferResolution, QualifiedIdentifier};
5use crate::error::{Error, Result};
6use crate::schema_cache::Table;
7use serde::{Deserialize, Serialize};
8
9/// A mutation plan.
10#[derive(Clone, Debug, Serialize, Deserialize)]
11pub enum MutatePlan {
12    /// INSERT operation
13    Insert {
14        /// Target table
15        target: QualifiedIdentifier,
16        /// Columns to insert
17        columns: Vec<CoercibleField>,
18        /// Request body (JSON)
19        body: Option<bytes::Bytes>,
20        /// ON CONFLICT handling
21        on_conflict: Option<(PreferResolution, Vec<String>)>,
22        /// WHERE clause (for filtered inserts)
23        where_clauses: Vec<CoercibleLogicTree>,
24        /// RETURNING columns
25        returning: Vec<String>,
26        /// Primary key columns
27        pk_cols: Vec<String>,
28        /// Apply defaults for missing columns
29        apply_defaults: bool,
30        /// Whether the statement can report which rows it created.
31        ///
32        /// It asks PostgreSQL for `xmax`, which a plain table will give back
33        /// and a view or a partitioned table will not.
34        #[serde(default)]
35        reports_inserted: bool,
36    },
37    /// UPDATE operation
38    Update {
39        /// Target table
40        target: QualifiedIdentifier,
41        /// Columns to update
42        columns: Vec<CoercibleField>,
43        /// Request body (JSON)
44        body: Option<bytes::Bytes>,
45        /// WHERE clauses
46        where_clauses: Vec<CoercibleLogicTree>,
47        /// RETURNING columns
48        returning: Vec<String>,
49        /// Apply defaults for NULL columns
50        apply_defaults: bool,
51    },
52    /// DELETE operation
53    Delete {
54        /// Target table
55        target: QualifiedIdentifier,
56        /// WHERE clauses
57        where_clauses: Vec<CoercibleLogicTree>,
58        /// RETURNING columns
59        returning: Vec<String>,
60    },
61}
62
63impl MutatePlan {
64    /// Create a mutation plan from an API request.
65    pub fn from_request(
66        request: &ApiRequest,
67        table: &Table,
68        mutation: &Mutation,
69        schema_cache: &crate::schema_cache::SchemaCache,
70    ) -> Result<Self> {
71        let qi = table.qualified_identifier();
72
73        match mutation {
74            Mutation::Create => Self::create_insert(request, table, qi, schema_cache),
75            Mutation::Update => Self::create_update(request, table, qi, schema_cache),
76            Mutation::Delete => Self::create_delete(request, table, qi),
77            Mutation::SingleUpsert => Self::create_upsert(request, table, qi, schema_cache),
78        }
79    }
80
81    /// Create an INSERT plan.
82    fn create_insert(
83        request: &ApiRequest,
84        table: &Table,
85        qi: QualifiedIdentifier,
86        schema_cache: &crate::schema_cache::SchemaCache,
87    ) -> Result<Self> {
88        let columns = get_payload_columns(request, table, schema_cache)?;
89        let body = get_body_bytes(request)?;
90        let returning = get_returning_columns(request, table, true);
91        let apply_defaults = asked_for_defaults(request);
92
93        // `Prefer: resolution=` says what to do about a duplicate; `on_conflict=`
94        // says which columns make one. Without the latter the answer is the
95        // primary key, which is what "duplicate" means when nothing else is
96        // said -- and without a resolution there is nothing to do about one.
97        let on_conflict = request
98            .preferences
99            .resolution
100            .clone()
101            .and_then(|resolution| {
102                let columns = request
103                    .query_params
104                    .on_conflict
105                    .clone()
106                    .unwrap_or_else(|| table.pk_cols.clone());
107                match columns.is_empty() {
108                    true => None,
109                    false => Some((resolution, columns)),
110                }
111            });
112
113        Ok(Self::Insert {
114            target: qi,
115            columns,
116            body,
117            on_conflict,
118            where_clauses: vec![],
119            returning,
120            pk_cols: table.pk_cols.clone(),
121            apply_defaults,
122            reports_inserted: !table.is_view && !table.is_partitioned,
123        })
124    }
125
126    /// Create an UPDATE plan.
127    fn create_update(
128        request: &ApiRequest,
129        table: &Table,
130        qi: QualifiedIdentifier,
131        schema_cache: &crate::schema_cache::SchemaCache,
132    ) -> Result<Self> {
133        let columns = get_payload_columns(request, table, schema_cache)?;
134        let body = get_body_bytes(request)?;
135        let where_clauses = build_mutation_where(request, table)?;
136        let returning = get_returning_columns(request, table, false);
137        let apply_defaults = asked_for_defaults(request);
138
139        Ok(Self::Update {
140            target: qi,
141            columns,
142            body,
143            where_clauses,
144            returning,
145            apply_defaults,
146        })
147    }
148
149    /// Create a DELETE plan.
150    fn create_delete(request: &ApiRequest, table: &Table, qi: QualifiedIdentifier) -> Result<Self> {
151        let where_clauses = build_mutation_where(request, table)?;
152        let returning = get_returning_columns(request, table, false);
153
154        Ok(Self::Delete {
155            target: qi,
156            where_clauses,
157            returning,
158        })
159    }
160
161    /// Create a PUT (upsert) plan.
162    fn create_upsert(
163        request: &ApiRequest,
164        table: &Table,
165        qi: QualifiedIdentifier,
166        schema_cache: &crate::schema_cache::SchemaCache,
167    ) -> Result<Self> {
168        validate_put(request, table)?;
169
170        let columns = get_payload_columns(request, table, schema_cache)?;
171        let body = get_body_bytes(request)?;
172        let returning = get_returning_columns(request, table, true);
173
174        // Upsert uses PK for conflict
175        let on_conflict = Some((PreferResolution::MergeDuplicates, table.pk_cols.clone()));
176
177        // The URL's filters narrow the *body*, not the table: a `PUT` writes
178        // the row the URL names, so a body row naming a different one is left
179        // unwritten rather than rejected outright. Whether exactly one row
180        // came of it is checked afterwards.
181        let where_clauses = build_mutation_where(request, table)?;
182
183        Ok(Self::Insert {
184            target: qi,
185            columns,
186            body,
187            on_conflict,
188            where_clauses,
189            returning,
190            pk_cols: table.pk_cols.clone(),
191            apply_defaults: asked_for_defaults(request),
192            reports_inserted: !table.is_view && !table.is_partitioned,
193        })
194    }
195
196    /// Get the target table.
197    pub fn target(&self) -> &QualifiedIdentifier {
198        match self {
199            Self::Insert { target, .. } => target,
200            Self::Update { target, .. } => target,
201            Self::Delete { target, .. } => target,
202        }
203    }
204
205    /// Check if this mutation has a body.
206    pub fn has_body(&self) -> bool {
207        match self {
208            Self::Insert { body, .. } => body.is_some(),
209            Self::Update { body, .. } => body.is_some(),
210            Self::Delete { .. } => false,
211        }
212    }
213}
214
215/// Get columns from payload.
216fn get_payload_columns(
217    request: &ApiRequest,
218    table: &Table,
219    schema_cache: &crate::schema_cache::SchemaCache,
220) -> Result<Vec<CoercibleField>> {
221    let keys = match &request.payload {
222        Some(Payload::ProcessedJson { keys, .. }) => keys,
223        Some(Payload::ProcessedUrlEncoded { keys, .. }) => keys,
224        _ => return Ok(vec![]),
225    };
226
227    // `?columns=` names the columns to write and fixes their order, which is
228    // what makes a bulk insert of ragged rows well-defined. Without it the
229    // body's own keys are the columns.
230    let names: Vec<&String> = match &request.query_params.columns {
231        Some(columns) => columns.iter().collect(),
232        None => keys.iter().collect(),
233    };
234
235    let mut columns = Vec::new();
236
237    for key in names {
238        let column = table.get_column(key).ok_or_else(|| Error::UnknownColumn {
239            column: key.clone(),
240            relation: table.name.clone(),
241        })?;
242
243        let mut field = CoercibleField::simple(key, &column.nominal_type);
244        field.default = column.default.clone();
245        // A schema that declared how one of its domains is written in JSON
246        // also declared how one arrives: the value is read out of the body as
247        // JSON and handed to that cast, rather than to PostgreSQL's own input
248        // function for the type underneath.
249        if let Some(function) = schema_cache.representation("json", column.representation_type()) {
250            field.ir_type = "json".to_string();
251            field.transform = Some(function.to_string());
252        }
253        columns.push(field);
254    }
255
256    Ok(columns)
257}
258
259/// Whether the request asked for a column it left out to take its default.
260///
261/// Read from what was actually sent rather than from the parsed field, which
262/// cannot say whether `missing=default` was requested or is merely the value
263/// the field holds when nothing was.
264fn asked_for_defaults(request: &ApiRequest) -> bool {
265    request
266        .preferences
267        .applied
268        .iter()
269        .any(|pref| pref == "missing=default")
270}
271
272/// Check that a `PUT` names exactly one row, and that its body agrees.
273///
274/// `PUT` replaces the row the URL identifies, so the URL has to identify one:
275/// every primary key column, compared with `eq`, and nothing else. A page
276/// makes no sense on top of that, and a body naming a different row would
277/// write somewhere the client did not ask for.
278fn validate_put(request: &ApiRequest, table: &Table) -> Result<()> {
279    use crate::api_request::{Operation, QuantOperator};
280
281    if request
282        .query_params
283        .ranges
284        .values()
285        .any(|range| range.limit.is_some() || range.offset != 0)
286    {
287        return Err(Error::PutLimitNotAllowed);
288    }
289
290    let mut keyed: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
291    for filter in &request.query_params.filters_root {
292        let Operation::Quant {
293            op: QuantOperator::Equal,
294            quantifier: None,
295            value,
296        } = &filter.op_expr.operation
297        else {
298            return Err(Error::InvalidPutFilters);
299        };
300        if filter.op_expr.negated {
301            return Err(Error::InvalidPutFilters);
302        }
303        keyed.insert(&filter.field.name, value);
304    }
305
306    // Anything else in the query string that filters -- a logic group, a
307    // filter on an embedded resource -- also fails to name exactly one row.
308    if !request.query_params.logic.is_empty() || !request.query_params.filters.is_empty() {
309        return Err(Error::InvalidPutFilters);
310    }
311
312    if table.pk_cols.is_empty()
313        || keyed.len() != table.pk_cols.len()
314        || !table
315            .pk_cols
316            .iter()
317            .all(|key| keyed.contains_key(key.as_str()))
318    {
319        return Err(Error::InvalidPutFilters);
320    }
321
322    // Whether the body agrees with the URL is not decided here: the filters
323    // are applied to the body itself, so a row naming another key is simply
324    // not written, and the count that comes back says whether exactly one was.
325
326    Ok(())
327}
328
329/// Get body as bytes.
330fn get_body_bytes(request: &ApiRequest) -> Result<Option<bytes::Bytes>> {
331    match &request.payload {
332        Some(Payload::ProcessedJson { raw, .. }) => Ok(Some(raw.clone())),
333        Some(Payload::RawJson(raw)) => Ok(Some(raw.clone())),
334        Some(Payload::RawPayload(raw)) => Ok(Some(raw.clone())),
335        Some(Payload::ProcessedUrlEncoded { data, .. }) => {
336            // Convert to JSON
337            let json = serde_json::to_vec(
338                &data
339                    .iter()
340                    .cloned()
341                    .collect::<std::collections::HashMap<_, _>>(),
342            )
343            .map_err(|e| Error::InvalidBody(e.to_string()))?;
344            Ok(Some(bytes::Bytes::from(json)))
345        }
346        None => Ok(None),
347    }
348}
349
350/// The columns a mutation has to return.
351///
352/// `RETURNING` runs with the caller's privileges, so asking for a column the
353/// role may write but not read fails the whole statement. A role granted
354/// `INSERT` on an audit table and nothing else is an ordinary arrangement, and
355/// returning every column made it impossible to insert into one.
356///
357/// So: only what the response will actually read. That is the selected
358/// columns, whatever the result is ordered by, and the key a `Location` header
359/// is built from. A caller wanting no representation reads nothing, and the
360/// list is then empty.
361///
362/// The exceptions return the whole row, because they need it: `*` says so, and
363/// a computed field is a function of the row rather than of any column.
364fn get_returning_columns(request: &ApiRequest, table: &Table, creates: bool) -> Vec<String> {
365    use crate::api_request::SelectItem;
366
367    let everything = || -> Vec<String> { table.column_names().map(str::to_string).collect() };
368
369    let mut wanted: Vec<String> = Vec::new();
370
371    // `headers-only` reads no columns but still needs the key.
372    if request.preferences.representation.needs_body() {
373        if request.query_params.select.is_empty() {
374            return everything();
375        }
376        for item in &request.query_params.select {
377            match item {
378                SelectItem::Field { field, .. } => {
379                    if field.name == "*"
380                        || table
381                            .get_computed_column(&field.name)
382                            .is_some_and(|c| !c.takes_arguments)
383                    {
384                        return everything();
385                    }
386                    wanted.push(field.name.clone());
387                }
388                // An embed joins on a column of this row, and that column was
389                // added to the selection before planning began.
390                SelectItem::Relation { .. } | SelectItem::SpreadRelation { .. } => {}
391            }
392        }
393        // Ordering the returned rows by a column means reading that column,
394        // whether or not the client asked to see it. A term naming an
395        // embedded resource orders by something this statement cannot return
396        // at all.
397        for (path, terms) in &request.query_params.order {
398            if !path.is_empty() {
399                continue;
400            }
401            for term in terms {
402                if let crate::api_request::OrderTerm::Field { field, .. } = term {
403                    wanted.push(field.name.clone());
404                }
405            }
406        }
407    }
408
409    // The created row's own address is built from its key, whether or not the
410    // client asked to see it.
411    if creates {
412        wanted.extend(table.pk_cols.iter().cloned());
413    }
414
415    // A name that is not a column of this table cannot be returned from it;
416    // where it is anything real -- a JSON path, an alias -- the read resolves
417    // it against what did come back.
418    wanted.retain(|name| table.get_column(name).is_some());
419    wanted.sort();
420    wanted.dedup();
421    wanted
422}
423
424/// Build WHERE clauses for mutations.
425fn build_mutation_where(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleLogicTree>> {
426    let type_resolver = |name: &str| -> String {
427        table
428            .get_column(name)
429            .map(|c| c.data_type.clone())
430            .unwrap_or_else(|| "text".to_string())
431    };
432
433    let mut clauses = Vec::new();
434
435    for filter in &request.query_params.filters_root {
436        let pg_type = type_resolver(&filter.field.name);
437        clauses.push(CoercibleLogicTree::Stmt(CoercibleFilter::from_filter(
438            filter, &pg_type,
439        )));
440    }
441
442    // `?or=(...)` names rows exactly as a plain filter does, and a mutation
443    // that reads only the plain ones has no `WHERE` at all -- so
444    // `DELETE /entities?or=(id.eq.1,id.eq.2)` deleted every row in the table.
445    // A request that names two rows must never touch a third.
446    for (path, tree) in &request.query_params.logic {
447        if path.is_empty() {
448            clauses.push(CoercibleLogicTree::from_logic_tree(tree, type_resolver));
449        }
450    }
451
452    Ok(clauses)
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    #[test]
460    fn test_mutate_plan_target() {
461        let qi = QualifiedIdentifier::new("public", "users");
462        let plan = MutatePlan::Delete {
463            target: qi.clone(),
464            where_clauses: vec![],
465            returning: vec!["id".into()],
466        };
467
468        assert_eq!(plan.target().name, "users");
469    }
470
471    #[test]
472    fn test_mutate_plan_has_body() {
473        let qi = QualifiedIdentifier::new("public", "users");
474
475        let insert = MutatePlan::Insert {
476            target: qi.clone(),
477            columns: vec![],
478            body: Some(bytes::Bytes::from("{}".as_bytes())),
479            on_conflict: None,
480            where_clauses: vec![],
481            returning: vec![],
482            pk_cols: vec![],
483            apply_defaults: true,
484            reports_inserted: false,
485        };
486        assert!(insert.has_body());
487
488        let delete = MutatePlan::Delete {
489            target: qi,
490            where_clauses: vec![],
491            returning: vec![],
492        };
493        assert!(!delete.has_body());
494    }
495}