postrust_graphql/resolver/
query.rs

1//! Query resolvers for GraphQL table queries.
2//!
3//! Converts GraphQL query arguments into ReadPlan structures that can be executed.
4
5use crate::input::filter::{
6    combine_with_and, filters_to_logic_tree, BooleanFilterInput, FloatFilterInput, IntFilterInput,
7    StringFilterInput, UuidFilterInput,
8};
9use crate::input::order::{OrderByField, PaginationInput};
10use postrust_core::api_request::{Filter, LogicTree, Range};
11use postrust_core::plan::{CoercibleLogicTree, CoercibleOrderTerm, CoercibleSelectField, ReadPlan};
12use postrust_core::schema_cache::Table;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// Arguments for a GraphQL table query.
17#[derive(Debug, Clone, Default)]
18pub struct QueryArgs {
19    /// Fields to select (column names)
20    pub select: Vec<String>,
21    /// Filter conditions
22    pub filter: Option<TableFilter>,
23    /// Order by specifications
24    pub order_by: Vec<OrderByField>,
25    /// Limit number of results
26    pub limit: Option<i64>,
27    /// Offset into results
28    pub offset: Option<i64>,
29    /// Nested relation queries
30    pub relations: HashMap<String, QueryArgs>,
31}
32
33impl QueryArgs {
34    /// Create empty query args.
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Set the select fields.
40    pub fn with_select(mut self, fields: Vec<String>) -> Self {
41        self.select = fields;
42        self
43    }
44
45    /// Set the filter.
46    pub fn with_filter(mut self, filter: TableFilter) -> Self {
47        self.filter = Some(filter);
48        self
49    }
50
51    /// Set order by.
52    pub fn with_order_by(mut self, order_by: Vec<OrderByField>) -> Self {
53        self.order_by = order_by;
54        self
55    }
56
57    /// Set pagination.
58    pub fn with_pagination(mut self, pagination: PaginationInput) -> Self {
59        self.limit = pagination.limit;
60        self.offset = pagination.offset;
61        self
62    }
63
64    /// Set limit.
65    pub fn with_limit(mut self, limit: i64) -> Self {
66        self.limit = Some(limit);
67        self
68    }
69
70    /// Set offset.
71    pub fn with_offset(mut self, offset: i64) -> Self {
72        self.offset = Some(offset);
73        self
74    }
75
76    /// Add a nested relation query.
77    pub fn with_relation(mut self, name: String, args: QueryArgs) -> Self {
78        self.relations.insert(name, args);
79        self
80    }
81
82    /// Get the pagination range.
83    pub fn to_range(&self) -> Range {
84        Range {
85            offset: self.offset.unwrap_or(0),
86            limit: self.limit,
87        }
88    }
89
90    /// Check if any select fields are specified.
91    pub fn has_select(&self) -> bool {
92        !self.select.is_empty()
93    }
94
95    /// Check if any filters are specified.
96    pub fn has_filter(&self) -> bool {
97        self.filter.is_some()
98    }
99
100    /// Check if any ordering is specified.
101    pub fn has_order_by(&self) -> bool {
102        !self.order_by.is_empty()
103    }
104
105    /// Check if pagination is specified.
106    pub fn has_pagination(&self) -> bool {
107        self.limit.is_some() || self.offset.is_some()
108    }
109}
110
111/// A dynamic filter for any table field.
112#[derive(Debug, Clone, Default, Serialize, Deserialize)]
113pub struct TableFilter {
114    /// Field-specific filters
115    #[serde(flatten)]
116    pub fields: HashMap<String, FieldFilter>,
117    /// AND combined filters
118    #[serde(rename = "_and")]
119    pub and: Option<Vec<TableFilter>>,
120    /// OR combined filters
121    #[serde(rename = "_or")]
122    pub or: Option<Vec<TableFilter>>,
123    /// Negated filter
124    #[serde(rename = "_not")]
125    pub not: Option<Box<TableFilter>>,
126}
127
128impl TableFilter {
129    /// Create an empty filter.
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Add a field filter.
135    pub fn with_field(mut self, name: impl Into<String>, filter: FieldFilter) -> Self {
136        self.fields.insert(name.into(), filter);
137        self
138    }
139
140    /// Add AND filters.
141    pub fn with_and(mut self, filters: Vec<TableFilter>) -> Self {
142        self.and = Some(filters);
143        self
144    }
145
146    /// Add OR filters.
147    pub fn with_or(mut self, filters: Vec<TableFilter>) -> Self {
148        self.or = Some(filters);
149        self
150    }
151
152    /// Add NOT filter.
153    pub fn with_not(mut self, filter: TableFilter) -> Self {
154        self.not = Some(Box::new(filter));
155        self
156    }
157
158    /// Check if filter is empty.
159    pub fn is_empty(&self) -> bool {
160        self.fields.is_empty() && self.and.is_none() && self.or.is_none() && self.not.is_none()
161    }
162
163    /// Convert to a LogicTree.
164    pub fn to_logic_tree(&self) -> Option<LogicTree> {
165        let mut trees = Vec::new();
166
167        // Add field filters
168        for (field_name, field_filter) in &self.fields {
169            let filters = field_filter.to_filters(field_name);
170            if let Some(tree) = filters_to_logic_tree(filters) {
171                trees.push(tree);
172            }
173        }
174
175        // Add AND filters
176        if let Some(and_filters) = &self.and {
177            let and_trees: Vec<LogicTree> = and_filters
178                .iter()
179                .filter_map(|f| f.to_logic_tree())
180                .collect();
181            if let Some(tree) = combine_with_and(and_trees) {
182                trees.push(tree);
183            }
184        }
185
186        // Add OR filters
187        if let Some(or_filters) = &self.or {
188            let or_trees: Vec<LogicTree> = or_filters
189                .iter()
190                .filter_map(|f| f.to_logic_tree())
191                .collect();
192            if !or_trees.is_empty() {
193                trees.push(LogicTree::or(or_trees));
194            }
195        }
196
197        // Add NOT filter
198        if let Some(not_filter) = &self.not {
199            if let Some(tree) = not_filter.to_logic_tree() {
200                let negated = match tree {
201                    LogicTree::Expr { op, children, .. } => LogicTree::Expr {
202                        negated: true,
203                        op,
204                        children,
205                    },
206                    LogicTree::Stmt(filter) => {
207                        let negated_expr = postrust_core::api_request::OpExpr {
208                            negated: !filter.op_expr.negated,
209                            operation: filter.op_expr.operation,
210                        };
211                        LogicTree::Stmt(Filter::new(filter.field, negated_expr))
212                    }
213                };
214                trees.push(negated);
215            }
216        }
217
218        combine_with_and(trees)
219    }
220}
221
222/// A filter for a single field that can handle different types.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(untagged)]
225pub enum FieldFilter {
226    /// String filter operations
227    String(StringFilterInput),
228    /// Integer filter operations
229    Int(IntFilterInput),
230    /// Float filter operations
231    Float(FloatFilterInput),
232    /// Boolean filter operations
233    Boolean(BooleanFilterInput),
234    /// UUID filter operations
235    Uuid(UuidFilterInput),
236}
237
238impl FieldFilter {
239    /// Create a string filter.
240    pub fn string(filter: StringFilterInput) -> Self {
241        Self::String(filter)
242    }
243
244    /// Create an int filter.
245    pub fn int(filter: IntFilterInput) -> Self {
246        Self::Int(filter)
247    }
248
249    /// Create a float filter.
250    pub fn float(filter: FloatFilterInput) -> Self {
251        Self::Float(filter)
252    }
253
254    /// Create a boolean filter.
255    pub fn boolean(filter: BooleanFilterInput) -> Self {
256        Self::Boolean(filter)
257    }
258
259    /// Create a UUID filter.
260    pub fn uuid(filter: UuidFilterInput) -> Self {
261        Self::Uuid(filter)
262    }
263
264    /// Convert to a list of Filters.
265    pub fn to_filters(&self, field_name: &str) -> Vec<Filter> {
266        match self {
267            Self::String(f) => f.to_filters(field_name),
268            Self::Int(f) => f.to_filters(field_name),
269            Self::Float(f) => f.to_filters(field_name),
270            Self::Boolean(f) => f.to_filters(field_name),
271            Self::Uuid(f) => f.to_filters(field_name),
272        }
273    }
274}
275
276/// Build select fields from column names.
277pub fn build_select_fields(columns: &[String], table: &Table) -> Vec<CoercibleSelectField> {
278    if columns.is_empty() {
279        // Default: select all columns
280        return table
281            .columns
282            .iter()
283            .map(|(name, col)| CoercibleSelectField::simple(name, &col.data_type))
284            .collect();
285    }
286
287    columns
288        .iter()
289        .filter_map(|name| {
290            table
291                .columns
292                .get(name)
293                .map(|col| CoercibleSelectField::simple(name, &col.data_type))
294        })
295        .collect()
296}
297
298/// Build order terms from OrderByFields.
299pub fn build_order_terms(order_by: &[OrderByField], table: &Table) -> Vec<CoercibleOrderTerm> {
300    order_by
301        .iter()
302        .filter_map(|ob| {
303            table.columns.get(&ob.field).map(|col| {
304                let order_term = ob.to_order_term();
305                CoercibleOrderTerm::from_order_term(&order_term, &col.data_type)
306            })
307        })
308        .collect()
309}
310
311/// Build where clauses from a TableFilter.
312pub fn build_where_clauses(filter: &Option<TableFilter>, table: &Table) -> Vec<CoercibleLogicTree> {
313    let Some(filter) = filter else {
314        return vec![];
315    };
316
317    let type_resolver = |name: &str| -> String {
318        table
319            .get_column(name)
320            .map(|c| c.data_type.clone())
321            .unwrap_or_else(|| "text".to_string())
322    };
323
324    filter
325        .to_logic_tree()
326        .map(|tree| vec![CoercibleLogicTree::from_logic_tree(&tree, type_resolver)])
327        .unwrap_or_default()
328}
329
330/// Build a ReadPlan from GraphQL query arguments.
331pub fn build_read_plan(args: &QueryArgs, table: &Table) -> ReadPlan {
332    let select = build_select_fields(&args.select, table);
333    let order = build_order_terms(&args.order_by, table);
334    let where_clauses = build_where_clauses(&args.filter, table);
335
336    ReadPlan {
337        select,
338        from: table.qualified_identifier(),
339        from_alias: None,
340        where_clauses,
341        order,
342        range: args.to_range(),
343        rel_name: table.name.clone(),
344        rel_to_parent: None,
345        rel_join_conds: vec![],
346        rel_join_type: None,
347        rel_select: vec![],
348        depth: 0,
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use crate::input::filter::IntFilterInput;
356    use indexmap::IndexMap;
357    use postrust_core::schema_cache::Column;
358    use pretty_assertions::assert_eq;
359
360    fn create_test_table() -> Table {
361        let mut columns = IndexMap::new();
362        columns.insert(
363            "id".into(),
364            Column {
365                name: "id".into(),
366                description: None,
367                nullable: false,
368                data_type: "integer".into(),
369                nominal_type: "int4".into(),
370                max_len: None,
371                default: Some("nextval('users_id_seq')".into()),
372                enum_values: vec![],
373                is_pk: true,
374                position: 1,
375            },
376        );
377        columns.insert(
378            "name".into(),
379            Column {
380                name: "name".into(),
381                description: None,
382                nullable: false,
383                data_type: "text".into(),
384                nominal_type: "text".into(),
385                max_len: None,
386                default: None,
387                enum_values: vec![],
388                is_pk: false,
389                position: 2,
390            },
391        );
392        columns.insert(
393            "email".into(),
394            Column {
395                name: "email".into(),
396                description: None,
397                nullable: true,
398                data_type: "text".into(),
399                nominal_type: "text".into(),
400                max_len: None,
401                default: None,
402                enum_values: vec![],
403                is_pk: false,
404                position: 3,
405            },
406        );
407        columns.insert(
408            "age".into(),
409            Column {
410                name: "age".into(),
411                description: None,
412                nullable: true,
413                data_type: "integer".into(),
414                nominal_type: "int4".into(),
415                max_len: None,
416                default: None,
417                enum_values: vec![],
418                is_pk: false,
419                position: 4,
420            },
421        );
422
423        Table {
424            schema: "public".into(),
425            name: "users".into(),
426            description: None,
427            is_view: false,
428            insertable: true,
429            updatable: true,
430            deletable: true,
431            pk_cols: vec!["id".into()],
432            columns,
433        }
434    }
435
436    // ============================================================================
437    // QueryArgs Tests
438    // ============================================================================
439
440    #[test]
441    fn test_query_args_default() {
442        let args = QueryArgs::new();
443        assert!(!args.has_select());
444        assert!(!args.has_filter());
445        assert!(!args.has_order_by());
446        assert!(!args.has_pagination());
447    }
448
449    #[test]
450    fn test_query_args_with_select() {
451        let args = QueryArgs::new().with_select(vec!["id".to_string(), "name".to_string()]);
452        assert!(args.has_select());
453        assert_eq!(args.select.len(), 2);
454    }
455
456    #[test]
457    fn test_query_args_with_filter() {
458        let filter = TableFilter::new().with_field(
459            "name",
460            FieldFilter::string(StringFilterInput {
461                eq: Some("test".to_string()),
462                ..Default::default()
463            }),
464        );
465        let args = QueryArgs::new().with_filter(filter);
466        assert!(args.has_filter());
467    }
468
469    #[test]
470    fn test_query_args_with_order_by() {
471        let args = QueryArgs::new().with_order_by(vec![OrderByField::desc("created_at")]);
472        assert!(args.has_order_by());
473        assert_eq!(args.order_by.len(), 1);
474    }
475
476    #[test]
477    fn test_query_args_with_pagination() {
478        let args = QueryArgs::new().with_limit(10).with_offset(20);
479        assert!(args.has_pagination());
480        assert_eq!(args.limit, Some(10));
481        assert_eq!(args.offset, Some(20));
482    }
483
484    #[test]
485    fn test_query_args_to_range() {
486        let args = QueryArgs::new().with_limit(10).with_offset(5);
487        let range = args.to_range();
488        assert_eq!(range.offset, 5);
489        assert_eq!(range.limit, Some(10));
490    }
491
492    #[test]
493    fn test_query_args_with_relation() {
494        let child_args = QueryArgs::new().with_limit(5);
495        let args = QueryArgs::new().with_relation("orders".to_string(), child_args);
496        assert!(args.relations.contains_key("orders"));
497    }
498
499    // ============================================================================
500    // TableFilter Tests
501    // ============================================================================
502
503    #[test]
504    fn test_table_filter_empty() {
505        let filter = TableFilter::new();
506        assert!(filter.is_empty());
507        assert!(filter.to_logic_tree().is_none());
508    }
509
510    #[test]
511    fn test_table_filter_single_field() {
512        let filter = TableFilter::new().with_field(
513            "name",
514            FieldFilter::string(StringFilterInput {
515                eq: Some("Alice".to_string()),
516                ..Default::default()
517            }),
518        );
519        assert!(!filter.is_empty());
520        assert!(filter.to_logic_tree().is_some());
521    }
522
523    #[test]
524    fn test_table_filter_multiple_fields() {
525        let filter = TableFilter::new()
526            .with_field(
527                "name",
528                FieldFilter::string(StringFilterInput {
529                    eq: Some("Alice".to_string()),
530                    ..Default::default()
531                }),
532            )
533            .with_field(
534                "age",
535                FieldFilter::int(IntFilterInput {
536                    gt: Some(18),
537                    ..Default::default()
538                }),
539            );
540
541        let tree = filter.to_logic_tree().unwrap();
542        match tree {
543            LogicTree::Expr { children, .. } => {
544                assert_eq!(children.len(), 2);
545            }
546            _ => panic!("Expected Expr for multiple fields"),
547        }
548    }
549
550    #[test]
551    fn test_table_filter_with_and() {
552        let filter1 = TableFilter::new().with_field(
553            "a",
554            FieldFilter::int(IntFilterInput {
555                eq: Some(1),
556                ..Default::default()
557            }),
558        );
559        let filter2 = TableFilter::new().with_field(
560            "b",
561            FieldFilter::int(IntFilterInput {
562                eq: Some(2),
563                ..Default::default()
564            }),
565        );
566
567        let combined = TableFilter::new().with_and(vec![filter1, filter2]);
568        let tree = combined.to_logic_tree().unwrap();
569        assert!(matches!(tree, LogicTree::Expr { .. }));
570    }
571
572    #[test]
573    fn test_table_filter_with_or() {
574        let filter1 = TableFilter::new().with_field(
575            "status",
576            FieldFilter::string(StringFilterInput {
577                eq: Some("active".to_string()),
578                ..Default::default()
579            }),
580        );
581        let filter2 = TableFilter::new().with_field(
582            "status",
583            FieldFilter::string(StringFilterInput {
584                eq: Some("pending".to_string()),
585                ..Default::default()
586            }),
587        );
588
589        let combined = TableFilter::new().with_or(vec![filter1, filter2]);
590        let tree = combined.to_logic_tree().unwrap();
591
592        match tree {
593            LogicTree::Expr {
594                op: postrust_core::api_request::LogicOperator::Or,
595                ..
596            } => {}
597            _ => panic!("Expected OR expression"),
598        }
599    }
600
601    #[test]
602    fn test_table_filter_with_not() {
603        let inner = TableFilter::new().with_field(
604            "deleted",
605            FieldFilter::boolean(BooleanFilterInput {
606                eq: Some(true),
607                ..Default::default()
608            }),
609        );
610
611        let filter = TableFilter::new().with_not(inner);
612        let tree = filter.to_logic_tree().unwrap();
613
614        match tree {
615            LogicTree::Expr { negated: true, .. } | LogicTree::Stmt(_) => {}
616            _ => panic!("Expected negated expression"),
617        }
618    }
619
620    // ============================================================================
621    // FieldFilter Tests
622    // ============================================================================
623
624    #[test]
625    fn test_field_filter_string() {
626        let filter = FieldFilter::string(StringFilterInput {
627            eq: Some("test".to_string()),
628            ..Default::default()
629        });
630        let filters = filter.to_filters("name");
631        assert_eq!(filters.len(), 1);
632    }
633
634    #[test]
635    fn test_field_filter_int() {
636        let filter = FieldFilter::int(IntFilterInput {
637            gte: Some(18),
638            lte: Some(65),
639            ..Default::default()
640        });
641        let filters = filter.to_filters("age");
642        assert_eq!(filters.len(), 2); // gte and lte
643    }
644
645    #[test]
646    fn test_field_filter_boolean() {
647        let filter = FieldFilter::boolean(BooleanFilterInput {
648            eq: Some(true),
649            ..Default::default()
650        });
651        let filters = filter.to_filters("active");
652        assert_eq!(filters.len(), 1);
653    }
654
655    // ============================================================================
656    // Build Functions Tests
657    // ============================================================================
658
659    #[test]
660    fn test_build_select_fields_empty() {
661        let table = create_test_table();
662        let fields = build_select_fields(&[], &table);
663        assert_eq!(fields.len(), 4); // All columns
664    }
665
666    #[test]
667    fn test_build_select_fields_specific() {
668        let table = create_test_table();
669        let fields = build_select_fields(&["id".to_string(), "name".to_string()], &table);
670        assert_eq!(fields.len(), 2);
671    }
672
673    #[test]
674    fn test_build_select_fields_invalid_column() {
675        let table = create_test_table();
676        let fields = build_select_fields(&["nonexistent".to_string()], &table);
677        assert_eq!(fields.len(), 0); // Invalid columns are skipped
678    }
679
680    #[test]
681    fn test_build_order_terms() {
682        let table = create_test_table();
683        let order_by = vec![OrderByField::desc("name"), OrderByField::asc("id")];
684        let terms = build_order_terms(&order_by, &table);
685        assert_eq!(terms.len(), 2);
686    }
687
688    #[test]
689    fn test_build_order_terms_invalid_column() {
690        let table = create_test_table();
691        let order_by = vec![OrderByField::desc("nonexistent")];
692        let terms = build_order_terms(&order_by, &table);
693        assert_eq!(terms.len(), 0); // Invalid columns are skipped
694    }
695
696    #[test]
697    fn test_build_where_clauses_none() {
698        let table = create_test_table();
699        let clauses = build_where_clauses(&None, &table);
700        assert!(clauses.is_empty());
701    }
702
703    #[test]
704    fn test_build_where_clauses_with_filter() {
705        let table = create_test_table();
706        let filter = TableFilter::new().with_field(
707            "name",
708            FieldFilter::string(StringFilterInput {
709                eq: Some("test".to_string()),
710                ..Default::default()
711            }),
712        );
713        let clauses = build_where_clauses(&Some(filter), &table);
714        assert_eq!(clauses.len(), 1);
715    }
716
717    // ============================================================================
718    // ReadPlan Building Tests
719    // ============================================================================
720
721    #[test]
722    fn test_build_read_plan_basic() {
723        let table = create_test_table();
724        let args = QueryArgs::new();
725        let plan = build_read_plan(&args, &table);
726
727        assert_eq!(plan.from.name, "users");
728        assert_eq!(plan.select.len(), 4); // All columns
729        assert!(plan.where_clauses.is_empty());
730        assert!(plan.order.is_empty());
731    }
732
733    #[test]
734    fn test_build_read_plan_with_select() {
735        let table = create_test_table();
736        let args = QueryArgs::new().with_select(vec!["id".to_string(), "name".to_string()]);
737        let plan = build_read_plan(&args, &table);
738
739        assert_eq!(plan.select.len(), 2);
740    }
741
742    #[test]
743    fn test_build_read_plan_with_filter() {
744        let table = create_test_table();
745        let filter = TableFilter::new().with_field(
746            "age",
747            FieldFilter::int(IntFilterInput {
748                gte: Some(18),
749                ..Default::default()
750            }),
751        );
752        let args = QueryArgs::new().with_filter(filter);
753        let plan = build_read_plan(&args, &table);
754
755        assert!(!plan.where_clauses.is_empty());
756    }
757
758    #[test]
759    fn test_build_read_plan_with_order() {
760        let table = create_test_table();
761        let args = QueryArgs::new().with_order_by(vec![OrderByField::desc("name")]);
762        let plan = build_read_plan(&args, &table);
763
764        assert_eq!(plan.order.len(), 1);
765    }
766
767    #[test]
768    fn test_build_read_plan_with_pagination() {
769        let table = create_test_table();
770        let args = QueryArgs::new().with_limit(10).with_offset(20);
771        let plan = build_read_plan(&args, &table);
772
773        assert_eq!(plan.range.limit, Some(10));
774        assert_eq!(plan.range.offset, 20);
775    }
776
777    #[test]
778    fn test_build_read_plan_full() {
779        let table = create_test_table();
780        let filter = TableFilter::new()
781            .with_field(
782                "name",
783                FieldFilter::string(StringFilterInput {
784                    like: Some("%John%".to_string()),
785                    ..Default::default()
786                }),
787            )
788            .with_field(
789                "age",
790                FieldFilter::int(IntFilterInput {
791                    gte: Some(21),
792                    ..Default::default()
793                }),
794            );
795
796        let args = QueryArgs::new()
797            .with_select(vec!["id".to_string(), "name".to_string(), "email".to_string()])
798            .with_filter(filter)
799            .with_order_by(vec![OrderByField::asc("name")])
800            .with_limit(50)
801            .with_offset(0);
802
803        let plan = build_read_plan(&args, &table);
804
805        assert_eq!(plan.select.len(), 3);
806        assert!(!plan.where_clauses.is_empty());
807        assert_eq!(plan.order.len(), 1);
808        assert_eq!(plan.range.limit, Some(50));
809        assert_eq!(plan.range.offset, 0);
810    }
811}