sift_query_core/query/builder/
mod.rs1use 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
18pub 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 #[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 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}