Skip to main content

dbrest_core/plan/
mutate_plan.rs

1//! MutatePlan types for dbrest
2//!
3//! Defines the plan types for INSERT, UPDATE, DELETE, and UPSERT operations.
4//! Matches the Haskell `MutatePlan` data type.
5
6use crate::api_request::types::{FieldName, Payload};
7use crate::types::identifiers::QualifiedIdentifier;
8
9use super::types::*;
10
11// ==========================================================================
12// MutatePlan -- top-level mutation plan
13// ==========================================================================
14
15/// A mutation plan
16///
17/// Matches the Haskell `MutatePlan` data type with three variants.
18#[derive(Debug, Clone)]
19pub enum MutatePlan {
20    /// INSERT plan
21    Insert(InsertPlan),
22    /// UPDATE plan
23    Update(UpdatePlan),
24    /// DELETE plan
25    Delete(DeletePlan),
26}
27
28impl MutatePlan {
29    /// Get the target table identifier
30    pub fn qi(&self) -> &QualifiedIdentifier {
31        match self {
32            MutatePlan::Insert(p) => &p.into,
33            MutatePlan::Update(p) => &p.into,
34            MutatePlan::Delete(p) => &p.from,
35        }
36    }
37
38    /// Get the RETURNING columns
39    pub fn returning(&self) -> &[CoercibleSelectField] {
40        match self {
41            MutatePlan::Insert(p) => &p.returning,
42            MutatePlan::Update(p) => &p.returning,
43            MutatePlan::Delete(p) => &p.returning,
44        }
45    }
46}
47
48// ==========================================================================
49// Insert / Update / Delete plans
50// ==========================================================================
51
52/// INSERT plan details
53#[derive(Debug, Clone)]
54pub struct InsertPlan {
55    /// Target table
56    pub into: QualifiedIdentifier,
57    /// Typed columns to insert
58    pub columns: Vec<CoercibleField>,
59    /// Request body
60    pub body: Payload,
61    /// ON CONFLICT handling
62    pub on_conflict: Option<OnConflict>,
63    /// WHERE clause for conditional insert
64    pub where_: Vec<CoercibleLogicTree>,
65    /// RETURNING columns
66    pub returning: Vec<CoercibleSelectField>,
67    /// Primary key columns
68    pub pk_cols: Vec<FieldName>,
69    /// Whether to apply column defaults
70    pub apply_defaults: bool,
71}
72
73/// UPDATE plan details
74#[derive(Debug, Clone)]
75pub struct UpdatePlan {
76    /// Target table
77    pub into: QualifiedIdentifier,
78    /// Typed columns to update
79    pub columns: Vec<CoercibleField>,
80    /// Request body
81    pub body: Payload,
82    /// WHERE clause for filtering rows to update
83    pub where_: Vec<CoercibleLogicTree>,
84    /// RETURNING columns
85    pub returning: Vec<CoercibleSelectField>,
86    /// Whether to apply column defaults
87    pub apply_defaults: bool,
88}
89
90/// DELETE plan details
91#[derive(Debug, Clone)]
92pub struct DeletePlan {
93    /// Target table
94    pub from: QualifiedIdentifier,
95    /// WHERE clause for filtering rows to delete
96    pub where_: Vec<CoercibleLogicTree>,
97    /// RETURNING columns
98    pub returning: Vec<CoercibleSelectField>,
99}
100
101/// ON CONFLICT clause for upsert
102#[derive(Debug, Clone)]
103pub struct OnConflict {
104    /// Conflict target columns
105    pub columns: Vec<FieldName>,
106    /// Whether to merge duplicates (DO UPDATE) vs ignore (DO NOTHING)
107    pub merge_duplicates: bool,
108}
109
110// ==========================================================================
111// Tests
112// ==========================================================================
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn test_qi(name: &str) -> QualifiedIdentifier {
119        QualifiedIdentifier::new("public", name)
120    }
121
122    #[test]
123    fn test_mutate_plan_qi() {
124        let insert = MutatePlan::Insert(InsertPlan {
125            into: test_qi("users"),
126            columns: vec![],
127            body: Payload::RawJSON(bytes::Bytes::new()),
128            on_conflict: None,
129            where_: vec![],
130            returning: vec![],
131            pk_cols: vec![],
132            apply_defaults: false,
133        });
134        assert_eq!(insert.qi().name.as_str(), "users");
135
136        let update = MutatePlan::Update(UpdatePlan {
137            into: test_qi("posts"),
138            columns: vec![],
139            body: Payload::RawJSON(bytes::Bytes::new()),
140            where_: vec![],
141            returning: vec![],
142            apply_defaults: false,
143        });
144        assert_eq!(update.qi().name.as_str(), "posts");
145
146        let delete = MutatePlan::Delete(DeletePlan {
147            from: test_qi("comments"),
148            where_: vec![],
149            returning: vec![],
150        });
151        assert_eq!(delete.qi().name.as_str(), "comments");
152    }
153
154    #[test]
155    fn test_on_conflict() {
156        let oc = OnConflict {
157            columns: vec!["id".into()],
158            merge_duplicates: true,
159        };
160        assert!(oc.merge_duplicates);
161        assert_eq!(oc.columns.len(), 1);
162    }
163
164    #[test]
165    fn test_insert_plan_with_on_conflict() {
166        let plan = InsertPlan {
167            into: test_qi("users"),
168            columns: vec![],
169            body: Payload::RawJSON(bytes::Bytes::from("{}")),
170            on_conflict: Some(OnConflict {
171                columns: vec!["id".into()],
172                merge_duplicates: true,
173            }),
174            where_: vec![],
175            returning: vec![],
176            pk_cols: vec!["id".into()],
177            apply_defaults: true,
178        };
179        assert!(plan.on_conflict.is_some());
180        assert!(plan.apply_defaults);
181        assert_eq!(plan.pk_cols.len(), 1);
182    }
183}