Skip to main content

parse_rust_storage/
query.rs

1//! The query AST adapters lower, and the update AST they apply.
2//!
3//! Not a Mongo query document. A Mongo query document is already lowered, and handing one to the
4//! Postgres adapter would mean writing a Mongo-query interpreter in SQL, which is roughly the
5//! shape upstream ended up with.
6//!
7//! **Anything outside the supported vocabulary is an error, never silently ignored**, because a
8//! dropped constraint broadens the result set, which is an authorization failure rather than a
9//! missing feature. That rule is enforced at parse time by [`Comparison::from_operator`] returning
10//! an error for an unknown operator, so there is no code path where an unrecognised constraint
11//! reaches an adapter.
12//!
13//! 0.2.0 turned the flat constraint list into a tree. That was not a generalization for its own
14//! sake: pointer permissions compose **disjunctively** across fields
15//! (`DatabaseController.js:1815`), so a class permitting `{find: {pointerFields: ['owner',
16//! 'editor']}}` cannot be expressed without `$or`. A flat list would have forced either a wrong
17//! conjunction, which under-returns, or no constraint at all, which is the breach.
18
19use indexmap::IndexMap;
20
21use parse_rust_core::{ParseError, ParseValue};
22
23/// How one field is compared to one value.
24#[derive(Debug, Clone)]
25pub enum Comparison {
26    /// Bare equality: `{"field": value}`. Lowers to the value itself, with no operator wrapper.
27    Equal(ParseValue),
28    /// Explicit `$eq`: `{"field": {"$eq": value}}`.
29    ///
30    /// **Deliberately not the same variant as [`Comparison::Equal`], because the two do not lower
31    /// the same way.** Bare equality puts the value directly under the field, so it cannot share
32    /// the field with another operator: `{"n": 5}` and `{"n": {"$gt": 1}}` are two whole documents
33    /// for one key and merging them means one overwrites the other. `$eq` is an operator like any
34    /// other and composes, which is the entire reason upstream's `replaceEquality` rewrites a
35    /// mixed constraint into this form rather than leaving it bare (`RestQuery.js:828-849`).
36    ///
37    /// Folding the two together compiles, passes a round-trip test, and silently undoes that
38    /// rewrite one layer below where it was applied.
39    ///
40    /// It also must not pin an objectId. `ParsedWhere::pinned_object_id` reads the shorthand form
41    /// only, matching upstream's direct `query.objectId` read (`DatabaseController.js:1838`), and
42    /// a separate variant is what keeps `{"objectId": {"$eq": "x"}}` out of that path.
43    EqualOperator(ParseValue),
44    NotEqual(ParseValue),
45    GreaterThan(ParseValue),
46    GreaterThanOrEqual(ParseValue),
47    LessThan(ParseValue),
48    LessThanOrEqual(ParseValue),
49    In(Vec<ParseValue>),
50    NotIn(Vec<ParseValue>),
51    Exists(bool),
52    /// `$all`: the array field contains every listed value.
53    All(Vec<ParseValue>),
54    /// `$regex`, with `$options` folded in.
55    ///
56    /// The two arrive as separate keys and upstream keeps them separate all the way down, relying
57    /// on reverse-alphabetical key iteration so that `$regex` is handled before `$options`
58    /// (`MongoTransform.js:670-675`). Folding them into one variant here removes the ordering
59    /// dependency without changing what reaches the backend, because a lone `$options` is
60    /// meaningless and a lone `$regex` gets `None`.
61    Regex {
62        pattern: String,
63        options: Option<String>,
64    },
65}
66
67impl Comparison {
68    /// Map a Parse `$` operator onto a comparison.
69    ///
70    /// Returns `INVALID_QUERY` for anything unsupported. That is the whole point: the alternative,
71    /// ignoring it, returns more rows than the caller asked for.
72    ///
73    /// `$regex` and `$options` are not handled here, because they are two keys producing one
74    /// comparison. The caller assembles them; see `parse_operators` in `parse-rust-rest`.
75    pub fn from_operator(op: &str, value: ParseValue) -> Result<Self, ParseError> {
76        Ok(match op {
77            // Upstream accepts an explicit `$eq` alongside the bare-value form
78            // (`MongoTransform.js:684-696`), and `replaceEquality` synthesizes one, so this arm is
79            // reachable both from a client that wrote `{"$eq": v}` and from a mixed constraint
80            // that was rewritten into one. Either way it stays an operator all the way down.
81            "$eq" => Comparison::EqualOperator(value),
82            "$ne" => Comparison::NotEqual(value),
83            "$gt" => Comparison::GreaterThan(value),
84            "$gte" => Comparison::GreaterThanOrEqual(value),
85            "$lt" => Comparison::LessThan(value),
86            "$lte" => Comparison::LessThanOrEqual(value),
87            "$in" | "$nin" | "$all" => {
88                let items = match value {
89                    ParseValue::Array(items) => items,
90                    _ => {
91                        return Err(ParseError::invalid_query(format!(
92                            "bad {op} value: expected an array"
93                        )))
94                    }
95                };
96                match op {
97                    "$in" => Comparison::In(items),
98                    "$nin" => Comparison::NotIn(items),
99                    _ => Comparison::All(items),
100                }
101            }
102            "$exists" => match value {
103                ParseValue::Bool(b) => Comparison::Exists(b),
104                _ => {
105                    return Err(ParseError::invalid_query(
106                        "bad $exists value: expected a boolean".to_string(),
107                    ))
108                }
109            },
110            other => {
111                return Err(ParseError::invalid_query(format!(
112                    "unsupported query operator: {other}"
113                )))
114            }
115        })
116    }
117}
118
119/// One field, one comparison.
120#[derive(Debug, Clone)]
121pub struct Constraint {
122    pub field: String,
123    pub comparison: Comparison,
124}
125
126impl Constraint {
127    pub fn equal(field: impl Into<String>, value: ParseValue) -> Self {
128        Self {
129            field: field.into(),
130            comparison: Comparison::Equal(value),
131        }
132    }
133
134    pub fn one_of(field: impl Into<String>, values: Vec<ParseValue>) -> Self {
135        Self {
136            field: field.into(),
137            comparison: Comparison::In(values),
138        }
139    }
140}
141
142/// One element of a query. Elements of a [`Query`] are conjoined.
143#[derive(Debug, Clone)]
144pub enum Clause {
145    Field(Constraint),
146    /// At least one sub-query matches.
147    Or(Vec<Query>),
148    /// Every sub-query matches. Distinct from putting the clauses side by side, because a nested
149    /// `$and` can carry two constraints on the same field without them merging.
150    And(Vec<Query>),
151    /// No sub-query matches.
152    Nor(Vec<Query>),
153}
154
155/// A conjunction of clauses. An empty query matches everything.
156#[derive(Debug, Clone, Default)]
157pub struct Query {
158    pub clauses: Vec<Clause>,
159}
160
161impl Query {
162    pub fn new() -> Self {
163        Self::default()
164    }
165
166    pub fn is_empty(&self) -> bool {
167        self.clauses.is_empty()
168    }
169
170    pub fn push(&mut self, clause: Clause) {
171        self.clauses.push(clause);
172    }
173
174    pub fn push_constraint(&mut self, constraint: Constraint) {
175        self.clauses.push(Clause::Field(constraint));
176    }
177
178    /// Conjoin another query into this one.
179    ///
180    /// Splicing the other query's clauses in rather than nesting an `And` keeps the common case
181    /// flat, which matters because the compiled Mongo document is snapshot-compared. Nesting
182    /// would be equally correct and would change every fixture.
183    ///
184    /// **Only safe when the two queries cannot name the same field.** Two constraints on one
185    /// field spliced side by side either merge, which silently drops one, or collide and fail;
186    /// [`Query::conjoin`] is the one to reach for when a server-imposed predicate meets a
187    /// client-supplied one.
188    pub fn extend(&mut self, other: Query) {
189        self.clauses.extend(other.clauses);
190    }
191
192    /// Conjoin another query into this one **without letting either predicate be lost**.
193    ///
194    /// A field the receiver already constrains at top level is nested under `And` instead of
195    /// spliced in beside the existing constraint. Upstream does the same thing and for the same
196    /// reason: `addPointerPermissions` tests `hasOwnProperty(query, key)` and falls back to
197    /// `reduceAndOperation({$and: [queryClause, query]})` when it holds
198    /// (`DatabaseController.js:1807-1811`).
199    ///
200    /// Splicing instead is not a cosmetic difference. A client that queries `owner` explicitly on
201    /// a class whose `find` CLP names `owner` as a pointer field produces two equalities on one
202    /// field, which `merge_constraint` reports as `INVALID_QUERY` rather than answering the query.
203    ///
204    /// Top level only, matching `hasOwnProperty`: a field named inside an `$or` is a different
205    /// key as far as the compiled document is concerned and cannot collide.
206    pub fn conjoin(&mut self, other: Query) {
207        let mut nested = Vec::new();
208        for clause in other.clauses {
209            match &clause {
210                Clause::Field(constraint) if self.constrains_field(&constraint.field) => {
211                    nested.push(Query {
212                        clauses: vec![clause],
213                    });
214                }
215                _ => self.clauses.push(clause),
216            }
217        }
218        if !nested.is_empty() {
219            self.clauses.push(Clause::And(nested));
220        }
221    }
222
223    /// Does a top-level clause constrain this field?
224    pub fn constrains_field(&self, field: &str) -> bool {
225        self.top_level_constraints().any(|c| c.field == field)
226    }
227
228    /// A disjunction of alternatives, simplified the way `reduceOrOperation` does
229    /// (`DatabaseController.js:1657-1724`): an `$or` with a single element collapses into that
230    /// element rather than staying wrapped.
231    pub fn any_of(alternatives: Vec<Query>) -> Query {
232        let mut alternatives: Vec<Query> =
233            alternatives.into_iter().filter(|q| !q.is_empty()).collect();
234        match alternatives.len() {
235            0 => Query::new(),
236            1 => alternatives.remove(0),
237            _ => {
238                let mut q = Query::new();
239                q.push(Clause::Or(alternatives));
240                q
241            }
242        }
243    }
244
245    pub fn from_constraints(constraints: Vec<Constraint>) -> Query {
246        Query {
247            clauses: constraints.into_iter().map(Clause::Field).collect(),
248        }
249    }
250
251    /// Every top-level field constraint, ignoring nested logical clauses.
252    ///
253    /// Used by the pieces that need to know whether a query is pinned to one objectId. It is
254    /// deliberately not a general "find the constraint on field X", because inside an `$or` no
255    /// such thing exists.
256    pub fn top_level_constraints(&self) -> impl Iterator<Item = &Constraint> {
257        self.clauses.iter().filter_map(|c| match c {
258            Clause::Field(f) => Some(f),
259            _ => None,
260        })
261    }
262}
263
264impl From<Vec<Constraint>> for Query {
265    fn from(constraints: Vec<Constraint>) -> Self {
266        Query::from_constraints(constraints)
267    }
268}
269
270/// What one field of an update does.
271///
272/// 0.1.0 modelled an update as a row of literal values, which is why `{"__op":"Increment"}`
273/// round-tripped into storage as an object with an `__op` key. Modelling the op set explicitly
274/// makes "the adapter forgot to handle Increment" a missing match arm.
275#[derive(Debug, Clone)]
276pub enum UpdateValue {
277    /// `$setOnInsert`: set the field only if this operation inserts the row
278    /// (`MongoTransform.js:993-998`).
279    ///
280    /// Carried because upstream carries it, not because a REST write can produce one:
281    /// `getObjectType` has no arm for `SetOnInsert` and throws before the write path is reached
282    /// (`SchemaController.js:1652-1653`). See `infer_op_type`.
283    SetOnInsert(ParseValue),
284    /// Replace the field.
285    Set(ParseValue),
286    /// `$inc`.
287    Increment(f64),
288    /// `$push` with `$each`. Duplicates allowed.
289    Add(Vec<ParseValue>),
290    /// `$addToSet` with `$each`.
291    AddUnique(Vec<ParseValue>),
292    /// `$pullAll`. Note the shape difference from `Add`: no `$each` wrapper
293    /// (`MongoTransform.js:1024-1029`).
294    Remove(Vec<ParseValue>),
295    /// `$unset`.
296    Unset,
297}
298
299impl UpdateValue {
300    /// Does applying this need the post-image read back?
301    ///
302    /// Only ops do. A `Set` tells the client nothing it did not already know, which is why
303    /// `_sanitizeDatabaseResult` returns only op keys (`DatabaseController.js:2129-2157`).
304    pub fn echoes_result(&self) -> bool {
305        !matches!(self, UpdateValue::Set(_) | UpdateValue::Unset)
306    }
307}
308
309/// An update: ordered, because the compiled document is snapshot-compared.
310pub type Update = IndexMap<String, UpdateValue>;
311
312/// Sort direction for one key. Parse spells descending with a leading `-`.
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum SortDirection {
315    Ascending,
316    Descending,
317}
318
319/// Parse's default page size when a query does not ask for one.
320///
321/// **Not unlimited.** An omitted `limit` used to produce an unbounded Mongo query, which is both
322/// the wrong answer and a denial-of-service surface: one request could ask for every row in a
323/// collection.
324pub const DEFAULT_LIMIT: u32 = 100;
325
326/// Everything about a read that is not a constraint.
327#[derive(Debug, Clone)]
328pub struct QueryOptions {
329    pub limit: Option<u32>,
330    pub skip: Option<u32>,
331    pub order: Vec<(String, SortDirection)>,
332    /// Projection. `None` means every field; `Some` is the explicit list.
333    ///
334    /// Upstream converts `excludeKeys` into `keys` before it reaches storage, so an adapter only
335    /// ever sees the positive form.
336    pub keys: Option<Vec<String>>,
337    /// Compare strings under upstream's case-insensitive collation rather than byte for byte.
338    ///
339    /// `{caseInsensitive: true}` (`MongoStorageAdapter.js:723`, `:801-803`), which resolves to
340    /// `{locale: "en_US", strength: 2}`. Used by the `_User` username and email uniqueness checks
341    /// and by nothing else, because it is the only place upstream asks for it.
342    ///
343    /// **A case-folding regex is not a substitute and the difference is not academic.** Strength 2
344    /// normalizes as well as folding case, so a precomposed `Café` and a decomposed `Café` are one
345    /// key to the collation and two distinct byte strings to any regex, which means a regex admits
346    /// identities upstream treats as duplicates. It does not fold diacritics: `Café` and `Cafe`
347    /// remain different identities under it.
348    pub case_insensitive: bool,
349}
350
351impl Default for QueryOptions {
352    fn default() -> Self {
353        Self {
354            limit: Some(DEFAULT_LIMIT),
355            skip: None,
356            order: Vec::new(),
357            keys: None,
358            case_insensitive: false,
359        }
360    }
361}
362
363impl QueryOptions {
364    /// Parse Parse's `order` parameter: comma-separated keys, `-` prefix for descending.
365    pub fn parse_order(order: &str) -> Vec<(String, SortDirection)> {
366        order
367            .split(',')
368            .map(str::trim)
369            .filter(|s| !s.is_empty())
370            .map(|k| match k.strip_prefix('-') {
371                Some(rest) => (rest.to_string(), SortDirection::Descending),
372                None => (k.to_string(), SortDirection::Ascending),
373            })
374            .collect()
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn supported_operators_map() {
384        for op in ["$ne", "$gt", "$gte", "$lt", "$lte"] {
385            assert!(
386                Comparison::from_operator(op, ParseValue::Number(1.0)).is_ok(),
387                "{op}"
388            );
389        }
390        assert!(Comparison::from_operator("$in", ParseValue::Array(vec![])).is_ok());
391        assert!(Comparison::from_operator("$nin", ParseValue::Array(vec![])).is_ok());
392        assert!(Comparison::from_operator("$all", ParseValue::Array(vec![])).is_ok());
393        assert!(Comparison::from_operator("$exists", ParseValue::Bool(true)).is_ok());
394    }
395
396    /// The rule that keeps a dropped constraint from broadening a result set. The list shrank at
397    /// 0.2.0 because some of these landed; what must not change is that the remainder error.
398    #[test]
399    fn an_unsupported_operator_is_an_error_not_a_no_op() {
400        for op in [
401            "$select",
402            "$dontSelect",
403            "$inQuery",
404            "$notInQuery",
405            "$nearSphere",
406            "$text",
407            "$containedBy",
408            "$geoWithin",
409        ] {
410            let e = Comparison::from_operator(op, ParseValue::Null).unwrap_err();
411            assert_eq!(e.code, parse_rust_core::ErrorCode::InvalidQuery, "{op}");
412            assert!(e.message.contains(op), "the message must name the operator");
413        }
414    }
415
416    #[test]
417    fn in_requires_an_array_and_exists_requires_a_boolean() {
418        assert!(Comparison::from_operator("$in", ParseValue::Number(1.0)).is_err());
419        assert!(Comparison::from_operator("$all", ParseValue::Number(1.0)).is_err());
420        assert!(Comparison::from_operator("$exists", ParseValue::Number(1.0)).is_err());
421    }
422
423    #[test]
424    fn the_default_limit_is_a_hundred_not_unlimited() {
425        assert_eq!(QueryOptions::default().limit, Some(DEFAULT_LIMIT));
426        assert_eq!(DEFAULT_LIMIT, 100);
427    }
428
429    #[test]
430    fn order_parsing_handles_the_minus_prefix() {
431        assert_eq!(
432            QueryOptions::parse_order("name,-createdAt, score"),
433            vec![
434                ("name".to_string(), SortDirection::Ascending),
435                ("createdAt".to_string(), SortDirection::Descending),
436                ("score".to_string(), SortDirection::Ascending),
437            ]
438        );
439        assert!(QueryOptions::parse_order("").is_empty());
440    }
441
442    /// `reduceOrOperation` collapses a single-element disjunction. Reproduced so that a class with
443    /// exactly one pointer field compiles to the same document upstream produces.
444    #[test]
445    fn a_single_alternative_disjunction_collapses() {
446        let one = Query::from_constraints(vec![Constraint::equal(
447            "owner",
448            ParseValue::String("u1".into()),
449        )]);
450        let q = Query::any_of(vec![one]);
451        assert_eq!(q.clauses.len(), 1);
452        assert!(matches!(q.clauses[0], Clause::Field(_)));
453
454        let two = Query::any_of(vec![
455            Query::from_constraints(vec![Constraint::equal("a", ParseValue::Null)]),
456            Query::from_constraints(vec![Constraint::equal("b", ParseValue::Null)]),
457        ]);
458        assert!(matches!(two.clauses.as_slice(), [Clause::Or(alts)] if alts.len() == 2));
459    }
460
461    #[test]
462    fn an_empty_alternative_is_dropped_and_an_empty_disjunction_is_unconstrained() {
463        assert!(Query::any_of(vec![]).is_empty());
464        assert!(Query::any_of(vec![Query::new(), Query::new()]).is_empty());
465    }
466
467    #[test]
468    fn conjoin_nests_a_colliding_field_and_splices_everything_else() {
469        let mut client = Query::from_constraints(vec![Constraint::equal(
470            "owner",
471            ParseValue::String("u1".into()),
472        )]);
473        client.conjoin(Query::from_constraints(vec![
474            Constraint::equal("owner", ParseValue::String("u1".into())),
475            Constraint::equal("state", ParseValue::String("open".into())),
476        ]));
477
478        // The client's `owner` survives untouched, `state` is spliced in flat, and the second
479        // `owner` is nested where it cannot merge with or displace the first.
480        assert!(matches!(
481            client.clauses.as_slice(),
482            [Clause::Field(a), Clause::Field(b), Clause::And(nested)]
483                if a.field == "owner" && b.field == "state" && nested.len() == 1
484        ));
485    }
486
487    #[test]
488    fn conjoin_stays_flat_when_no_field_collides() {
489        let mut q = Query::from_constraints(vec![Constraint::equal(
490            "title",
491            ParseValue::String("a".into()),
492        )]);
493        q.conjoin(Query::from_constraints(vec![Constraint::equal(
494            "owner",
495            ParseValue::String("u1".into()),
496        )]));
497        assert!(matches!(
498            q.clauses.as_slice(),
499            [Clause::Field(_), Clause::Field(_)]
500        ));
501    }
502
503    #[test]
504    fn conjoin_only_looks_at_top_level_fields() {
505        // `owner` named inside an `$or` is a different key in the compiled document, so it cannot
506        // collide and must not force the nesting. `hasOwnProperty` upstream behaves the same way.
507        let mut q = Query::new();
508        q.push(Clause::Or(vec![Query::from_constraints(vec![
509            Constraint::equal("owner", ParseValue::String("u2".into())),
510        ])]));
511        q.conjoin(Query::from_constraints(vec![Constraint::equal(
512            "owner",
513            ParseValue::String("u1".into()),
514        )]));
515        assert!(matches!(
516            q.clauses.as_slice(),
517            [Clause::Or(_), Clause::Field(_)]
518        ));
519    }
520
521    #[test]
522    fn only_ops_echo_their_result_back() {
523        assert!(!UpdateValue::Set(ParseValue::Null).echoes_result());
524        assert!(!UpdateValue::Unset.echoes_result());
525        assert!(UpdateValue::Increment(1.0).echoes_result());
526        assert!(UpdateValue::Add(vec![]).echoes_result());
527        assert!(UpdateValue::AddUnique(vec![]).echoes_result());
528        assert!(UpdateValue::Remove(vec![]).echoes_result());
529    }
530}