Skip to main content

sift_query_core/query/scope/
group.rs

1use super::{ScopeLimits, validate_conditions, validate_predicates};
2use crate::{
3    Condition, FieldId, FieldKind, FieldRef, GroupScope, IntoScalarValue, Operator, ParamError,
4    PredicateArena, PredicatePhase, ScalarValue, SearchEntity,
5};
6use std::marker::PhantomData;
7
8/// Builder for service-owned aggregate predicates evaluated in HAVING.
9pub struct GroupScopeBuilder<E: SearchEntity> {
10    conditions: Vec<Condition>,
11    limits: ScopeLimits,
12    error: Option<ParamError>,
13    _entity: PhantomData<fn() -> E>,
14}
15
16impl<E: SearchEntity> GroupScope<E> {
17    /// Combines service-owned HAVING scopes with OR.
18    pub fn any(scopes: impl IntoIterator<Item = Self>) -> Result<Self, ParamError> {
19        Self::any_with_limits(scopes, ScopeLimits::default())
20    }
21    #[must_use]
22    pub fn new() -> GroupScopeBuilder<E> {
23        Self::builder(ScopeLimits::default())
24    }
25
26    #[must_use]
27    pub fn builder(limits: ScopeLimits) -> GroupScopeBuilder<E> {
28        GroupScopeBuilder {
29            conditions: Vec::new(),
30            limits,
31            error: None,
32            _entity: PhantomData,
33        }
34    }
35
36    pub fn all(scopes: impl IntoIterator<Item = Self>) -> Result<Self, ParamError> {
37        Self::all_with_limits(scopes, ScopeLimits::default())
38    }
39
40    pub fn all_with_limits(
41        scopes: impl IntoIterator<Item = Self>,
42        limits: ScopeLimits,
43    ) -> Result<Self, ParamError> {
44        let scopes = scopes.into_iter().collect::<Vec<_>>();
45        if scopes.is_empty() {
46            return Err(ParamError::InvalidValue {
47                field: "group scope".to_owned(),
48                reason: "scope composition must not be empty".to_owned(),
49            });
50        }
51        if scopes
52            .iter()
53            .any(|scope| scope.schema_fingerprint != E::META.schema_fingerprint)
54        {
55            return Err(ParamError::UnknownField(
56                "entity schema mismatch".to_owned(),
57            ));
58        }
59        validate_predicates(scopes.iter().map(|scope| &scope.predicate), limits)?;
60        Ok(Self {
61            predicate: PredicateArena::combine_and(scopes.into_iter().map(|scope| scope.predicate)),
62            schema_fingerprint: E::META.schema_fingerprint,
63            _entity: PhantomData,
64        })
65    }
66
67    pub fn any_with_limits(
68        scopes: impl IntoIterator<Item = Self>,
69        limits: ScopeLimits,
70    ) -> Result<Self, ParamError> {
71        let scopes = scopes.into_iter().collect::<Vec<_>>();
72        if scopes.is_empty() {
73            return Err(ParamError::InvalidValue {
74                field: "group scope".to_owned(),
75                reason: "scope composition must not be empty".to_owned(),
76            });
77        }
78        if scopes
79            .iter()
80            .any(|scope| scope.schema_fingerprint != E::META.schema_fingerprint)
81        {
82            return Err(ParamError::UnknownField(
83                "entity schema mismatch".to_owned(),
84            ));
85        }
86        validate_predicates(scopes.iter().map(|scope| &scope.predicate), limits)?;
87        Ok(Self {
88            predicate: PredicateArena::combine_or(scopes.into_iter().map(|scope| scope.predicate)),
89            schema_fingerprint: E::META.schema_fingerprint,
90            _entity: PhantomData,
91        })
92    }
93}
94
95impl<E: SearchEntity> GroupScopeBuilder<E> {
96    #[must_use]
97    pub fn eq<T: IntoScalarValue>(
98        self,
99        field: FieldRef<E, T, crate::AggregateKind>,
100        value: T,
101    ) -> Self {
102        self.push_condition(field.id(), Operator::Eq, vec![value.into_scalar_value()])
103    }
104
105    #[must_use]
106    pub fn ne<T: IntoScalarValue>(
107        self,
108        field: FieldRef<E, T, crate::AggregateKind>,
109        value: T,
110    ) -> Self {
111        self.push_condition(field.id(), Operator::Ne, vec![value.into_scalar_value()])
112    }
113
114    #[must_use]
115    pub fn gt<T: IntoScalarValue>(
116        self,
117        field: FieldRef<E, T, crate::AggregateKind>,
118        value: T,
119    ) -> Self {
120        self.push_condition(field.id(), Operator::Gt, vec![value.into_scalar_value()])
121    }
122
123    #[must_use]
124    pub fn ge<T: IntoScalarValue>(
125        self,
126        field: FieldRef<E, T, crate::AggregateKind>,
127        value: T,
128    ) -> Self {
129        self.push_condition(field.id(), Operator::Ge, vec![value.into_scalar_value()])
130    }
131
132    #[must_use]
133    pub fn lt<T: IntoScalarValue>(
134        self,
135        field: FieldRef<E, T, crate::AggregateKind>,
136        value: T,
137    ) -> Self {
138        self.push_condition(field.id(), Operator::Lt, vec![value.into_scalar_value()])
139    }
140
141    #[must_use]
142    pub fn le<T: IntoScalarValue>(
143        self,
144        field: FieldRef<E, T, crate::AggregateKind>,
145        value: T,
146    ) -> Self {
147        self.push_condition(field.id(), Operator::Le, vec![value.into_scalar_value()])
148    }
149
150    #[must_use]
151    pub fn between<T: IntoScalarValue>(
152        self,
153        field: FieldRef<E, T, crate::AggregateKind>,
154        lower: T,
155        upper: T,
156    ) -> Self {
157        self.push_condition(
158            field.id(),
159            Operator::Between,
160            vec![lower.into_scalar_value(), upper.into_scalar_value()],
161        )
162    }
163
164    #[must_use]
165    pub fn in_list<T, I>(self, field: FieldRef<E, T, crate::AggregateKind>, values: I) -> Self
166    where
167        T: IntoScalarValue,
168        I: IntoIterator<Item = T>,
169    {
170        self.push_condition(
171            field.id(),
172            Operator::InList,
173            values
174                .into_iter()
175                .map(IntoScalarValue::into_scalar_value)
176                .collect(),
177        )
178    }
179
180    fn push_condition(
181        mut self,
182        field: FieldId,
183        operator: Operator,
184        values: Vec<ScalarValue>,
185    ) -> Self {
186        if self.error.is_some() {
187            return self;
188        }
189        let Some(meta) = E::META.field(field) else {
190            self.error = Some(ParamError::UnknownField(format!("#{}", field.0)));
191            return self;
192        };
193        if meta.kind != FieldKind::Aggregate
194            || !operator.accepts_field_type(meta.value_type)
195            || values
196                .iter()
197                .any(|value| value.value_type() != operator.input_type(meta.value_type))
198        {
199            self.error = Some(ParamError::InvalidValue {
200                field: meta.public_name.to_owned(),
201                reason: "group scope requires an aggregate field with a matching type".to_owned(),
202            });
203        } else if !meta.operators.contains(operator) {
204            self.error = Some(ParamError::OperatorNotAllowed {
205                field: meta.public_name.to_owned(),
206                operator: operator.short_name(),
207            });
208        } else if !operator.accepts_arity(values.len()) {
209            self.error = Some(ParamError::WrongArity {
210                field: meta.public_name.to_owned(),
211                operator: operator.short_name(),
212                expected: operator.expected_arity(),
213                actual: values.len(),
214            });
215        } else {
216            if let Some(error) = values.iter().find_map(|value| value.validate().err()) {
217                self.error = Some(error);
218                return self;
219            }
220            self.conditions.push(Condition {
221                field,
222                operator,
223                values,
224                ignore_case: false,
225                phase: PredicatePhase::Group,
226            });
227        }
228        self
229    }
230
231    pub fn build(self) -> Result<GroupScope<E>, ParamError> {
232        if let Some(error) = self.error {
233            return Err(error);
234        }
235        if self.conditions.is_empty() {
236            return Err(ParamError::InvalidValue {
237                field: "group scope".to_owned(),
238                reason: "empty scope must use GroupScope::unrestricted() explicitly".to_owned(),
239            });
240        }
241        validate_conditions(self.conditions.iter(), self.limits)?;
242        Ok(GroupScope {
243            predicate: PredicateArena::combine_and(
244                self.conditions
245                    .into_iter()
246                    .map(PredicateArena::from_condition),
247            ),
248            schema_fingerprint: E::META.schema_fingerprint,
249            _entity: PhantomData,
250        })
251    }
252}