1use 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#[derive(Debug, Clone, Default)]
18pub struct QueryArgs {
19 pub select: Vec<String>,
21 pub filter: Option<TableFilter>,
23 pub order_by: Vec<OrderByField>,
25 pub limit: Option<i64>,
27 pub offset: Option<i64>,
29 pub relations: HashMap<String, QueryArgs>,
31}
32
33impl QueryArgs {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn with_select(mut self, fields: Vec<String>) -> Self {
41 self.select = fields;
42 self
43 }
44
45 pub fn with_filter(mut self, filter: TableFilter) -> Self {
47 self.filter = Some(filter);
48 self
49 }
50
51 pub fn with_order_by(mut self, order_by: Vec<OrderByField>) -> Self {
53 self.order_by = order_by;
54 self
55 }
56
57 pub fn with_pagination(mut self, pagination: PaginationInput) -> Self {
59 self.limit = pagination.limit;
60 self.offset = pagination.offset;
61 self
62 }
63
64 pub fn with_limit(mut self, limit: i64) -> Self {
66 self.limit = Some(limit);
67 self
68 }
69
70 pub fn with_offset(mut self, offset: i64) -> Self {
72 self.offset = Some(offset);
73 self
74 }
75
76 pub fn with_relation(mut self, name: String, args: QueryArgs) -> Self {
78 self.relations.insert(name, args);
79 self
80 }
81
82 pub fn to_range(&self) -> Range {
84 Range {
85 offset: self.offset.unwrap_or(0),
86 limit: self.limit,
87 }
88 }
89
90 pub fn has_select(&self) -> bool {
92 !self.select.is_empty()
93 }
94
95 pub fn has_filter(&self) -> bool {
97 self.filter.is_some()
98 }
99
100 pub fn has_order_by(&self) -> bool {
102 !self.order_by.is_empty()
103 }
104
105 pub fn has_pagination(&self) -> bool {
107 self.limit.is_some() || self.offset.is_some()
108 }
109}
110
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
113pub struct TableFilter {
114 #[serde(flatten)]
116 pub fields: HashMap<String, FieldFilter>,
117 #[serde(rename = "_and")]
119 pub and: Option<Vec<TableFilter>>,
120 #[serde(rename = "_or")]
122 pub or: Option<Vec<TableFilter>>,
123 #[serde(rename = "_not")]
125 pub not: Option<Box<TableFilter>>,
126}
127
128impl TableFilter {
129 pub fn new() -> Self {
131 Self::default()
132 }
133
134 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 pub fn with_and(mut self, filters: Vec<TableFilter>) -> Self {
142 self.and = Some(filters);
143 self
144 }
145
146 pub fn with_or(mut self, filters: Vec<TableFilter>) -> Self {
148 self.or = Some(filters);
149 self
150 }
151
152 pub fn with_not(mut self, filter: TableFilter) -> Self {
154 self.not = Some(Box::new(filter));
155 self
156 }
157
158 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 pub fn to_logic_tree(&self) -> Option<LogicTree> {
165 let mut trees = Vec::new();
166
167 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(untagged)]
225pub enum FieldFilter {
226 String(StringFilterInput),
228 Int(IntFilterInput),
230 Float(FloatFilterInput),
232 Boolean(BooleanFilterInput),
234 Uuid(UuidFilterInput),
236}
237
238impl FieldFilter {
239 pub fn string(filter: StringFilterInput) -> Self {
241 Self::String(filter)
242 }
243
244 pub fn int(filter: IntFilterInput) -> Self {
246 Self::Int(filter)
247 }
248
249 pub fn float(filter: FloatFilterInput) -> Self {
251 Self::Float(filter)
252 }
253
254 pub fn boolean(filter: BooleanFilterInput) -> Self {
256 Self::Boolean(filter)
257 }
258
259 pub fn uuid(filter: UuidFilterInput) -> Self {
261 Self::Uuid(filter)
262 }
263
264 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
276pub fn build_select_fields(columns: &[String], table: &Table) -> Vec<CoercibleSelectField> {
278 if columns.is_empty() {
279 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
298pub 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
311pub 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
330pub 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 #[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 #[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 #[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); }
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 #[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); }
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); }
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); }
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 #[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); 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}