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    }
318}
319
320/// Build an update MutatePlan from GraphQL arguments.
321pub fn build_update_plan(args: &UpdateArgs, table: &Table) -> MutatePlan {
322    let columns = build_coercible_fields(&args.column_names(), table);
323    let body = args.to_json_bytes();
324    let where_clauses = build_where_clauses(&args.filter, table);
325    let returning = if args.returning.is_empty() {
326        table.pk_cols.clone()
327    } else {
328        args.returning.clone()
329    };
330
331    MutatePlan::Update {
332        target: table.qualified_identifier(),
333        columns,
334        body,
335        where_clauses,
336        returning,
337        apply_defaults: false,
338    }
339}
340
341/// Build a delete MutatePlan from GraphQL arguments.
342pub fn build_delete_plan(args: &DeleteArgs, table: &Table) -> MutatePlan {
343    let where_clauses = build_where_clauses(&args.filter, table);
344    let returning = if args.returning.is_empty() {
345        table.pk_cols.clone()
346    } else {
347        args.returning.clone()
348    };
349
350    MutatePlan::Delete {
351        target: table.qualified_identifier(),
352        where_clauses,
353        returning,
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use crate::input::filter::IntFilterInput;
361    use crate::resolver::query::FieldFilter;
362    use indexmap::IndexMap;
363    use postrust_core::schema_cache::Column;
364    use pretty_assertions::assert_eq;
365
366    fn create_test_table() -> Table {
367        let mut columns = IndexMap::new();
368        columns.insert(
369            "id".into(),
370            Column {
371                name: "id".into(),
372                description: None,
373                nullable: false,
374                data_type: "integer".into(),
375                nominal_type: "int4".into(),
376                max_len: None,
377                default: Some("nextval('users_id_seq')".into()),
378                enum_values: vec![],
379                is_pk: true,
380                position: 1,
381            },
382        );
383        columns.insert(
384            "name".into(),
385            Column {
386                name: "name".into(),
387                description: None,
388                nullable: false,
389                data_type: "text".into(),
390                nominal_type: "text".into(),
391                max_len: None,
392                default: None,
393                enum_values: vec![],
394                is_pk: false,
395                position: 2,
396            },
397        );
398        columns.insert(
399            "email".into(),
400            Column {
401                name: "email".into(),
402                description: None,
403                nullable: true,
404                data_type: "text".into(),
405                nominal_type: "text".into(),
406                max_len: None,
407                default: None,
408                enum_values: vec![],
409                is_pk: false,
410                position: 3,
411            },
412        );
413
414        Table {
415            schema: "public".into(),
416            name: "users".into(),
417            description: None,
418            is_view: false,
419            insertable: true,
420            updatable: true,
421            deletable: true,
422            pk_cols: vec!["id".into()],
423            columns,
424        }
425    }
426
427    // ============================================================================
428    // InsertArgs Tests
429    // ============================================================================
430
431    #[test]
432    fn test_insert_args_default() {
433        let args = InsertArgs::new();
434        assert!(!args.has_objects());
435        assert_eq!(args.object_count(), 0);
436    }
437
438    #[test]
439    fn test_insert_args_with_object() {
440        let mut object = HashMap::new();
441        object.insert("name".to_string(), InputValue::String("Alice".to_string()));
442        object.insert(
443            "email".to_string(),
444            InputValue::String("alice@example.com".to_string()),
445        );
446
447        let args = InsertArgs::new().with_object(object);
448        assert!(args.has_objects());
449        assert_eq!(args.object_count(), 1);
450    }
451
452    #[test]
453    fn test_insert_args_with_multiple_objects() {
454        let obj1: HashMap<String, InputValue> =
455            [("name".to_string(), InputValue::String("Alice".to_string()))]
456                .into_iter()
457                .collect();
458        let obj2: HashMap<String, InputValue> =
459            [("name".to_string(), InputValue::String("Bob".to_string()))]
460                .into_iter()
461                .collect();
462
463        let args = InsertArgs::new().with_objects(vec![obj1, obj2]);
464        assert_eq!(args.object_count(), 2);
465    }
466
467    #[test]
468    fn test_insert_args_column_names() {
469        let object: HashMap<String, InputValue> = [
470            ("name".to_string(), InputValue::String("Alice".to_string())),
471            (
472                "email".to_string(),
473                InputValue::String("alice@example.com".to_string()),
474            ),
475        ]
476        .into_iter()
477        .collect();
478
479        let args = InsertArgs::new().with_object(object);
480        let columns = args.column_names();
481        assert_eq!(columns.len(), 2);
482        assert!(columns.contains(&"name".to_string()));
483        assert!(columns.contains(&"email".to_string()));
484    }
485
486    #[test]
487    fn test_insert_args_to_json_bytes() {
488        let object: HashMap<String, InputValue> =
489            [("name".to_string(), InputValue::String("Alice".to_string()))]
490                .into_iter()
491                .collect();
492
493        let args = InsertArgs::new().with_object(object);
494        let bytes = args.to_json_bytes().unwrap();
495        let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
496        assert_eq!(json["name"], "Alice");
497    }
498
499    #[test]
500    fn test_insert_args_with_returning() {
501        let args = InsertArgs::new().with_returning(vec!["id".to_string(), "name".to_string()]);
502        assert_eq!(args.returning.len(), 2);
503    }
504
505    #[test]
506    fn test_insert_args_with_on_conflict() {
507        let on_conflict = OnConflictArgs::new(vec!["email".to_string()])
508            .with_update_columns(vec!["name".to_string()]);
509
510        let args = InsertArgs::new().with_on_conflict(on_conflict);
511        assert!(args.on_conflict.is_some());
512    }
513
514    // ============================================================================
515    // OnConflictArgs Tests
516    // ============================================================================
517
518    #[test]
519    fn test_on_conflict_args() {
520        let args = OnConflictArgs::new(vec!["id".to_string()])
521            .with_update_columns(vec!["name".to_string(), "email".to_string()]);
522
523        assert_eq!(args.constraint, vec!["id".to_string()]);
524        assert_eq!(args.update_columns.len(), 2);
525    }
526
527    // ============================================================================
528    // UpdateArgs Tests
529    // ============================================================================
530
531    #[test]
532    fn test_update_args_default() {
533        let args = UpdateArgs::new();
534        assert!(!args.has_filter());
535        assert!(!args.has_set());
536    }
537
538    #[test]
539    fn test_update_args_with_set() {
540        let set: HashMap<String, InputValue> = [(
541            "name".to_string(),
542            InputValue::String("Updated".to_string()),
543        )]
544        .into_iter()
545        .collect();
546
547        let args = UpdateArgs::new().with_set(set);
548        assert!(args.has_set());
549        assert_eq!(args.column_names().len(), 1);
550    }
551
552    #[test]
553    fn test_update_args_with_filter() {
554        let filter = TableFilter::new().with_field(
555            "id",
556            FieldFilter::int(IntFilterInput {
557                eq: Some(1),
558                ..Default::default()
559            }),
560        );
561
562        let args = UpdateArgs::new().with_filter(filter);
563        assert!(args.has_filter());
564    }
565
566    #[test]
567    fn test_update_args_to_json_bytes() {
568        let set: HashMap<String, InputValue> = [
569            (
570                "name".to_string(),
571                InputValue::String("Updated".to_string()),
572            ),
573            ("active".to_string(), InputValue::Bool(true)),
574        ]
575        .into_iter()
576        .collect();
577
578        let args = UpdateArgs::new().with_set(set);
579        let bytes = args.to_json_bytes().unwrap();
580        let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
581        assert_eq!(json["name"], "Updated");
582        assert_eq!(json["active"], true);
583    }
584
585    // ============================================================================
586    // DeleteArgs Tests
587    // ============================================================================
588
589    #[test]
590    fn test_delete_args_default() {
591        let args = DeleteArgs::new();
592        assert!(!args.has_filter());
593    }
594
595    #[test]
596    fn test_delete_args_with_filter() {
597        let filter = TableFilter::new().with_field(
598            "id",
599            FieldFilter::int(IntFilterInput {
600                eq: Some(1),
601                ..Default::default()
602            }),
603        );
604
605        let args = DeleteArgs::new().with_filter(filter);
606        assert!(args.has_filter());
607    }
608
609    #[test]
610    fn test_delete_args_with_returning() {
611        let args = DeleteArgs::new().with_returning(vec!["id".to_string(), "name".to_string()]);
612        assert_eq!(args.returning.len(), 2);
613    }
614
615    // ============================================================================
616    // InputValue to JSON Tests
617    // ============================================================================
618
619    #[test]
620    fn test_input_value_to_json_null() {
621        let json = input_value_to_json(&InputValue::Null);
622        assert!(json.is_null());
623    }
624
625    #[test]
626    fn test_input_value_to_json_bool() {
627        let json = input_value_to_json(&InputValue::Bool(true));
628        assert_eq!(json, serde_json::Value::Bool(true));
629    }
630
631    #[test]
632    fn test_input_value_to_json_int() {
633        let json = input_value_to_json(&InputValue::Int(42));
634        assert_eq!(json, serde_json::json!(42));
635    }
636
637    #[test]
638    fn test_input_value_to_json_float() {
639        let json = input_value_to_json(&InputValue::Float(1.5));
640        assert_eq!(json, serde_json::json!(1.5));
641    }
642
643    #[test]
644    fn test_input_value_to_json_string() {
645        let json = input_value_to_json(&InputValue::String("hello".to_string()));
646        assert_eq!(json, serde_json::json!("hello"));
647    }
648
649    #[test]
650    fn test_input_value_to_json_array() {
651        let arr = vec![InputValue::Int(1), InputValue::Int(2), InputValue::Int(3)];
652        let json = input_value_to_json(&InputValue::Array(arr));
653        assert_eq!(json, serde_json::json!([1, 2, 3]));
654    }
655
656    #[test]
657    fn test_input_value_to_json_object() {
658        let obj: HashMap<String, InputValue> = [
659            ("name".to_string(), InputValue::String("test".to_string())),
660            ("count".to_string(), InputValue::Int(5)),
661        ]
662        .into_iter()
663        .collect();
664        let json = input_value_to_json(&InputValue::Object(obj));
665        assert_eq!(json["name"], "test");
666        assert_eq!(json["count"], 5);
667    }
668
669    // ============================================================================
670    // MutatePlan Building Tests
671    // ============================================================================
672
673    #[test]
674    fn test_build_insert_plan_basic() {
675        let table = create_test_table();
676        let object: HashMap<String, InputValue> =
677            [("name".to_string(), InputValue::String("Alice".to_string()))]
678                .into_iter()
679                .collect();
680
681        let args = InsertArgs::new().with_object(object);
682        let plan = build_insert_plan(&args, &table);
683
684        match plan {
685            MutatePlan::Insert {
686                target,
687                body,
688                returning,
689                ..
690            } => {
691                assert_eq!(target.name, "users");
692                assert!(body.is_some());
693                assert_eq!(returning, vec!["id".to_string()]);
694            }
695            _ => panic!("Expected Insert plan"),
696        }
697    }
698
699    #[test]
700    fn test_build_insert_plan_with_returning() {
701        let table = create_test_table();
702        let object: HashMap<String, InputValue> =
703            [("name".to_string(), InputValue::String("Alice".to_string()))]
704                .into_iter()
705                .collect();
706
707        let args = InsertArgs::new()
708            .with_object(object)
709            .with_returning(vec!["id".to_string(), "name".to_string()]);
710        let plan = build_insert_plan(&args, &table);
711
712        match plan {
713            MutatePlan::Insert { returning, .. } => {
714                assert_eq!(returning.len(), 2);
715            }
716            _ => panic!("Expected Insert plan"),
717        }
718    }
719
720    #[test]
721    fn test_build_insert_plan_with_on_conflict() {
722        let table = create_test_table();
723        let object: HashMap<String, InputValue> =
724            [("name".to_string(), InputValue::String("Alice".to_string()))]
725                .into_iter()
726                .collect();
727
728        let on_conflict = OnConflictArgs::new(vec!["id".to_string()]);
729        let args = InsertArgs::new()
730            .with_object(object)
731            .with_on_conflict(on_conflict);
732        let plan = build_insert_plan(&args, &table);
733
734        match plan {
735            MutatePlan::Insert { on_conflict, .. } => {
736                assert!(on_conflict.is_some());
737                let (_, cols) = on_conflict.unwrap();
738                assert_eq!(cols, vec!["id".to_string()]);
739            }
740            _ => panic!("Expected Insert plan"),
741        }
742    }
743
744    #[test]
745    fn test_build_update_plan_basic() {
746        let table = create_test_table();
747        let set: HashMap<String, InputValue> = [(
748            "name".to_string(),
749            InputValue::String("Updated".to_string()),
750        )]
751        .into_iter()
752        .collect();
753
754        let filter = TableFilter::new().with_field(
755            "id",
756            FieldFilter::int(IntFilterInput {
757                eq: Some(1),
758                ..Default::default()
759            }),
760        );
761
762        let args = UpdateArgs::new().with_set(set).with_filter(filter);
763        let plan = build_update_plan(&args, &table);
764
765        match plan {
766            MutatePlan::Update {
767                target,
768                body,
769                where_clauses,
770                ..
771            } => {
772                assert_eq!(target.name, "users");
773                assert!(body.is_some());
774                assert!(!where_clauses.is_empty());
775            }
776            _ => panic!("Expected Update plan"),
777        }
778    }
779
780    #[test]
781    fn test_build_update_plan_with_returning() {
782        let table = create_test_table();
783        let set: HashMap<String, InputValue> = [(
784            "name".to_string(),
785            InputValue::String("Updated".to_string()),
786        )]
787        .into_iter()
788        .collect();
789
790        let args = UpdateArgs::new()
791            .with_set(set)
792            .with_returning(vec!["id".to_string(), "name".to_string()]);
793        let plan = build_update_plan(&args, &table);
794
795        match plan {
796            MutatePlan::Update { returning, .. } => {
797                assert_eq!(returning.len(), 2);
798            }
799            _ => panic!("Expected Update plan"),
800        }
801    }
802
803    #[test]
804    fn test_build_delete_plan_basic() {
805        let table = create_test_table();
806        let filter = TableFilter::new().with_field(
807            "id",
808            FieldFilter::int(IntFilterInput {
809                eq: Some(1),
810                ..Default::default()
811            }),
812        );
813
814        let args = DeleteArgs::new().with_filter(filter);
815        let plan = build_delete_plan(&args, &table);
816
817        match plan {
818            MutatePlan::Delete {
819                target,
820                where_clauses,
821                returning,
822            } => {
823                assert_eq!(target.name, "users");
824                assert!(!where_clauses.is_empty());
825                assert_eq!(returning, vec!["id".to_string()]);
826            }
827            _ => panic!("Expected Delete plan"),
828        }
829    }
830
831    #[test]
832    fn test_build_delete_plan_with_returning() {
833        let table = create_test_table();
834        let args = DeleteArgs::new().with_returning(vec![
835            "id".to_string(),
836            "name".to_string(),
837            "email".to_string(),
838        ]);
839        let plan = build_delete_plan(&args, &table);
840
841        match plan {
842            MutatePlan::Delete { returning, .. } => {
843                assert_eq!(returning.len(), 3);
844            }
845            _ => panic!("Expected Delete plan"),
846        }
847    }
848
849    #[test]
850    fn test_build_delete_plan_no_filter() {
851        let table = create_test_table();
852        let args = DeleteArgs::new();
853        let plan = build_delete_plan(&args, &table);
854
855        match plan {
856            MutatePlan::Delete { where_clauses, .. } => {
857                assert!(where_clauses.is_empty());
858            }
859            _ => panic!("Expected Delete plan"),
860        }
861    }
862}