Skip to main content

sift_query_core/protocol/parser/
mod.rs

1use std::collections::BTreeMap;
2
3use indexmap::IndexMap;
4use indexmap::map::Entry;
5use sift_query_algorithm::BoundedCounter;
6
7mod group_expr;
8mod options;
9
10use crate::policy::names::is_name;
11use crate::{
12    CompatibilityPolicy, Condition, FieldKind, FieldMeta, Operator, Paging, ParamError,
13    PredicateArena, PredicatePhase, QueryPolicy, RawParams, ScalarValue, SearchEntity, UserQuery,
14    ValueType,
15};
16use group_expr::parse_group_expression;
17use options::{parse_order, parse_paging, parse_projection};
18
19/// Strict parser from [`RawParams`] to an entity-bound query.
20pub struct ParamParser<'policy> {
21    policy: &'policy QueryPolicy,
22}
23
24impl<'policy> ParamParser<'policy> {
25    pub fn new(policy: &'policy QueryPolicy) -> Result<Self, ParamError> {
26        Ok(Self { policy })
27    }
28
29    pub fn parse<E: SearchEntity>(&self, raw: &RawParams) -> Result<UserQuery<E>, ParamError> {
30        validate_raw_limits(raw, self.policy)?;
31        self.policy.validate_codecs::<E>()?;
32        self.policy.validate_operators::<E>()?;
33        let raw = self.policy.apply_raw_filters::<E>(raw)?;
34        validate_raw_limits(&raw, self.policy)?;
35        self.policy.validate_entity_names::<E>()?;
36        let mut controls = Controls::default();
37        let mut fields: IndexMap<ConditionKey<'_>, FieldAccumulator<'_>> = IndexMap::new();
38
39        for (key, value) in raw.entries() {
40            if controls.capture(self.policy, key, value)? {
41                continue;
42            }
43            let parsed = parse_field_key(key)?;
44            let Some(meta) = E::META.resolve_field(parsed.field) else {
45                if self.policy.compatibility() == CompatibilityPolicy::BeanSearcher4812 {
46                    continue;
47                }
48                return Err(ParamError::UnknownField(parsed.field.to_owned()));
49            };
50            let condition_key = ConditionKey {
51                group: parsed.group,
52                field: parsed.field,
53            };
54            let accumulator = match fields.entry(condition_key) {
55                Entry::Occupied(entry) => entry.into_mut(),
56                Entry::Vacant(entry) => {
57                    let key = *entry.key();
58                    entry.insert(FieldAccumulator::new(meta, key.group, key.field))
59                }
60            };
61            accumulator.capture(parsed.component, value, self.policy)?;
62        }
63
64        let predicate = self.build_predicate::<E>(fields, controls.group_expr.as_deref())?;
65        let projection = parse_projection::<E>(
66            controls.only_select.as_deref(),
67            controls.select_exclude.as_deref(),
68        )?;
69        let order = parse_order::<E>(
70            self.policy,
71            controls.sort.as_deref(),
72            controls.order.as_deref(),
73            controls.order_by.as_deref(),
74        )?;
75        let paging = parse_paging::<E>(
76            self.policy,
77            controls.page.as_deref(),
78            controls.size.as_deref(),
79            controls.offset.as_deref(),
80            controls.cursor.as_deref(),
81            &order,
82        );
83        validate_order_bind_budget::<E>(&predicate, &order, self.policy.limits())?;
84        let (paging, force_empty) = match paging {
85            Ok(paging) => (paging, false),
86            Err(ParamError::PagingOutOfRange(_))
87                if self.policy.compatibility() == CompatibilityPolicy::BeanSearcher4812 =>
88            {
89                (Paging::default(), true)
90            }
91            Err(error) => return Err(error),
92        };
93
94        Ok(UserQuery::from_parts(
95            predicate,
96            projection,
97            order,
98            paging,
99            E::META.schema_fingerprint,
100            self.policy.fingerprint(),
101            force_empty,
102        ))
103    }
104
105    fn build_predicate<'a, E: SearchEntity>(
106        &self,
107        accumulators: IndexMap<ConditionKey<'a>, FieldAccumulator<'a>>,
108        group_expr: Option<&str>,
109    ) -> Result<PredicateArena, ParamError> {
110        let limits = self.policy.limits();
111        if accumulators.len() > limits.max_conditions {
112            return Err(ParamError::TooComplex("condition count"));
113        }
114
115        let mut logical_values = BoundedCounter::new(limits.max_logical_values);
116        let mut bind_slots = 0usize;
117        let mut root_conditions = Vec::new();
118        let mut grouped_conditions: BTreeMap<String, Vec<Condition>> = BTreeMap::new();
119        for (key, accumulator) in accumulators {
120            let condition =
121                accumulator.finish::<E>(limits.max_values_per_condition, self.policy)?;
122            for value in &condition.values {
123                logical_values
124                    .try_add(value.logical_weight())
125                    .map_err(|_| ParamError::TooComplex("logical value count"))?;
126            }
127            bind_slots += match condition.operator {
128                Operator::IsNull
129                | Operator::NotNull
130                | Operator::Empty
131                | Operator::NotEmpty
132                | Operator::AlwaysTrue
133                | Operator::AlwaysFalse => 0,
134                Operator::InList | Operator::NotIn => 1,
135                _ => condition.values.len(),
136            };
137            if bind_slots > limits.max_bind_slots {
138                return Err(ParamError::TooComplex("database bind slot count"));
139            }
140            match key.group {
141                Some(group) => grouped_conditions
142                    .entry(group.to_owned())
143                    .or_default()
144                    .push(condition),
145                None => root_conditions.push(condition),
146            }
147        }
148
149        root_conditions.sort_by_key(|condition| condition.field);
150        for conditions in grouped_conditions.values_mut() {
151            conditions.sort_by_key(|condition| condition.field);
152        }
153
154        let root = conditions_to_and(root_conditions);
155        let named = grouped_conditions
156            .into_iter()
157            .map(|(name, conditions)| (name, conditions_to_and(conditions)))
158            .collect::<BTreeMap<_, _>>();
159
160        let grouped = match group_expr {
161            None => PredicateArena::combine_and(named.into_values()),
162            Some(expression) => {
163                if expression.len() > limits.max_group_expr_bytes {
164                    return Err(ParamError::TooComplex("gexpr length"));
165                }
166                parse_group_expression(
167                    expression,
168                    &named,
169                    limits.max_group_depth,
170                    limits.max_group_nodes,
171                )?
172            }
173        };
174
175        let predicate = PredicateArena::combine_and([root, grouped]);
176        predicate.partition()?;
177        Ok(predicate)
178    }
179}
180
181pub(crate) fn validate_order_bind_budget<E: SearchEntity>(
182    predicate: &PredicateArena,
183    order: &[crate::OrderBy],
184    limits: &crate::QueryLimits,
185) -> Result<(), ParamError> {
186    let extra = order
187        .iter()
188        .filter(|order| E::condition_order(order.field).is_some())
189        .count();
190    if extra == 0 {
191        return Ok(());
192    }
193    let binds = predicate
194        .nodes()
195        .iter()
196        .filter_map(|node| match node {
197            crate::PredicateNode::Condition(condition) => Some(
198                if matches!(condition.operator, Operator::InList | Operator::NotIn) {
199                    1
200                } else {
201                    condition.values.len()
202                },
203            ),
204            _ => None,
205        })
206        .sum::<usize>();
207    if binds.saturating_add(extra) > limits.max_bind_slots {
208        return Err(ParamError::TooComplex("database bind slot count"));
209    }
210    Ok(())
211}
212
213fn validate_raw_limits(raw: &RawParams, policy: &QueryPolicy) -> Result<(), ParamError> {
214    let limits = policy.limits();
215    if raw.raw_bytes() > limits.max_raw_query_bytes {
216        return Err(ParamError::QueryTooLong);
217    }
218    if raw.len() > limits.max_raw_items {
219        return Err(ParamError::TooComplex("raw parameter item count"));
220    }
221    if raw.decoded_bytes() > limits.max_decoded_bytes {
222        return Err(ParamError::TooComplex("decoded parameter bytes"));
223    }
224    if raw.entries().iter().any(|(key, value)| {
225        key.len() > limits.max_key_bytes || value.len() > limits.max_value_bytes
226    }) {
227        return Err(ParamError::TooComplex("parameter key or value length"));
228    }
229    Ok(())
230}
231
232pub(crate) fn validate_entity_names<E: SearchEntity>(
233    policy: &QueryPolicy,
234) -> Result<(), ParamError> {
235    for field in E::META.fields {
236        if !is_name(field.public_name) || field.public_name.len() > 64 {
237            return Err(ParamError::InvalidName(field.public_name.to_owned()));
238        }
239        if policy.names().all().contains(&field.public_name) {
240            return Err(ParamError::ConflictingParameters(format!(
241                "field {} conflicts with a control parameter",
242                field.public_name
243            )));
244        }
245    }
246    Ok(())
247}
248
249#[derive(Debug, Clone, Default)]
250struct Controls {
251    page: Option<String>,
252    size: Option<String>,
253    offset: Option<String>,
254    cursor: Option<String>,
255    sort: Option<String>,
256    order: Option<String>,
257    order_by: Option<String>,
258    group_expr: Option<String>,
259    only_select: Option<String>,
260    select_exclude: Option<String>,
261}
262
263impl Controls {
264    fn capture(
265        &mut self,
266        policy: &QueryPolicy,
267        key: &str,
268        value: &str,
269    ) -> Result<bool, ParamError> {
270        let names = policy.names();
271        let target = if key == names.page {
272            Some(&mut self.page)
273        } else if key == names.size {
274            Some(&mut self.size)
275        } else if key == names.offset {
276            Some(&mut self.offset)
277        } else if key == names.cursor {
278            Some(&mut self.cursor)
279        } else if key == names.sort {
280            Some(&mut self.sort)
281        } else if key == names.order {
282            Some(&mut self.order)
283        } else if key == names.order_by {
284            Some(&mut self.order_by)
285        } else if key == names.group_expr {
286            Some(&mut self.group_expr)
287        } else if key == names.only_select {
288            Some(&mut self.only_select)
289        } else if key == names.select_exclude {
290            Some(&mut self.select_exclude)
291        } else {
292            None
293        };
294        let Some(target) = target else {
295            return Ok(false);
296        };
297        if target.is_some() {
298            return Err(ParamError::DuplicateParameter(key.to_owned()));
299        }
300        *target = Some(value.to_owned());
301        Ok(true)
302    }
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
306struct ConditionKey<'a> {
307    group: Option<&'a str>,
308    field: &'a str,
309}
310
311#[derive(Clone, Copy)]
312enum FieldComponent {
313    Value,
314    Indexed(usize),
315    Operator,
316    IgnoreCase,
317}
318
319struct ParsedFieldKey<'a> {
320    group: Option<&'a str>,
321    field: &'a str,
322    component: FieldComponent,
323}
324
325fn parse_field_key(key: &str) -> Result<ParsedFieldKey<'_>, ParamError> {
326    let (base, component) = if let Some(base) = key.strip_suffix("-op") {
327        (base, FieldComponent::Operator)
328    } else if let Some(base) = key.strip_suffix("-ic") {
329        (base, FieldComponent::IgnoreCase)
330    } else if let Some((base, suffix)) = key.rsplit_once('-') {
331        if suffix.bytes().all(|byte| byte.is_ascii_digit()) {
332            let index = suffix
333                .parse()
334                .map_err(|_| ParamError::InvalidName(key.to_owned()))?;
335            if suffix.len() > 1 && suffix.starts_with('0') {
336                return Err(ParamError::InvalidName(key.to_owned()));
337            }
338            (base, FieldComponent::Indexed(index))
339        } else {
340            return Err(ParamError::UnknownParameter(key.to_owned()));
341        }
342    } else {
343        (key, FieldComponent::Value)
344    };
345
346    let (group, field) = if let Some((group, field)) = base.split_once('.') {
347        if field.contains('.') {
348            return Err(ParamError::InvalidName(key.to_owned()));
349        }
350        (Some(group), field)
351    } else {
352        (None, base)
353    };
354    if field.len() > 64
355        || !is_name(field)
356        || group
357            .as_ref()
358            .is_some_and(|name| name.len() > 64 || !is_name(name))
359    {
360        return Err(ParamError::InvalidName(key.to_owned()));
361    }
362    Ok(ParsedFieldKey {
363        group,
364        field,
365        component,
366    })
367}
368
369struct FieldAccumulator<'a> {
370    meta: &'static FieldMeta,
371    group: Option<&'a str>,
372    field: &'a str,
373    repeated: Vec<String>,
374    indexed: BTreeMap<usize, String>,
375    operator: Option<Operator>,
376    ignore_case: Option<bool>,
377}
378
379impl<'a> FieldAccumulator<'a> {
380    fn new(meta: &'static FieldMeta, group: Option<&'a str>, field: &'a str) -> Self {
381        Self {
382            meta,
383            group,
384            field,
385            repeated: Vec::new(),
386            indexed: BTreeMap::new(),
387            operator: None,
388            ignore_case: None,
389        }
390    }
391
392    fn display(&self) -> String {
393        format_display(self.group, self.field)
394    }
395
396    fn capture(
397        &mut self,
398        component: FieldComponent,
399        value: &str,
400        policy: &QueryPolicy,
401    ) -> Result<(), ParamError> {
402        match component {
403            FieldComponent::Value => {
404                if !self.indexed.is_empty() {
405                    return Err(ParamError::ConflictingParameters(self.display()));
406                }
407                self.repeated.push(value.to_owned());
408            }
409            FieldComponent::Indexed(index) => {
410                if !self.repeated.is_empty() {
411                    return Err(ParamError::ConflictingParameters(self.display()));
412                }
413                if self.indexed.insert(index, value.to_owned()).is_some() {
414                    return Err(ParamError::DuplicateParameter(format!(
415                        "{}-{index}",
416                        self.display()
417                    )));
418                }
419            }
420            FieldComponent::Operator => {
421                let operator = Operator::parse(value)
422                    .or_else(|| {
423                        policy
424                            .registered_operator(value)
425                            .map(|registration| Operator::Custom(registration.operator))
426                    })
427                    .ok_or_else(|| ParamError::InvalidValue {
428                        field: self.display(),
429                        reason: format!("unknown operator {value}"),
430                    })?;
431                if self.operator.replace(operator).is_some() {
432                    return Err(ParamError::DuplicateParameter(format!(
433                        "{}-op",
434                        self.display()
435                    )));
436                }
437            }
438            FieldComponent::IgnoreCase => {
439                let ignore_case = match value {
440                    "true" => true,
441                    "false" => false,
442                    _ => {
443                        return Err(ParamError::InvalidValue {
444                            field: self.display(),
445                            reason: "ignore-case must be true or false".to_owned(),
446                        });
447                    }
448                };
449                if self.ignore_case.replace(ignore_case).is_some() {
450                    return Err(ParamError::DuplicateParameter(format!(
451                        "{}-ic",
452                        self.display()
453                    )));
454                }
455            }
456        }
457        Ok(())
458    }
459
460    fn finish<E: SearchEntity>(
461        self,
462        max_values: usize,
463        policy: &QueryPolicy,
464    ) -> Result<Condition, ParamError> {
465        let group = self.group;
466        let field = self.field;
467        if !self.meta.filterable {
468            return Err(ParamError::NotFilterable(format_display(group, field)));
469        }
470        let operator = self.operator.unwrap_or(Operator::Eq);
471        let allowed = match operator {
472            Operator::Custom(custom) => {
473                E::custom_operator_names(self.meta.id).contains(&custom.name())
474                    && policy
475                        .registered_operator(custom.name())
476                        .is_some_and(|item| {
477                            item.operator == custom && item.field_type == self.meta.value_type
478                        })
479            }
480            builtin => self.meta.operators.contains(builtin),
481        };
482        if !allowed {
483            return Err(ParamError::OperatorNotAllowed {
484                field: format_display(group, field),
485                operator: operator.short_name(),
486            });
487        }
488        let raw_values = if self.indexed.is_empty() {
489            self.repeated
490        } else {
491            let mut values = Vec::with_capacity(self.indexed.len());
492            for (expected, (actual, value)) in self.indexed.into_iter().enumerate() {
493                if actual != expected {
494                    return Err(ParamError::NonContiguousIndex {
495                        field: format_display(group, field),
496                        expected,
497                    });
498                }
499                values.push(value);
500            }
501            values
502        };
503        if raw_values.len() > max_values {
504            return Err(ParamError::TooComplex("values per condition"));
505        }
506        if !operator.accepts_arity(raw_values.len()) {
507            return Err(ParamError::WrongArity {
508                field: format_display(group, field),
509                operator: operator.short_name(),
510                expected: operator.expected_arity(),
511                actual: raw_values.len(),
512            });
513        }
514        let ignore_case = self.ignore_case.unwrap_or(false);
515        if ignore_case
516            && (self.meta.value_type != ValueType::Text || !operator.supports_ignore_case())
517        {
518            return Err(ParamError::IgnoreCaseNotAllowed(format_display(
519                group, field,
520            )));
521        }
522        let input_type = operator.input_type(self.meta.value_type);
523        let input_field = FieldMeta {
524            value_type: input_type,
525            ..*self.meta
526        };
527        let values = raw_values
528            .iter()
529            .map(|value| {
530                if let Some(codec) = policy.codec_for::<E>(self.meta) {
531                    let parsed = codec.parse(&input_field, value).map_err(|reason| {
532                        ParamError::InvalidValue {
533                            field: format_display(group, field),
534                            reason,
535                        }
536                    })?;
537                    if parsed.value_type() != input_type {
538                        return Err(ParamError::InvalidValue {
539                            field: format_display(group, field),
540                            reason: "codec returned a value with the wrong field type".to_owned(),
541                        });
542                    }
543                    if matches!(&parsed, ScalarValue::F32(value) if !value.is_finite())
544                        || matches!(&parsed, ScalarValue::F64(value) if !value.is_finite())
545                    {
546                        return Err(ParamError::InvalidValue {
547                            field: format_display(group, field),
548                            reason: "codec returned a non-finite value".to_owned(),
549                        });
550                    }
551                    Ok(parsed)
552                } else {
553                    ScalarValue::parse(input_type, value, self.field).map_err(|error| match error {
554                        ParamError::InvalidValue { reason, .. } => ParamError::InvalidValue {
555                            field: format_display(group, field),
556                            reason,
557                        },
558                        other => other,
559                    })
560                }
561            })
562            .collect::<Result<Vec<_>, _>>()?;
563        if values
564            .iter()
565            .map(ScalarValue::logical_weight)
566            .sum::<usize>()
567            > max_values
568        {
569            return Err(ParamError::TooComplex("values per condition"));
570        }
571        for value in &values {
572            value.validate()?;
573        }
574        Ok(Condition {
575            field: self.meta.id,
576            operator,
577            values,
578            ignore_case,
579            phase: if self.meta.kind == FieldKind::Aggregate {
580                PredicatePhase::Group
581            } else {
582                PredicatePhase::Row
583            },
584        })
585    }
586}
587
588fn format_display(group: Option<&str>, field: &str) -> String {
589    group.map_or_else(|| field.to_owned(), |group| format!("{group}.{field}"))
590}
591
592fn conditions_to_and(conditions: Vec<Condition>) -> PredicateArena {
593    PredicateArena::combine_and(conditions.into_iter().map(PredicateArena::from_condition))
594}