Skip to main content

ormdb_proto/
query.rs

1//! Query IR types for graph queries.
2
3use crate::value::Value;
4use rkyv::{Archive, Deserialize, Serialize};
5use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize};
6
7/// Aggregate function types for analytics queries.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
9pub enum AggregateFunction {
10    /// Count of entities/values.
11    Count,
12    /// Sum of numeric values.
13    Sum,
14    /// Average of numeric values.
15    Avg,
16    /// Minimum value.
17    Min,
18    /// Maximum value.
19    Max,
20}
21
22/// A single aggregation operation.
23#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
24pub struct Aggregation {
25    /// The aggregation function to apply.
26    pub function: AggregateFunction,
27    /// Field to aggregate (None for COUNT(*)).
28    pub field: Option<String>,
29}
30
31impl Aggregation {
32    /// Create a COUNT(*) aggregation.
33    pub fn count() -> Self {
34        Self {
35            function: AggregateFunction::Count,
36            field: None,
37        }
38    }
39
40    /// Create a COUNT(field) aggregation.
41    pub fn count_field(field: impl Into<String>) -> Self {
42        Self {
43            function: AggregateFunction::Count,
44            field: Some(field.into()),
45        }
46    }
47
48    /// Create a SUM aggregation.
49    pub fn sum(field: impl Into<String>) -> Self {
50        Self {
51            function: AggregateFunction::Sum,
52            field: Some(field.into()),
53        }
54    }
55
56    /// Create an AVG aggregation.
57    pub fn avg(field: impl Into<String>) -> Self {
58        Self {
59            function: AggregateFunction::Avg,
60            field: Some(field.into()),
61        }
62    }
63
64    /// Create a MIN aggregation.
65    pub fn min(field: impl Into<String>) -> Self {
66        Self {
67            function: AggregateFunction::Min,
68            field: Some(field.into()),
69        }
70    }
71
72    /// Create a MAX aggregation.
73    pub fn max(field: impl Into<String>) -> Self {
74        Self {
75            function: AggregateFunction::Max,
76            field: Some(field.into()),
77        }
78    }
79}
80
81/// An aggregate query for analytics operations.
82#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
83pub struct AggregateQuery {
84    /// Root entity type to aggregate.
85    pub root_entity: String,
86    /// Aggregation operations to perform.
87    pub aggregations: Vec<Aggregation>,
88    /// Optional filter to apply before aggregation.
89    pub filter: Option<Filter>,
90}
91
92impl AggregateQuery {
93    /// Create a new aggregate query for an entity.
94    pub fn new(root_entity: impl Into<String>) -> Self {
95        Self {
96            root_entity: root_entity.into(),
97            aggregations: vec![],
98            filter: None,
99        }
100    }
101
102    /// Add an aggregation operation.
103    pub fn with_aggregation(mut self, aggregation: Aggregation) -> Self {
104        self.aggregations.push(aggregation);
105        self
106    }
107
108    /// Add a COUNT(*) aggregation.
109    pub fn count(mut self) -> Self {
110        self.aggregations.push(Aggregation::count());
111        self
112    }
113
114    /// Add a SUM aggregation.
115    pub fn sum(mut self, field: impl Into<String>) -> Self {
116        self.aggregations.push(Aggregation::sum(field));
117        self
118    }
119
120    /// Add an AVG aggregation.
121    pub fn avg(mut self, field: impl Into<String>) -> Self {
122        self.aggregations.push(Aggregation::avg(field));
123        self
124    }
125
126    /// Add a MIN aggregation.
127    pub fn min(mut self, field: impl Into<String>) -> Self {
128        self.aggregations.push(Aggregation::min(field));
129        self
130    }
131
132    /// Add a MAX aggregation.
133    pub fn max(mut self, field: impl Into<String>) -> Self {
134        self.aggregations.push(Aggregation::max(field));
135        self
136    }
137
138    /// Set a filter for this query.
139    pub fn with_filter(mut self, filter: Filter) -> Self {
140        self.filter = Some(filter);
141        self
142    }
143}
144
145/// A graph query that fetches entities and their relations.
146///
147/// Note: To avoid recursive type issues with rkyv, includes are represented
148/// as a flat list with path-based nesting (e.g., "posts", "posts.comments").
149#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
150pub struct GraphQuery {
151    /// The root entity type to query.
152    pub root_entity: String,
153    /// Fields to select from the root entity.
154    pub fields: Vec<String>,
155    /// Flat list of relation includes (nested relations use dot-notation paths).
156    pub includes: Vec<RelationInclude>,
157    /// Optional filter for the root entity.
158    pub filter: Option<Filter>,
159    /// Ordering specification.
160    pub order_by: Vec<OrderSpec>,
161    /// Pagination parameters.
162    pub pagination: Option<Pagination>,
163}
164
165/// An included relation in a graph query.
166///
167/// The `path` field uses dot-notation for nested relations:
168/// - "posts" - include posts from root entity
169/// - "posts.comments" - include comments from posts
170/// - "posts.author" - include author from posts
171#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
172pub struct RelationInclude {
173    /// Dot-separated path to this relation (e.g., "posts.comments").
174    pub path: String,
175    /// Fields to select from the related entity.
176    pub fields: Vec<String>,
177    /// Optional filter for the related entities.
178    pub filter: Option<Filter>,
179    /// Ordering for the related entities.
180    pub order_by: Vec<OrderSpec>,
181    /// Pagination for the related entities.
182    pub pagination: Option<Pagination>,
183}
184
185impl RelationInclude {
186    /// Create a new include for a relation.
187    pub fn new(path: impl Into<String>) -> Self {
188        Self {
189            path: path.into(),
190            fields: vec![],
191            filter: None,
192            order_by: vec![],
193            pagination: None,
194        }
195    }
196
197    /// Set the fields to select.
198    pub fn with_fields(mut self, fields: Vec<String>) -> Self {
199        self.fields = fields;
200        self
201    }
202
203    /// Set a filter for this include.
204    pub fn with_filter(mut self, filter: Filter) -> Self {
205        self.filter = Some(filter);
206        self
207    }
208
209    /// Add ordering for this include.
210    pub fn with_order(mut self, order: OrderSpec) -> Self {
211        self.order_by.push(order);
212        self
213    }
214
215    /// Set pagination for this include.
216    pub fn with_pagination(mut self, pagination: Pagination) -> Self {
217        self.pagination = Some(pagination);
218        self
219    }
220
221    /// Get the relation name (last segment of the path).
222    pub fn relation_name(&self) -> &str {
223        self.path.rsplit('.').next().unwrap_or(&self.path)
224    }
225
226    /// Get the parent path (all segments except the last).
227    pub fn parent_path(&self) -> Option<&str> {
228        self.path.rsplit_once('.').map(|(parent, _)| parent)
229    }
230
231    /// Check if this is a top-level include (no dots in path).
232    pub fn is_top_level(&self) -> bool {
233        !self.path.contains('.')
234    }
235
236    /// Get the depth of this include (number of dots + 1).
237    pub fn depth(&self) -> usize {
238        self.path.matches('.').count() + 1
239    }
240}
241
242/// A filter condition wrapper.
243#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
244pub struct Filter {
245    /// The filter expression.
246    pub expression: FilterExpr,
247}
248
249impl Filter {
250    /// Create a filter from an expression.
251    pub fn new(expression: FilterExpr) -> Self {
252        Self { expression }
253    }
254}
255
256impl From<FilterExpr> for Filter {
257    fn from(expression: FilterExpr) -> Self {
258        Self { expression }
259    }
260}
261
262/// Filter expression for querying entities.
263///
264/// Note: This uses a flat design without recursive Box types to work with rkyv.
265/// And/Or contain Vec<FilterExpr> which is handled specially.
266#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
267pub enum FilterExpr {
268    /// Field equals value.
269    Eq { field: String, value: Value },
270    /// Field not equals value.
271    Ne { field: String, value: Value },
272    /// Field less than value.
273    Lt { field: String, value: Value },
274    /// Field less than or equal to value.
275    Le { field: String, value: Value },
276    /// Field greater than value.
277    Gt { field: String, value: Value },
278    /// Field greater than or equal to value.
279    Ge { field: String, value: Value },
280    /// Field is in a set of values.
281    In { field: String, values: Vec<Value> },
282    /// Field is not in a set of values.
283    NotIn { field: String, values: Vec<Value> },
284    /// Field is null.
285    IsNull { field: String },
286    /// Field is not null.
287    IsNotNull { field: String },
288    /// Field matches a LIKE pattern.
289    Like { field: String, pattern: String },
290    /// Field does not match a LIKE pattern.
291    NotLike { field: String, pattern: String },
292    /// All conditions must be true (flat list, single level).
293    And(Vec<SimpleFilter>),
294    /// At least one condition must be true (flat list, single level).
295    Or(Vec<SimpleFilter>),
296}
297
298/// A simple (non-compound) filter for use in And/Or expressions.
299///
300/// This flattens the filter structure to avoid recursion issues with rkyv.
301#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
302pub enum SimpleFilter {
303    /// Field equals value.
304    Eq { field: String, value: Value },
305    /// Field not equals value.
306    Ne { field: String, value: Value },
307    /// Field less than value.
308    Lt { field: String, value: Value },
309    /// Field less than or equal to value.
310    Le { field: String, value: Value },
311    /// Field greater than value.
312    Gt { field: String, value: Value },
313    /// Field greater than or equal to value.
314    Ge { field: String, value: Value },
315    /// Field is in a set of values.
316    In { field: String, values: Vec<Value> },
317    /// Field is not in a set of values.
318    NotIn { field: String, values: Vec<Value> },
319    /// Field is null.
320    IsNull { field: String },
321    /// Field is not null.
322    IsNotNull { field: String },
323    /// Field matches a LIKE pattern.
324    Like { field: String, pattern: String },
325    /// Field does not match a LIKE pattern.
326    NotLike { field: String, pattern: String },
327}
328
329impl SimpleFilter {
330    /// Create an equality filter.
331    pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Self {
332        SimpleFilter::Eq {
333            field: field.into(),
334            value: value.into(),
335        }
336    }
337
338    /// Create a not-equal filter.
339    pub fn ne(field: impl Into<String>, value: impl Into<Value>) -> Self {
340        SimpleFilter::Ne {
341            field: field.into(),
342            value: value.into(),
343        }
344    }
345
346    /// Create an IS NULL filter.
347    pub fn is_null(field: impl Into<String>) -> Self {
348        SimpleFilter::IsNull {
349            field: field.into(),
350        }
351    }
352
353    /// Create an IS NOT NULL filter.
354    pub fn is_not_null(field: impl Into<String>) -> Self {
355        SimpleFilter::IsNotNull {
356            field: field.into(),
357        }
358    }
359}
360
361impl FilterExpr {
362    /// Create an equality filter.
363    pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Self {
364        FilterExpr::Eq {
365            field: field.into(),
366            value: value.into(),
367        }
368    }
369
370    /// Create a not-equal filter.
371    pub fn ne(field: impl Into<String>, value: impl Into<Value>) -> Self {
372        FilterExpr::Ne {
373            field: field.into(),
374            value: value.into(),
375        }
376    }
377
378    /// Create a less-than filter.
379    pub fn lt(field: impl Into<String>, value: impl Into<Value>) -> Self {
380        FilterExpr::Lt {
381            field: field.into(),
382            value: value.into(),
383        }
384    }
385
386    /// Create a less-than-or-equal filter.
387    pub fn le(field: impl Into<String>, value: impl Into<Value>) -> Self {
388        FilterExpr::Le {
389            field: field.into(),
390            value: value.into(),
391        }
392    }
393
394    /// Create a greater-than filter.
395    pub fn gt(field: impl Into<String>, value: impl Into<Value>) -> Self {
396        FilterExpr::Gt {
397            field: field.into(),
398            value: value.into(),
399        }
400    }
401
402    /// Create a greater-than-or-equal filter.
403    pub fn ge(field: impl Into<String>, value: impl Into<Value>) -> Self {
404        FilterExpr::Ge {
405            field: field.into(),
406            value: value.into(),
407        }
408    }
409
410    /// Create an IN filter.
411    pub fn in_values(field: impl Into<String>, values: Vec<Value>) -> Self {
412        FilterExpr::In {
413            field: field.into(),
414            values,
415        }
416    }
417
418    /// Create a NOT IN filter.
419    pub fn not_in_values(field: impl Into<String>, values: Vec<Value>) -> Self {
420        FilterExpr::NotIn {
421            field: field.into(),
422            values,
423        }
424    }
425
426    /// Create an IS NULL filter.
427    pub fn is_null(field: impl Into<String>) -> Self {
428        FilterExpr::IsNull {
429            field: field.into(),
430        }
431    }
432
433    /// Create an IS NOT NULL filter.
434    pub fn is_not_null(field: impl Into<String>) -> Self {
435        FilterExpr::IsNotNull {
436            field: field.into(),
437        }
438    }
439
440    /// Create a LIKE filter.
441    pub fn like(field: impl Into<String>, pattern: impl Into<String>) -> Self {
442        FilterExpr::Like {
443            field: field.into(),
444            pattern: pattern.into(),
445        }
446    }
447
448    /// Create an AND filter combining multiple simple expressions.
449    pub fn and(exprs: Vec<SimpleFilter>) -> Self {
450        FilterExpr::And(exprs)
451    }
452
453    /// Create an OR filter combining multiple simple expressions.
454    pub fn or(exprs: Vec<SimpleFilter>) -> Self {
455        FilterExpr::Or(exprs)
456    }
457}
458
459/// Order specification for sorting results.
460#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
461pub struct OrderSpec {
462    /// Field to order by.
463    pub field: String,
464    /// Sort direction.
465    pub direction: OrderDirection,
466}
467
468impl OrderSpec {
469    /// Create an ascending order spec.
470    pub fn asc(field: impl Into<String>) -> Self {
471        Self {
472            field: field.into(),
473            direction: OrderDirection::Asc,
474        }
475    }
476
477    /// Create a descending order spec.
478    pub fn desc(field: impl Into<String>) -> Self {
479        Self {
480            field: field.into(),
481            direction: OrderDirection::Desc,
482        }
483    }
484}
485
486/// Sort direction.
487#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
488pub enum OrderDirection {
489    /// Ascending order.
490    Asc,
491    /// Descending order.
492    Desc,
493}
494
495/// Pagination parameters.
496#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
497pub struct Pagination {
498    /// Maximum number of results to return.
499    pub limit: u32,
500    /// Number of results to skip.
501    pub offset: u32,
502    /// Optional cursor for cursor-based pagination.
503    pub cursor: Option<Vec<u8>>,
504}
505
506impl Pagination {
507    /// Create pagination with limit and offset.
508    pub fn new(limit: u32, offset: u32) -> Self {
509        Self {
510            limit,
511            offset,
512            cursor: None,
513        }
514    }
515
516    /// Create pagination with just a limit.
517    pub fn limit(limit: u32) -> Self {
518        Self {
519            limit,
520            offset: 0,
521            cursor: None,
522        }
523    }
524
525    /// Create cursor-based pagination.
526    pub fn cursor(cursor: Vec<u8>, limit: u32) -> Self {
527        Self {
528            limit,
529            offset: 0,
530            cursor: Some(cursor),
531        }
532    }
533}
534
535impl GraphQuery {
536    /// Create a new graph query for an entity.
537    pub fn new(root_entity: impl Into<String>) -> Self {
538        Self {
539            root_entity: root_entity.into(),
540            fields: vec![],
541            includes: vec![],
542            filter: None,
543            order_by: vec![],
544            pagination: None,
545        }
546    }
547
548    /// Set the fields to select.
549    pub fn with_fields(mut self, fields: Vec<String>) -> Self {
550        self.fields = fields;
551        self
552    }
553
554    /// Add a field to select.
555    pub fn select(mut self, field: impl Into<String>) -> Self {
556        self.fields.push(field.into());
557        self
558    }
559
560    /// Add an include.
561    pub fn include(mut self, include: RelationInclude) -> Self {
562        self.includes.push(include);
563        self
564    }
565
566    /// Set a filter for this query.
567    pub fn with_filter(mut self, filter: Filter) -> Self {
568        self.filter = Some(filter);
569        self
570    }
571
572    /// Add ordering for this query.
573    pub fn with_order(mut self, order: OrderSpec) -> Self {
574        self.order_by.push(order);
575        self
576    }
577
578    /// Set pagination for this query.
579    pub fn with_pagination(mut self, pagination: Pagination) -> Self {
580        self.pagination = Some(pagination);
581        self
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    #[test]
590    fn test_simple_query() {
591        let query = GraphQuery::new("User")
592            .with_fields(vec!["id".into(), "name".into(), "email".into()])
593            .with_filter(FilterExpr::eq("active", true).into())
594            .with_order(OrderSpec::asc("name"))
595            .with_pagination(Pagination::limit(10));
596
597        assert_eq!(query.root_entity, "User");
598        assert_eq!(query.fields.len(), 3);
599        assert!(query.filter.is_some());
600        assert_eq!(query.order_by.len(), 1);
601        assert!(query.pagination.is_some());
602    }
603
604    #[test]
605    fn test_nested_query_with_path_notation() {
606        let query = GraphQuery::new("User")
607            .with_fields(vec!["id".into(), "name".into()])
608            .include(RelationInclude::new("posts").with_fields(vec!["id".into(), "title".into()]))
609            .include(
610                RelationInclude::new("posts.comments")
611                    .with_filter(FilterExpr::is_not_null("content").into()),
612            )
613            .include(RelationInclude::new("posts.author"))
614            .with_filter(FilterExpr::eq("id", Value::Uuid([1; 16])).into());
615
616        assert_eq!(query.includes.len(), 3);
617        assert_eq!(query.includes[0].path, "posts");
618        assert!(query.includes[0].is_top_level());
619        assert_eq!(query.includes[1].path, "posts.comments");
620        assert!(!query.includes[1].is_top_level());
621        assert_eq!(query.includes[1].parent_path(), Some("posts"));
622        assert_eq!(query.includes[1].depth(), 2);
623    }
624
625    #[test]
626    fn test_relation_include_helpers() {
627        let include = RelationInclude::new("posts.comments.likes");
628        assert_eq!(include.relation_name(), "likes");
629        assert_eq!(include.parent_path(), Some("posts.comments"));
630        assert_eq!(include.depth(), 3);
631        assert!(!include.is_top_level());
632
633        let top_level = RelationInclude::new("posts");
634        assert_eq!(top_level.relation_name(), "posts");
635        assert_eq!(top_level.parent_path(), None);
636        assert_eq!(top_level.depth(), 1);
637        assert!(top_level.is_top_level());
638    }
639
640    #[test]
641    fn test_complex_filter() {
642        let filter = FilterExpr::and(vec![
643            SimpleFilter::eq("status", "active"),
644            SimpleFilter::is_not_null("email"),
645        ]);
646
647        if let FilterExpr::And(exprs) = &filter {
648            assert_eq!(exprs.len(), 2);
649        } else {
650            panic!("Expected And filter");
651        }
652    }
653
654    #[test]
655    fn test_query_serialization_roundtrip() {
656        let query = GraphQuery::new("Post")
657            .with_fields(vec!["id".into(), "title".into(), "content".into()])
658            .include(RelationInclude::new("author"))
659            .include(RelationInclude::new("comments").with_pagination(Pagination::limit(10)))
660            .with_filter(
661                FilterExpr::and(vec![
662                    SimpleFilter::eq("published", true),
663                ])
664                .into(),
665            )
666            .with_order(OrderSpec::desc("created_at"))
667            .with_pagination(Pagination::new(20, 0));
668
669        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&query).unwrap();
670        let archived = rkyv::access::<ArchivedGraphQuery, rkyv::rancor::Error>(&bytes).unwrap();
671        let deserialized: GraphQuery =
672            rkyv::deserialize::<GraphQuery, rkyv::rancor::Error>(archived).unwrap();
673
674        assert_eq!(query, deserialized);
675    }
676}