Skip to main content

sift_query_core/query/builder/
mod.rs

1use std::collections::BTreeSet;
2use std::marker::PhantomData;
3
4use crate::{
5    Condition, Cursor, FieldId, FieldKind, FieldRef, FilterExpr, NullOrder, Operator, OrderBy,
6    Paging, PagingMode, ParamError, PredicateArena, PredicatePhase, Projection, QueryPolicy,
7    ScalarValue, SearchEntity, SortOrder, UserQuery,
8};
9
10mod operators;
11
12enum ProjectionBuilder {
13    All,
14    Only(Vec<FieldId>),
15    Exclude(Vec<FieldId>),
16}
17
18/// Programmatic builder for a validated [`UserQuery`].
19pub struct QueryBuilder<'policy, E: SearchEntity> {
20    pub(crate) policy: &'policy QueryPolicy,
21    conditions: Vec<Condition>,
22    expressions: Vec<FilterExpr<E>>,
23    projection: ProjectionBuilder,
24    order: Vec<OrderBy>,
25    paging: Paging,
26    error: Option<ParamError>,
27    _entity: PhantomData<fn() -> E>,
28}
29
30impl<'policy, E: SearchEntity> QueryBuilder<'policy, E> {
31    #[must_use]
32    pub fn new(policy: &'policy QueryPolicy) -> Self {
33        Self {
34            policy,
35            conditions: Vec::new(),
36            expressions: Vec::new(),
37            projection: ProjectionBuilder::All,
38            order: Vec::new(),
39            paging: Paging::default(),
40            error: None,
41            _entity: PhantomData,
42        }
43    }
44
45    /// Adds a pre-built nested predicate. Multiple calls are combined with AND.
46    #[must_use]
47    pub fn filter(mut self, expression: FilterExpr<E>) -> Self {
48        if self.error.is_none() {
49            self.expressions.push(expression);
50        }
51        self
52    }
53
54    #[must_use]
55    pub fn only_select<T, K>(mut self, field: FieldRef<E, T, K>) -> Self {
56        self.push_projection(field.id(), false);
57        self
58    }
59
60    #[must_use]
61    pub fn exclude<T, K>(mut self, field: FieldRef<E, T, K>) -> Self {
62        self.push_projection(field.id(), true);
63        self
64    }
65
66    #[must_use]
67    pub fn order_by<T, K>(mut self, field: FieldRef<E, T, K>, direction: SortOrder) -> Self {
68        if self.error.is_some() {
69            return self;
70        }
71        let Some(meta) = E::META.field(field.id()) else {
72            self.error = Some(ParamError::UnknownField(format!("#{}", field.id().0)));
73            return self;
74        };
75        if !meta.sortable {
76            self.error = Some(ParamError::NotSortable(meta.public_name.to_owned()));
77        } else if self.order.iter().any(|item| item.field == field.id()) {
78            self.error = Some(ParamError::DuplicateParameter(format!(
79                "sort {}",
80                meta.public_name
81            )));
82        } else {
83            self.order.push(OrderBy {
84                field: field.id(),
85                direction,
86                nulls: NullOrder::from(direction),
87            });
88        }
89        self
90    }
91
92    #[must_use]
93    pub fn page(mut self, page: u64, size: u32) -> Self {
94        if self.error.is_some() {
95            return self;
96        }
97        let Some(offset) = page
98            .checked_sub(1)
99            .and_then(|page| page.checked_mul(u64::from(size)))
100        else {
101            self.error = Some(ParamError::PagingOutOfRange("page".to_owned()));
102            return self;
103        };
104        self.set_paging(PagingMode::Page { page }, size, offset, true, false);
105        self
106    }
107
108    #[must_use]
109    pub fn offset(mut self, offset: u64, size: u32) -> Self {
110        if self.error.is_some() {
111            return self;
112        }
113        self.set_paging(PagingMode::Offset, size, offset, false, true);
114        self
115    }
116
117    #[must_use]
118    pub fn cursor(mut self, cursor: Cursor, size: u32) -> Self {
119        if self.error.is_some() {
120            return self;
121        }
122        let max_size = self
123            .policy
124            .limits()
125            .max_page_size
126            .min(E::META.limits.max_page_size);
127        if size > max_size || cursor.values().len() != self.final_order_len() {
128            self.error = Some(ParamError::PagingOutOfRange("cursor".to_owned()));
129        } else {
130            self.paging = Paging {
131                mode: PagingMode::Cursor(Some(cursor)),
132                size,
133                offset: 0,
134                explicit_page: false,
135                explicit_size: true,
136                explicit_offset: false,
137                explicit_cursor: true,
138            };
139        }
140        self
141    }
142
143    pub fn build(mut self) -> Result<UserQuery<E>, ParamError> {
144        self.policy.validate_codecs::<E>()?;
145        self.policy.validate_operators::<E>()?;
146        if let Some(error) = self.error {
147            return Err(error);
148        }
149        crate::policy::budget::bounded_sum(
150            std::iter::once(self.conditions.len()).chain(
151                self.expressions
152                    .iter()
153                    .map(|expression| expression.condition_count),
154            ),
155            self.policy.limits().max_conditions,
156            "condition count",
157        )?;
158        crate::policy::budget::bounded_sum(
159            self.conditions
160                .iter()
161                .flat_map(|condition| &condition.values)
162                .map(ScalarValue::logical_weight)
163                .chain(
164                    self.expressions
165                        .iter()
166                        .map(|expression| expression.logical_values),
167                ),
168            self.policy.limits().max_logical_values,
169            "logical value count",
170        )?;
171        if self.order.len() > self.policy.limits().max_sort_fields {
172            return Err(ParamError::TooComplex("sort field count"));
173        }
174        if self.order.is_empty() {
175            self.order
176                .extend(E::META.default_order.iter().copied().map(OrderBy::from));
177        }
178        if E::META.stable_order.is_empty() {
179            return Err(ParamError::UnstablePagination);
180        }
181        for stable in E::META.stable_order {
182            if !self.order.iter().any(|item| item.field == stable.field) {
183                self.order.push((*stable).into());
184            }
185        }
186        let predicate = PredicateArena::combine_and(
187            self.conditions
188                .into_iter()
189                .map(PredicateArena::from_condition)
190                .chain(
191                    self.expressions
192                        .into_iter()
193                        .map(|expression| expression.arena),
194                ),
195        );
196        predicate.partition()?;
197        crate::protocol::parser::validate_order_bind_budget::<E>(
198            &predicate,
199            &self.order,
200            self.policy.limits(),
201        )?;
202        for node in predicate.nodes() {
203            if let crate::PredicateNode::Condition(condition) = node
204                && condition
205                    .values
206                    .iter()
207                    .map(ScalarValue::logical_weight)
208                    .sum::<usize>()
209                    > self.policy.limits().max_values_per_condition
210            {
211                return Err(ParamError::TooComplex("values per condition"));
212            }
213        }
214        let projection = resolve_projection::<E>(self.projection)?;
215        if let PagingMode::Cursor(Some(cursor)) = &self.paging.mode {
216            for (order, value) in self.order.iter().zip(cursor.values()) {
217                if E::cursor_key_non_null(order.field) && matches!(value, crate::CursorValue::Null)
218                {
219                    return Err(ParamError::InvalidValue {
220                        field: "cursor".to_owned(),
221                        reason: "non-null cursor key cannot contain NULL".to_owned(),
222                    });
223                }
224            }
225        }
226        Ok(UserQuery::from_parts(
227            predicate,
228            projection,
229            self.order,
230            self.paging,
231            E::META.schema_fingerprint,
232            self.policy.fingerprint(),
233            false,
234        ))
235    }
236
237    fn push_condition(
238        mut self,
239        field: FieldId,
240        operator: Operator,
241        values: Vec<ScalarValue>,
242        ignore_case: bool,
243    ) -> Self {
244        if self.error.is_some() {
245            return self;
246        }
247        let Some(meta) = E::META.field(field) else {
248            self.error = Some(ParamError::UnknownField(format!("#{}", field.0)));
249            return self;
250        };
251        if !meta.filterable {
252            self.error = Some(ParamError::NotFilterable(meta.public_name.to_owned()));
253        } else if !matches!(operator, Operator::Custom(_)) && !meta.operators.contains(operator) {
254            self.error = Some(ParamError::OperatorNotAllowed {
255                field: meta.public_name.to_owned(),
256                operator: operator.short_name(),
257            });
258        } else if !operator.accepts_arity(values.len()) {
259            self.error = Some(ParamError::WrongArity {
260                field: meta.public_name.to_owned(),
261                operator: operator.short_name(),
262                expected: operator.expected_arity(),
263                actual: values.len(),
264            });
265        } else if values
266            .iter()
267            .map(ScalarValue::logical_weight)
268            .sum::<usize>()
269            > self.policy.limits().max_values_per_condition
270        {
271            self.error = Some(ParamError::TooComplex("values per condition"));
272        } else if !operator.accepts_field_type(meta.value_type)
273            || values
274                .iter()
275                .any(|value| value.value_type() != operator.input_type(meta.value_type))
276        {
277            self.error = Some(ParamError::InvalidValue {
278                field: meta.public_name.to_owned(),
279                reason: "programmatic value type does not match field".to_owned(),
280            });
281        } else if ignore_case
282            && (meta.value_type != crate::ValueType::Text || !operator.supports_ignore_case())
283        {
284            self.error = Some(ParamError::IgnoreCaseNotAllowed(
285                meta.public_name.to_owned(),
286            ));
287        } else {
288            if let Some(error) = values.iter().find_map(|value| value.validate().err()) {
289                self.error = Some(error);
290                return self;
291            }
292            self.conditions.push(Condition {
293                field,
294                operator,
295                values,
296                ignore_case,
297                phase: if meta.kind == FieldKind::Aggregate {
298                    PredicatePhase::Group
299                } else {
300                    PredicatePhase::Row
301                },
302            });
303        }
304        self
305    }
306
307    fn push_projection(&mut self, field: FieldId, excluding: bool) {
308        if self.error.is_some() {
309            return;
310        }
311        let Some(meta) = E::META.field(field) else {
312            self.error = Some(ParamError::UnknownField(format!("#{}", field.0)));
313            return;
314        };
315        if !meta.selectable || meta.result_alias.is_none() {
316            self.error = Some(ParamError::NotSelectable(meta.public_name.to_owned()));
317            return;
318        }
319        // The first call picks the projection mode; later calls append to it.
320        // Resolving `All` before taking a mutable borrow keeps the match below
321        // down to one reachable arm plus the conflict arm, with no
322        // `unreachable!` to justify.
323        if matches!(self.projection, ProjectionBuilder::All) {
324            self.projection = if excluding {
325                ProjectionBuilder::Exclude(Vec::new())
326            } else {
327                ProjectionBuilder::Only(Vec::new())
328            };
329        }
330        let ((ProjectionBuilder::Only(fields), false) | (ProjectionBuilder::Exclude(fields), true)) =
331            (&mut self.projection, excluding)
332        else {
333            self.error = Some(ParamError::ConflictingParameters(
334                "only-select and exclude projection".to_owned(),
335            ));
336            return;
337        };
338        if fields.contains(&field) {
339            self.error = Some(ParamError::DuplicateParameter(format!(
340                "projection {}",
341                meta.public_name
342            )));
343        } else {
344            fields.push(field);
345        }
346    }
347
348    fn set_paging(
349        &mut self,
350        mode: PagingMode,
351        size: u32,
352        offset: u64,
353        explicit_page: bool,
354        explicit_offset: bool,
355    ) {
356        let max_size = self
357            .policy
358            .limits()
359            .max_page_size
360            .min(E::META.limits.max_page_size);
361        let max_offset = self
362            .policy
363            .limits()
364            .max_offset
365            .min(E::META.limits.max_offset);
366        if size > max_size || offset > max_offset {
367            self.error = Some(ParamError::PagingOutOfRange("page".to_owned()));
368        } else {
369            self.paging = Paging {
370                mode,
371                size,
372                offset,
373                explicit_page,
374                explicit_size: true,
375                explicit_offset,
376                explicit_cursor: false,
377            };
378        }
379    }
380
381    fn final_order_len(&self) -> usize {
382        let requested = if self.order.is_empty() {
383            E::META
384                .default_order
385                .iter()
386                .map(|item| item.field)
387                .collect()
388        } else {
389            self.order.iter().map(|item| item.field).collect::<Vec<_>>()
390        };
391        let missing_stable = E::META
392            .stable_order
393            .iter()
394            .filter(|stable| !requested.contains(&stable.field))
395            .count();
396        requested.len() + missing_stable
397    }
398}
399
400fn resolve_projection<E: SearchEntity>(
401    projection: ProjectionBuilder,
402) -> Result<Projection, ParamError> {
403    let fields = match projection {
404        ProjectionBuilder::All => return Ok(Projection::All),
405        ProjectionBuilder::Only(fields) => {
406            let included = fields.into_iter().collect::<BTreeSet<_>>();
407            E::META
408                .fields
409                .iter()
410                .filter(|field| included.contains(&field.id))
411                .map(|field| field.id)
412                .collect::<Vec<_>>()
413        }
414        ProjectionBuilder::Exclude(fields) => {
415            let excluded = fields.into_iter().collect::<BTreeSet<_>>();
416            E::META
417                .fields
418                .iter()
419                .filter(|field| {
420                    field.selectable
421                        && field.result_alias.is_some()
422                        && !excluded.contains(&field.id)
423                })
424                .map(|field| field.id)
425                .collect::<Vec<_>>()
426        }
427    };
428    if fields.is_empty() {
429        Err(ParamError::EmptyProjection)
430    } else {
431        Ok(Projection::Fields(fields))
432    }
433}