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    },
31    /// UPDATE operation
32    Update {
33        /// Target table
34        target: QualifiedIdentifier,
35        /// Columns to update
36        columns: Vec<CoercibleField>,
37        /// Request body (JSON)
38        body: Option<bytes::Bytes>,
39        /// WHERE clauses
40        where_clauses: Vec<CoercibleLogicTree>,
41        /// RETURNING columns
42        returning: Vec<String>,
43        /// Apply defaults for NULL columns
44        apply_defaults: bool,
45    },
46    /// DELETE operation
47    Delete {
48        /// Target table
49        target: QualifiedIdentifier,
50        /// WHERE clauses
51        where_clauses: Vec<CoercibleLogicTree>,
52        /// RETURNING columns
53        returning: Vec<String>,
54    },
55}
56
57impl MutatePlan {
58    /// Create a mutation plan from an API request.
59    pub fn from_request(request: &ApiRequest, table: &Table, mutation: &Mutation) -> Result<Self> {
60        let qi = table.qualified_identifier();
61
62        match mutation {
63            Mutation::Create => Self::create_insert(request, table, qi),
64            Mutation::Update => Self::create_update(request, table, qi),
65            Mutation::Delete => Self::create_delete(request, table, qi),
66            Mutation::SingleUpsert => Self::create_upsert(request, table, qi),
67        }
68    }
69
70    /// Create an INSERT plan.
71    fn create_insert(request: &ApiRequest, table: &Table, qi: QualifiedIdentifier) -> Result<Self> {
72        let columns = get_payload_columns(request, table)?;
73        let body = get_body_bytes(request)?;
74        let returning = get_returning_columns(request, table);
75        let apply_defaults =
76            request.preferences.missing == crate::api_request::PreferMissing::ApplyDefaults;
77
78        let on_conflict = request.query_params.on_conflict.as_ref().map(|cols| {
79            let resolution = request
80                .preferences
81                .resolution
82                .clone()
83                .unwrap_or(PreferResolution::MergeDuplicates);
84            (resolution, cols.clone())
85        });
86
87        Ok(Self::Insert {
88            target: qi,
89            columns,
90            body,
91            on_conflict,
92            where_clauses: vec![],
93            returning,
94            pk_cols: table.pk_cols.clone(),
95            apply_defaults,
96        })
97    }
98
99    /// Create an UPDATE plan.
100    fn create_update(request: &ApiRequest, table: &Table, qi: QualifiedIdentifier) -> Result<Self> {
101        let columns = get_payload_columns(request, table)?;
102        let body = get_body_bytes(request)?;
103        let where_clauses = build_mutation_where(request, table)?;
104        let returning = get_returning_columns(request, table);
105        let apply_defaults =
106            request.preferences.missing == crate::api_request::PreferMissing::ApplyDefaults;
107
108        Ok(Self::Update {
109            target: qi,
110            columns,
111            body,
112            where_clauses,
113            returning,
114            apply_defaults,
115        })
116    }
117
118    /// Create a DELETE plan.
119    fn create_delete(request: &ApiRequest, table: &Table, qi: QualifiedIdentifier) -> Result<Self> {
120        let where_clauses = build_mutation_where(request, table)?;
121        let returning = get_returning_columns(request, table);
122
123        Ok(Self::Delete {
124            target: qi,
125            where_clauses,
126            returning,
127        })
128    }
129
130    /// Create a PUT (upsert) plan.
131    fn create_upsert(request: &ApiRequest, table: &Table, qi: QualifiedIdentifier) -> Result<Self> {
132        let columns = get_payload_columns(request, table)?;
133        let body = get_body_bytes(request)?;
134        let returning = get_returning_columns(request, table);
135
136        // Upsert uses PK for conflict
137        let on_conflict = Some((PreferResolution::MergeDuplicates, table.pk_cols.clone()));
138
139        Ok(Self::Insert {
140            target: qi,
141            columns,
142            body,
143            on_conflict,
144            where_clauses: vec![],
145            returning,
146            pk_cols: table.pk_cols.clone(),
147            apply_defaults: true,
148        })
149    }
150
151    /// Get the target table.
152    pub fn target(&self) -> &QualifiedIdentifier {
153        match self {
154            Self::Insert { target, .. } => target,
155            Self::Update { target, .. } => target,
156            Self::Delete { target, .. } => target,
157        }
158    }
159
160    /// Check if this mutation has a body.
161    pub fn has_body(&self) -> bool {
162        match self {
163            Self::Insert { body, .. } => body.is_some(),
164            Self::Update { body, .. } => body.is_some(),
165            Self::Delete { .. } => false,
166        }
167    }
168}
169
170/// Get columns from payload.
171fn get_payload_columns(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleField>> {
172    let keys = match &request.payload {
173        Some(Payload::ProcessedJson { keys, .. }) => keys,
174        Some(Payload::ProcessedUrlEncoded { keys, .. }) => keys,
175        _ => return Ok(vec![]),
176    };
177
178    let mut columns = Vec::new();
179
180    for key in keys {
181        let column = table
182            .get_column(key)
183            .ok_or_else(|| Error::UnknownColumn(key.clone()))?;
184
185        columns.push(CoercibleField::simple(key, &column.data_type));
186    }
187
188    Ok(columns)
189}
190
191/// Get body as bytes.
192fn get_body_bytes(request: &ApiRequest) -> Result<Option<bytes::Bytes>> {
193    match &request.payload {
194        Some(Payload::ProcessedJson { raw, .. }) => Ok(Some(raw.clone())),
195        Some(Payload::RawJson(raw)) => Ok(Some(raw.clone())),
196        Some(Payload::RawPayload(raw)) => Ok(Some(raw.clone())),
197        Some(Payload::ProcessedUrlEncoded { data, .. }) => {
198            // Convert to JSON
199            let json = serde_json::to_vec(
200                &data
201                    .iter()
202                    .cloned()
203                    .collect::<std::collections::HashMap<_, _>>(),
204            )
205            .map_err(|e| Error::InvalidBody(e.to_string()))?;
206            Ok(Some(bytes::Bytes::from(json)))
207        }
208        None => Ok(None),
209    }
210}
211
212/// Get returning columns.
213fn get_returning_columns(request: &ApiRequest, table: &Table) -> Vec<String> {
214    if request.preferences.representation.needs_body() {
215        table.column_names().map(|s| s.to_string()).collect()
216    } else {
217        // Always return PK for Location header
218        table.pk_cols.clone()
219    }
220}
221
222/// Build WHERE clauses for mutations.
223fn build_mutation_where(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleLogicTree>> {
224    let type_resolver = |name: &str| -> String {
225        table
226            .get_column(name)
227            .map(|c| c.data_type.clone())
228            .unwrap_or_else(|| "text".to_string())
229    };
230
231    let mut clauses = Vec::new();
232
233    for filter in &request.query_params.filters_root {
234        let pg_type = type_resolver(&filter.field.name);
235        clauses.push(CoercibleLogicTree::Stmt(CoercibleFilter::from_filter(
236            filter, &pg_type,
237        )));
238    }
239
240    Ok(clauses)
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_mutate_plan_target() {
249        let qi = QualifiedIdentifier::new("public", "users");
250        let plan = MutatePlan::Delete {
251            target: qi.clone(),
252            where_clauses: vec![],
253            returning: vec!["id".into()],
254        };
255
256        assert_eq!(plan.target().name, "users");
257    }
258
259    #[test]
260    fn test_mutate_plan_has_body() {
261        let qi = QualifiedIdentifier::new("public", "users");
262
263        let insert = MutatePlan::Insert {
264            target: qi.clone(),
265            columns: vec![],
266            body: Some(bytes::Bytes::from("{}".as_bytes())),
267            on_conflict: None,
268            where_clauses: vec![],
269            returning: vec![],
270            pk_cols: vec![],
271            apply_defaults: true,
272        };
273        assert!(insert.has_body());
274
275        let delete = MutatePlan::Delete {
276            target: qi,
277            where_clauses: vec![],
278            returning: vec![],
279        };
280        assert!(!delete.has_body());
281    }
282}