Skip to main content

postrust_graphql/resolver/
mutation.rs

1//! Mutation resolvers for GraphQL insert/update/delete operations.
2//!
3//! Converts GraphQL mutation arguments into MutatePlan structures that can be executed.
4
5use crate::input::mutation::InputValue;
6use crate::resolver::query::TableFilter;
7use bytes::Bytes;
8use postrust_core::plan::{CoercibleField, CoercibleLogicTree, MutatePlan};
9use postrust_core::schema_cache::Table;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Arguments for a GraphQL insert mutation.
14#[derive(Debug, Clone, Default)]
15pub struct InsertArgs {
16    /// Objects to insert
17    pub objects: Vec<HashMap<String, InputValue>>,
18    /// On conflict handling
19    pub on_conflict: Option<OnConflictArgs>,
20    /// Fields to return
21    pub returning: Vec<String>,
22}
23
24impl InsertArgs {
25    /// Create new insert args.
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Add an object to insert.
31    pub fn with_object(mut self, object: HashMap<String, InputValue>) -> Self {
32        self.objects.push(object);
33        self
34    }
35
36    /// Add multiple objects to insert.
37    pub fn with_objects(mut self, objects: Vec<HashMap<String, InputValue>>) -> Self {
38        self.objects = objects;
39        self
40    }
41
42    /// Set on conflict handling.
43    pub fn with_on_conflict(mut self, on_conflict: OnConflictArgs) -> Self {
44        self.on_conflict = Some(on_conflict);
45        self
46    }
47
48    /// Set returning fields.
49    pub fn with_returning(mut self, returning: Vec<String>) -> Self {
50        self.returning = returning;
51        self
52    }
53
54    /// Check if there are objects to insert.
55    pub fn has_objects(&self) -> bool {
56        !self.objects.is_empty()
57    }
58
59    /// Get the number of objects to insert.
60    pub fn object_count(&self) -> usize {
61        self.objects.len()
62    }
63
64    /// Convert objects to JSON bytes.
65    pub fn to_json_bytes(&self) -> Option<Bytes> {
66        if self.objects.is_empty() {
67            return None;
68        }
69
70        // Convert InputValue to serde_json::Value
71        let json_objects: Vec<serde_json::Value> = self
72            .objects
73            .iter()
74            .map(|obj| {
75                let map: serde_json::Map<String, serde_json::Value> = obj
76                    .iter()
77                    .map(|(k, v)| (k.clone(), input_value_to_json(v)))
78                    .collect();
79                serde_json::Value::Object(map)
80            })
81            .collect();
82
83        let json = if json_objects.len() == 1 {
84            serde_json::to_vec(&json_objects[0]).ok()
85        } else {
86            serde_json::to_vec(&json_objects).ok()
87        };
88
89        json.map(Bytes::from)
90    }
91
92    /// Get column names from the first object.
93    pub fn column_names(&self) -> Vec<String> {
94        self.objects
95            .first()
96            .map(|obj| obj.keys().cloned().collect())
97            .unwrap_or_default()
98    }
99}
100
101/// Arguments for on conflict handling.
102#[derive(Debug, Clone, Default, Serialize, Deserialize)]
103pub struct OnConflictArgs {
104    /// Constraint columns for conflict detection
105    pub constraint: Vec<String>,
106    /// Update action on conflict
107    pub update_columns: Vec<String>,
108    /// Additional where condition for update
109    pub where_filter: Option<TableFilter>,
110}
111
112impl OnConflictArgs {
113    /// Create new on conflict args.
114    pub fn new(constraint: Vec<String>) -> Self {
115        Self {
116            constraint,
117            update_columns: vec![],
118            where_filter: None,
119        }
120    }
121
122    /// Set columns to update on conflict.
123    pub fn with_update_columns(mut self, columns: Vec<String>) -> Self {
124        self.update_columns = columns;
125        self
126    }
127
128    /// Set where filter for update.
129    pub fn with_where(mut self, filter: TableFilter) -> Self {
130        self.where_filter = Some(filter);
131        self
132    }
133}
134
135/// Arguments for a GraphQL update mutation.
136#[derive(Debug, Clone, Default)]
137pub struct UpdateArgs {
138    /// Filter to select rows to update
139    pub filter: Option<TableFilter>,
140    /// Values to set
141    pub set: HashMap<String, InputValue>,
142    /// Fields to return
143    pub returning: Vec<String>,
144}
145
146impl UpdateArgs {
147    /// Create new update args.
148    pub fn new() -> Self {
149        Self::default()
150    }
151
152    /// Set the filter.
153    pub fn with_filter(mut self, filter: TableFilter) -> Self {
154        self.filter = Some(filter);
155        self
156    }
157
158    /// Set the values to update.
159    pub fn with_set(mut self, set: HashMap<String, InputValue>) -> Self {
160        self.set = set;
161        self
162    }
163
164    /// Set returning fields.
165    pub fn with_returning(mut self, returning: Vec<String>) -> Self {
166        self.returning = returning;
167        self
168    }
169
170    /// Check if filter is specified.
171    pub fn has_filter(&self) -> bool {
172        self.filter.is_some()
173    }
174
175    /// Check if any values are set.
176    pub fn has_set(&self) -> bool {
177        !self.set.is_empty()
178    }
179
180    /// Convert set values to JSON bytes.
181    pub fn to_json_bytes(&self) -> Option<Bytes> {
182        if self.set.is_empty() {
183            return None;
184        }
185
186        let map: serde_json::Map<String, serde_json::Value> = self
187            .set
188            .iter()
189            .map(|(k, v)| (k.clone(), input_value_to_json(v)))
190            .collect();
191
192        serde_json::to_vec(&serde_json::Value::Object(map))
193            .ok()
194            .map(Bytes::from)
195    }
196
197    /// Get column names being updated.
198    pub fn column_names(&self) -> Vec<String> {
199        self.set.keys().cloned().collect()
200    }
201}
202
203/// Arguments for a GraphQL delete mutation.
204#[derive(Debug, Clone, Default)]
205pub struct DeleteArgs {
206    /// Filter to select rows to delete
207    pub filter: Option<TableFilter>,
208    /// Fields to return
209    pub returning: Vec<String>,
210}
211
212impl DeleteArgs {
213    /// Create new delete args.
214    pub fn new() -> Self {
215        Self::default()
216    }
217
218    /// Set the filter.
219    pub fn with_filter(mut self, filter: TableFilter) -> Self {
220        self.filter = Some(filter);
221        self
222    }
223
224    /// Set returning fields.
225    pub fn with_returning(mut self, returning: Vec<String>) -> Self {
226        self.returning = returning;
227        self
228    }
229
230    /// Check if filter is specified.
231    pub fn has_filter(&self) -> bool {
232        self.filter.is_some()
233    }
234}
235
236/// Convert InputValue to serde_json::Value.
237fn input_value_to_json(value: &InputValue) -> serde_json::Value {
238    match value {
239        InputValue::Null => serde_json::Value::Null,
240        InputValue::Bool(b) => serde_json::Value::Bool(*b),
241        InputValue::Int(i) => serde_json::Value::Number((*i).into()),
242        InputValue::Float(f) => serde_json::Number::from_f64(*f)
243            .map(serde_json::Value::Number)
244            .unwrap_or(serde_json::Value::Null),
245        InputValue::String(s) => serde_json::Value::String(s.clone()),
246        InputValue::Object(obj) => {
247            let map: serde_json::Map<String, serde_json::Value> = obj
248                .iter()
249                .map(|(k, v)| (k.clone(), input_value_to_json(v)))
250                .collect();
251            serde_json::Value::Object(map)
252        }
253        InputValue::Array(arr) => {
254            serde_json::Value::Array(arr.iter().map(input_value_to_json).collect())
255        }
256    }
257}
258
259/// Build coercible fields from column names.
260fn build_coercible_fields(columns: &[String], table: &Table) -> Vec<CoercibleField> {
261    columns
262        .iter()
263        .filter_map(|name| {
264            table
265                .columns
266                .get(name)
267                .map(|col| CoercibleField::simple(name, &col.data_type))
268        })
269        .collect()
270}
271
272/// Build where clauses from a TableFilter.
273fn build_where_clauses(filter: &Option<TableFilter>, table: &Table) -> Vec<CoercibleLogicTree> {
274    let Some(filter) = filter else {
275        return vec![];
276    };
277
278    let type_resolver = |name: &str| -> String {
279        table
280            .get_column(name)
281            .map(|c| c.data_type.clone())
282            .unwrap_or_else(|| "text".to_string())
283    };
284
285    filter
286        .to_logic_tree()
287        .map(|tree| vec![CoercibleLogicTree::from_logic_tree(&tree, type_resolver)])
288        .unwrap_or_default()
289}
290
291/// Build an insert MutatePlan from GraphQL arguments.
292pub fn build_insert_plan(args: &InsertArgs, table: &Table) -> MutatePlan {
293    let columns = build_coercible_fields(&args.column_names(), table);
294    let body = args.to_json_bytes();
295    let returning = if args.returning.is_empty() {
296        table.pk_cols.clone()
297    } else {
298        args.returning.clone()
299    };
300
301    let on_conflict = args.on_conflict.as_ref().map(|oc| {
302        (
303            postrust_core::api_request::PreferResolution::MergeDuplicates,
304            oc.constraint.clone(),
305        )
306    });
307
308    MutatePlan::Insert {
309        target: table.qualified_identifier(),
310        columns,
311        body,
312        on_conflict,
313        where_clauses: vec![],
314        returning,
315        pk_cols: table.pk_cols.clone(),
316        apply_defaults: true,
317        reports_inserted: false,
318    }
319}
320
321/// Build an update MutatePlan from GraphQL arguments.
322pub fn build_update_plan(args: &UpdateArgs, table: &Table) -> MutatePlan {
323    let columns = build_coercible_fields(&args.column_names(), table);
324    let body = args.to_json_bytes();
325    let where_clauses = build_where_clauses(&args.filter, table);
326    let returning = if args.returning.is_empty() {
327        table.pk_cols.clone()
328    } else {
329        args.returning.clone()
330    };
331
332    MutatePlan::Update {
333        target: table.qualified_identifier(),
334        columns,
335        body,
336        where_clauses,
337        returning,
338        apply_defaults: false,
339    }
340}
341
342/// Build a delete MutatePlan from GraphQL arguments.
343pub fn build_delete_plan(args: &DeleteArgs, table: &Table) -> MutatePlan {
344    let where_clauses = build_where_clauses(&args.filter, table);
345    let returning = if args.returning.is_empty() {
346        table.pk_cols.clone()
347    } else {
348        args.returning.clone()
349    };
350
351    MutatePlan::Delete {
352        target: table.qualified_identifier(),
353        where_clauses,
354        returning,
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::input::filter::IntFilterInput;
362    use crate::resolver::query::FieldFilter;
363    use indexmap::IndexMap;
364    use postrust_core::schema_cache::Column;
365    use pretty_assertions::assert_eq;
366
367    fn create_test_table() -> Table {
368        let mut columns = IndexMap::new();
369        columns.insert(
370            "id".into(),
371            Column {
372                name: "id".into(),
373                description: None,
374                nullable: false,
375                data_type: "integer".into(),
376                nominal_type: "int4".into(),
377                max_len: None,
378                default: Some("nextval('users_id_seq')".into()),
379                enum_values: vec![],
380                is_pk: true,
381                position: 1,
382                always_generated: false,
383                domain_type: None,
384            },
385        );
386        columns.insert(
387            "name".into(),
388            Column {
389                name: "name".into(),
390                description: None,
391                nullable: false,
392                data_type: "text".into(),
393                nominal_type: "text".into(),
394                max_len: None,
395                default: None,
396                enum_values: vec![],
397                is_pk: false,
398                position: 2,
399                always_generated: false,
400                domain_type: None,
401            },
402        );
403        columns.insert(
404            "email".into(),
405            Column {
406                name: "email".into(),
407                description: None,
408                nullable: true,
409                data_type: "text".into(),
410                nominal_type: "text".into(),
411                max_len: None,
412                default: None,
413                enum_values: vec![],
414                is_pk: false,
415                position: 3,
416                always_generated: false,
417                domain_type: None,
418            },
419        );
420
421        Table {
422            schema: "public".into(),
423            name: "users".into(),
424            description: None,
425            is_view: false,
426            insertable: true,
427            updatable: true,
428            deletable: true,
429            pk_cols: vec!["id".into()],
430            unique_constraints: Vec::new(),
431            columns,
432            computed_columns: Default::default(),
433            is_partitioned: false,
434        }
435    }
436
437    // ============================================================================
438    // InsertArgs Tests
439    // ============================================================================
440
441    #[test]
442    fn test_insert_args_default() {
443        let args = InsertArgs::new();
444        assert!(!args.has_objects());
445        assert_eq!(args.object_count(), 0);
446    }
447
448    #[test]
449    fn test_insert_args_with_object() {
450        let mut object = HashMap::new();
451        object.insert("name".to_string(), InputValue::String("Alice".to_string()));
452        object.insert(
453            "email".to_string(),
454            InputValue::String("alice@example.com".to_string()),
455        );
456
457        let args = InsertArgs::new().with_object(object);
458        assert!(args.has_objects());
459        assert_eq!(args.object_count(), 1);
460    }
461
462    #[test]
463    fn test_insert_args_with_multiple_objects() {
464        let obj1: HashMap<String, InputValue> =
465            [("name".to_string(), InputValue::String("Alice".to_string()))]
466                .into_iter()
467                .collect();
468        let obj2: HashMap<String, InputValue> =
469            [("name".to_string(), InputValue::String("Bob".to_string()))]
470                .into_iter()
471                .collect();
472
473        let args = InsertArgs::new().with_objects(vec![obj1, obj2]);
474        assert_eq!(args.object_count(), 2);
475    }
476
477    #[test]
478    fn test_insert_args_column_names() {
479        let object: HashMap<String, InputValue> = [
480            ("name".to_string(), InputValue::String("Alice".to_string())),
481            (
482                "email".to_string(),
483                InputValue::String("alice@example.com".to_string()),
484            ),
485        ]
486        .into_iter()
487        .collect();
488
489        let args = InsertArgs::new().with_object(object);
490        let columns = args.column_names();
491        assert_eq!(columns.len(), 2);
492        assert!(columns.contains(&"name".to_string()));
493        assert!(columns.contains(&"email".to_string()));
494    }
495
496    #[test]
497    fn test_insert_args_to_json_bytes() {
498        let object: HashMap<String, InputValue> =
499            [("name".to_string(), InputValue::String("Alice".to_string()))]
500                .into_iter()
501                .collect();
502
503        let args = InsertArgs::new().with_object(object);
504        let bytes = args.to_json_bytes().unwrap();
505        let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
506        assert_eq!(json["name"], "Alice");
507    }
508
509    #[test]
510    fn test_insert_args_with_returning() {
511        let args = InsertArgs::new().with_returning(vec!["id".to_string(), "name".to_string()]);
512        assert_eq!(args.returning.len(), 2);
513    }
514
515    #[test]
516    fn test_insert_args_with_on_conflict() {
517        let on_conflict = OnConflictArgs::new(vec!["email".to_string()])
518            .with_update_columns(vec!["name".to_string()]);
519
520        let args = InsertArgs::new().with_on_conflict(on_conflict);
521        assert!(args.on_conflict.is_some());
522    }
523
524    // ============================================================================
525    // OnConflictArgs Tests
526    // ============================================================================
527
528    #[test]
529    fn test_on_conflict_args() {
530        let args = OnConflictArgs::new(vec!["id".to_string()])
531            .with_update_columns(vec!["name".to_string(), "email".to_string()]);
532
533        assert_eq!(args.constraint, vec!["id".to_string()]);
534        assert_eq!(args.update_columns.len(), 2);
535    }
536
537    // ============================================================================
538    // UpdateArgs Tests
539    // ============================================================================
540
541    #[test]
542    fn test_update_args_default() {
543        let args = UpdateArgs::new();
544        assert!(!args.has_filter());
545        assert!(!args.has_set());
546    }
547
548    #[test]
549    fn test_update_args_with_set() {
550        let set: HashMap<String, InputValue> = [(
551            "name".to_string(),
552            InputValue::String("Updated".to_string()),
553        )]
554        .into_iter()
555        .collect();
556
557        let args = UpdateArgs::new().with_set(set);
558        assert!(args.has_set());
559        assert_eq!(args.column_names().len(), 1);
560    }
561
562    #[test]
563    fn test_update_args_with_filter() {
564        let filter = TableFilter::new().with_field(
565            "id",
566            FieldFilter::int(IntFilterInput {
567                eq: Some(1),
568                ..Default::default()
569            }),
570        );
571
572        let args = UpdateArgs::new().with_filter(filter);
573        assert!(args.has_filter());
574    }
575
576    #[test]
577    fn test_update_args_to_json_bytes() {
578        let set: HashMap<String, InputValue> = [
579            (
580                "name".to_string(),
581                InputValue::String("Updated".to_string()),
582            ),
583            ("active".to_string(), InputValue::Bool(true)),
584        ]
585        .into_iter()
586        .collect();
587
588        let args = UpdateArgs::new().with_set(set);
589        let bytes = args.to_json_bytes().unwrap();
590        let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
591        assert_eq!(json["name"], "Updated");
592        assert_eq!(json["active"], true);
593    }
594
595    // ============================================================================
596    // DeleteArgs Tests
597    // ============================================================================
598
599    #[test]
600    fn test_delete_args_default() {
601        let args = DeleteArgs::new();
602        assert!(!args.has_filter());
603    }
604
605    #[test]
606    fn test_delete_args_with_filter() {
607        let filter = TableFilter::new().with_field(
608            "id",
609            FieldFilter::int(IntFilterInput {
610                eq: Some(1),
611                ..Default::default()
612            }),
613        );
614
615        let args = DeleteArgs::new().with_filter(filter);
616        assert!(args.has_filter());
617    }
618
619    #[test]
620    fn test_delete_args_with_returning() {
621        let args = DeleteArgs::new().with_returning(vec!["id".to_string(), "name".to_string()]);
622        assert_eq!(args.returning.len(), 2);
623    }
624
625    // ============================================================================
626    // InputValue to JSON Tests
627    // ============================================================================
628
629    #[test]
630    fn test_input_value_to_json_null() {
631        let json = input_value_to_json(&InputValue::Null);
632        assert!(json.is_null());
633    }
634
635    #[test]
636    fn test_input_value_to_json_bool() {
637        let json = input_value_to_json(&InputValue::Bool(true));
638        assert_eq!(json, serde_json::Value::Bool(true));
639    }
640
641    #[test]
642    fn test_input_value_to_json_int() {
643        let json = input_value_to_json(&InputValue::Int(42));
644        assert_eq!(json, serde_json::json!(42));
645    }
646
647    #[test]
648    fn test_input_value_to_json_float() {
649        let json = input_value_to_json(&InputValue::Float(1.5));
650        assert_eq!(json, serde_json::json!(1.5));
651    }
652
653    #[test]
654    fn test_input_value_to_json_string() {
655        let json = input_value_to_json(&InputValue::String("hello".to_string()));
656        assert_eq!(json, serde_json::json!("hello"));
657    }
658
659    #[test]
660    fn test_input_value_to_json_array() {
661        let arr = vec![InputValue::Int(1), InputValue::Int(2), InputValue::Int(3)];
662        let json = input_value_to_json(&InputValue::Array(arr));
663        assert_eq!(json, serde_json::json!([1, 2, 3]));
664    }
665
666    #[test]
667    fn test_input_value_to_json_object() {
668        let obj: HashMap<String, InputValue> = [
669            ("name".to_string(), InputValue::String("test".to_string())),
670            ("count".to_string(), InputValue::Int(5)),
671        ]
672        .into_iter()
673        .collect();
674        let json = input_value_to_json(&InputValue::Object(obj));
675        assert_eq!(json["name"], "test");
676        assert_eq!(json["count"], 5);
677    }
678
679    // ============================================================================
680    // MutatePlan Building Tests
681    // ============================================================================
682
683    #[test]
684    fn test_build_insert_plan_basic() {
685        let table = create_test_table();
686        let object: HashMap<String, InputValue> =
687            [("name".to_string(), InputValue::String("Alice".to_string()))]
688                .into_iter()
689                .collect();
690
691        let args = InsertArgs::new().with_object(object);
692        let plan = build_insert_plan(&args, &table);
693
694        match plan {
695            MutatePlan::Insert {
696                target,
697                body,
698                returning,
699                ..
700            } => {
701                assert_eq!(target.name, "users");
702                assert!(body.is_some());
703                assert_eq!(returning, vec!["id".to_string()]);
704            }
705            _ => panic!("Expected Insert plan"),
706        }
707    }
708
709    #[test]
710    fn test_build_insert_plan_with_returning() {
711        let table = create_test_table();
712        let object: HashMap<String, InputValue> =
713            [("name".to_string(), InputValue::String("Alice".to_string()))]
714                .into_iter()
715                .collect();
716
717        let args = InsertArgs::new()
718            .with_object(object)
719            .with_returning(vec!["id".to_string(), "name".to_string()]);
720        let plan = build_insert_plan(&args, &table);
721
722        match plan {
723            MutatePlan::Insert { returning, .. } => {
724                assert_eq!(returning.len(), 2);
725            }
726            _ => panic!("Expected Insert plan"),
727        }
728    }
729
730    #[test]
731    fn test_build_insert_plan_with_on_conflict() {
732        let table = create_test_table();
733        let object: HashMap<String, InputValue> =
734            [("name".to_string(), InputValue::String("Alice".to_string()))]
735                .into_iter()
736                .collect();
737
738        let on_conflict = OnConflictArgs::new(vec!["id".to_string()]);
739        let args = InsertArgs::new()
740            .with_object(object)
741            .with_on_conflict(on_conflict);
742        let plan = build_insert_plan(&args, &table);
743
744        match plan {
745            MutatePlan::Insert { on_conflict, .. } => {
746                assert!(on_conflict.is_some());
747                let (_, cols) = on_conflict.unwrap();
748                assert_eq!(cols, vec!["id".to_string()]);
749            }
750            _ => panic!("Expected Insert plan"),
751        }
752    }
753
754    #[test]
755    fn test_build_update_plan_basic() {
756        let table = create_test_table();
757        let set: HashMap<String, InputValue> = [(
758            "name".to_string(),
759            InputValue::String("Updated".to_string()),
760        )]
761        .into_iter()
762        .collect();
763
764        let filter = TableFilter::new().with_field(
765            "id",
766            FieldFilter::int(IntFilterInput {
767                eq: Some(1),
768                ..Default::default()
769            }),
770        );
771
772        let args = UpdateArgs::new().with_set(set).with_filter(filter);
773        let plan = build_update_plan(&args, &table);
774
775        match plan {
776            MutatePlan::Update {
777                target,
778                body,
779                where_clauses,
780                ..
781            } => {
782                assert_eq!(target.name, "users");
783                assert!(body.is_some());
784                assert!(!where_clauses.is_empty());
785            }
786            _ => panic!("Expected Update plan"),
787        }
788    }
789
790    #[test]
791    fn test_build_update_plan_with_returning() {
792        let table = create_test_table();
793        let set: HashMap<String, InputValue> = [(
794            "name".to_string(),
795            InputValue::String("Updated".to_string()),
796        )]
797        .into_iter()
798        .collect();
799
800        let args = UpdateArgs::new()
801            .with_set(set)
802            .with_returning(vec!["id".to_string(), "name".to_string()]);
803        let plan = build_update_plan(&args, &table);
804
805        match plan {
806            MutatePlan::Update { returning, .. } => {
807                assert_eq!(returning.len(), 2);
808            }
809            _ => panic!("Expected Update plan"),
810        }
811    }
812
813    #[test]
814    fn test_build_delete_plan_basic() {
815        let table = create_test_table();
816        let filter = TableFilter::new().with_field(
817            "id",
818            FieldFilter::int(IntFilterInput {
819                eq: Some(1),
820                ..Default::default()
821            }),
822        );
823
824        let args = DeleteArgs::new().with_filter(filter);
825        let plan = build_delete_plan(&args, &table);
826
827        match plan {
828            MutatePlan::Delete {
829                target,
830                where_clauses,
831                returning,
832            } => {
833                assert_eq!(target.name, "users");
834                assert!(!where_clauses.is_empty());
835                assert_eq!(returning, vec!["id".to_string()]);
836            }
837            _ => panic!("Expected Delete plan"),
838        }
839    }
840
841    #[test]
842    fn test_build_delete_plan_with_returning() {
843        let table = create_test_table();
844        let args = DeleteArgs::new().with_returning(vec![
845            "id".to_string(),
846            "name".to_string(),
847            "email".to_string(),
848        ]);
849        let plan = build_delete_plan(&args, &table);
850
851        match plan {
852            MutatePlan::Delete { returning, .. } => {
853                assert_eq!(returning.len(), 3);
854            }
855            _ => panic!("Expected Delete plan"),
856        }
857    }
858
859    #[test]
860    fn test_build_delete_plan_no_filter() {
861        let table = create_test_table();
862        let args = DeleteArgs::new();
863        let plan = build_delete_plan(&args, &table);
864
865        match plan {
866            MutatePlan::Delete { where_clauses, .. } => {
867                assert!(where_clauses.is_empty());
868            }
869            _ => panic!("Expected Delete plan"),
870        }
871    }
872}