Skip to main content

flusso_query/
query.rs

1//! The [`Query`] type: an OpenSearch query clause, tagged with the **scope** it
2//! was built in, composed with `and` / `or` / `not`.
3//!
4//! Scope `S` is the query context a handle belongs to. Root fields and flattened
5//! object / to-one-join sub-fields are all [`Root`]; a `nested` array introduces
6//! its own scope (the element type), so a nested query must be lifted
7//! ([`Nested::any`](crate::Nested::any)/[`all`](crate::Nested::all)) before it can
8//! join a `Root` query — the compiler enforces it.
9
10use std::marker::PhantomData;
11
12use serde_json::{Map, Value};
13
14use crate::handles::MinimumShouldMatch;
15
16/// The default query scope — the document root. Root fields and flattened
17/// object / to-one-join sub-fields share it.
18///
19/// It is a [`FlussoDocument`](crate::FlussoDocument) with an empty `PATH`: a
20/// root/flattened-object field sits under no `nested` boundary, so it sorts
21/// plainly. (A `nested` array's element struct is its own scope, with a
22/// non-empty `PATH`.)
23#[derive(Debug, Clone, Copy)]
24pub struct Root;
25
26impl crate::FlussoDocument for Root {
27    const PATH: &'static [crate::Segment] = &[];
28}
29
30/// A composable query clause in scope `S`.
31///
32/// Handles produce a `Query<S>` (`User::email().eq(…)` → `Query<Root>`,
33/// `Order::status().eq(…)` → `Query<Order>`). `and`/`or`/`not` and the
34/// [`crate::Search`] clauses only combine the **same** scope; a nested query is
35/// lifted to its parent scope through the nested handle.
36#[derive(Debug, Clone)]
37pub struct Query<S = Root> {
38    inner: Inner,
39    _scope: PhantomData<fn() -> S>,
40}
41
42/// The scope-free internal representation (the scope is purely a type-level tag).
43#[derive(Debug, Clone)]
44enum Inner {
45    Leaf(Value),
46    Bool(BoolInner),
47}
48
49#[derive(Debug, Clone, Default)]
50pub(crate) struct BoolInner {
51    must: Vec<Inner>,
52    filter: Vec<Inner>,
53    should: Vec<Inner>,
54    must_not: Vec<Inner>,
55    minimum_should_match: Option<Value>,
56    boost: Option<f32>,
57}
58
59#[derive(Debug, Clone, Copy)]
60enum Clause {
61    Must,
62    Filter,
63    Should,
64    MustNot,
65}
66
67impl BoolInner {
68    pub(crate) fn is_empty(&self) -> bool {
69        self.must.is_empty()
70            && self.filter.is_empty()
71            && self.should.is_empty()
72            && self.must_not.is_empty()
73    }
74
75    fn push(&mut self, clause: Clause, inner: Inner) {
76        match clause {
77            Clause::Must => self.must.push(inner),
78            Clause::Filter => self.filter.push(inner),
79            Clause::Should => self.should.push(inner),
80            Clause::MustNot => self.must_not.push(inner),
81        }
82    }
83
84    /// True when only `clause`'s list is populated, so a new entry can be
85    /// appended without changing the query's meaning.
86    fn is_pure(&self, clause: Clause) -> bool {
87        match clause {
88            Clause::Must => {
89                self.filter.is_empty() && self.should.is_empty() && self.must_not.is_empty()
90            }
91            Clause::Filter => {
92                self.must.is_empty() && self.should.is_empty() && self.must_not.is_empty()
93            }
94            Clause::Should => {
95                self.must.is_empty() && self.filter.is_empty() && self.must_not.is_empty()
96            }
97            Clause::MustNot => {
98                self.must.is_empty() && self.filter.is_empty() && self.should.is_empty()
99            }
100        }
101    }
102
103    pub(crate) fn to_value(&self) -> Value {
104        let mut body = Map::new();
105        insert_clause(&mut body, "must", &self.must);
106        insert_clause(&mut body, "filter", &self.filter);
107        insert_clause(&mut body, "should", &self.should);
108        insert_clause(&mut body, "must_not", &self.must_not);
109        if let Some(msm) = &self.minimum_should_match {
110            body.insert("minimum_should_match".to_string(), msm.clone());
111        }
112        if let Some(boost) = self.boost {
113            body.insert("boost".to_string(), Value::from(boost));
114        }
115        let mut outer = Map::new();
116        outer.insert("bool".to_string(), Value::Object(body));
117        Value::Object(outer)
118    }
119}
120
121fn insert_clause(target: &mut Map<String, Value>, key: &str, clauses: &[Inner]) {
122    if clauses.is_empty() {
123        return;
124    }
125    let array = clauses.iter().map(Inner::to_value).collect();
126    target.insert(key.to_string(), Value::Array(array));
127}
128
129impl Inner {
130    fn to_value(&self) -> Value {
131        match self {
132            Inner::Leaf(value) => value.clone(),
133            Inner::Bool(bool_inner) => bool_inner.to_value(),
134        }
135    }
136}
137
138/// Combine `a` and `b` under `clause`, flattening when `a` is already a pure
139/// bool for that clause (so `x.and(y).and(z)` is one bool with three `must`).
140fn combine(a: Inner, b: Inner, clause: Clause) -> Inner {
141    if let Inner::Bool(mut bool_inner) = a {
142        if bool_inner.is_pure(clause) {
143            bool_inner.push(clause, b);
144            return Inner::Bool(bool_inner);
145        }
146        let mut combined = BoolInner::default();
147        combined.push(clause, Inner::Bool(bool_inner));
148        combined.push(clause, b);
149        return Inner::Bool(combined);
150    }
151    let mut combined = BoolInner::default();
152    combined.push(clause, a);
153    combined.push(clause, b);
154    Inner::Bool(combined)
155}
156
157/// Combine two optional clauses under `clause`, treating an absent side as the
158/// identity (so `Some(a).or(None)` is just `a`). Both absent → `match_all`.
159/// Backs the [`AsQuery`] `and`/`or` combinators, which any builder inherits.
160fn combine_opt<S>(a: Option<Query<S>>, b: Option<Query<S>>, clause: Clause) -> Query<S> {
161    match (a, b) {
162        (Some(a), Some(b)) => Query::wrap(combine(a.inner, b.inner, clause)),
163        (Some(only), None) | (None, Some(only)) => only,
164        (None, None) => Query::match_all(),
165    }
166}
167
168impl<S> Query<S> {
169    /// Wrap a leaf clause value. Crate-internal: handles call this.
170    pub(crate) fn leaf(value: Value) -> Self {
171        Query {
172            inner: Inner::Leaf(value),
173            _scope: PhantomData,
174        }
175    }
176
177    /// A `match_all` clause — the identity when combining absent clauses.
178    pub(crate) fn match_all() -> Self {
179        Query::leaf(crate::handles::match_all_value())
180    }
181
182    fn wrap(inner: Inner) -> Self {
183        Query {
184            inner,
185            _scope: PhantomData,
186        }
187    }
188
189    /// `self AND other`, within the same scope.
190    #[must_use]
191    pub fn and(self, other: impl AsQuery<S>) -> Query<S> {
192        match other.into_query() {
193            Some(other) => Query::wrap(combine(self.inner, other.inner, Clause::Must)),
194            None => self,
195        }
196    }
197
198    /// `self OR other`, within the same scope.
199    #[must_use]
200    pub fn or(self, other: impl AsQuery<S>) -> Query<S> {
201        match other.into_query() {
202            Some(other) => Query::wrap(combine(self.inner, other.inner, Clause::Should)),
203            None => self,
204        }
205    }
206
207    /// `NOT self`.
208    #[must_use]
209    #[allow(clippy::should_implement_trait)]
210    pub fn not(self) -> Query<S> {
211        Query::wrap(Inner::Bool(BoolInner {
212            must_not: vec![self.inner],
213            ..BoolInner::default()
214        }))
215    }
216
217    /// Set `boost` on this clause. On a `bool` (an `and`/`or`/`not` result) it
218    /// becomes the bool's `boost`; a leaf clause is wrapped in a `bool` whose
219    /// single `must` is the leaf (prefer the leaf builder's own `boost` there).
220    #[must_use]
221    pub fn boost(mut self, boost: f32) -> Query<S> {
222        self.inner = match self.inner {
223            Inner::Bool(mut bool_inner) => {
224                bool_inner.boost = Some(boost);
225                Inner::Bool(bool_inner)
226            }
227            leaf => Inner::Bool(BoolInner {
228                must: vec![leaf],
229                boost: Some(boost),
230                ..BoolInner::default()
231            }),
232        };
233        self
234    }
235
236    /// Set `minimum_should_match` on a `should`-group. Use this on an `or`
237    /// chain (or `Search::min_should_match`) so the optional clauses become a
238    /// real constraint — without it, `should` beside `must`/`filter` only
239    /// scores. Accepts a count (`1`) or [`MinimumShouldMatch::percent`] / `raw`.
240    /// A leaf clause is wrapped as a single `should`.
241    #[must_use]
242    pub fn min_should_match(mut self, value: impl Into<MinimumShouldMatch>) -> Query<S> {
243        let value = value.into().to_value();
244        self.inner = match self.inner {
245            Inner::Bool(mut bool_inner) => {
246                bool_inner.minimum_should_match = Some(value);
247                Inner::Bool(bool_inner)
248            }
249            leaf => Inner::Bool(BoolInner {
250                should: vec![leaf],
251                minimum_should_match: Some(value),
252                ..BoolInner::default()
253            }),
254        };
255        self
256    }
257
258    /// Render to the OpenSearch query DSL.
259    #[must_use]
260    pub fn to_value(&self) -> Value {
261        self.inner.to_value()
262    }
263
264    /// The scope-free inner clause. Crate-internal — `Search` collects these.
265    pub(crate) fn into_inner(self) -> InnerClause {
266        InnerClause(self.inner)
267    }
268}
269
270/// An opaque scope-free clause, handed from a [`Query`] to [`crate::Search`].
271pub(crate) struct InnerClause(Inner);
272
273/// A bool builder over scope-free clauses, used by [`crate::Search`] (root scope).
274#[derive(Debug, Clone, Default)]
275pub(crate) struct BoolBuilder {
276    bool_inner: BoolInner,
277}
278
279impl BoolBuilder {
280    pub(crate) fn push_must(&mut self, clause: InnerClause) {
281        self.bool_inner.push(Clause::Must, clause.0);
282    }
283    pub(crate) fn push_filter(&mut self, clause: InnerClause) {
284        self.bool_inner.push(Clause::Filter, clause.0);
285    }
286    pub(crate) fn push_should(&mut self, clause: InnerClause) {
287        self.bool_inner.push(Clause::Should, clause.0);
288    }
289    pub(crate) fn push_must_not(&mut self, clause: InnerClause) {
290        self.bool_inner.push(Clause::MustNot, clause.0);
291    }
292    pub(crate) fn set_min_should_match(&mut self, value: Value) {
293        self.bool_inner.minimum_should_match = Some(value);
294    }
295    pub(crate) fn is_empty(&self) -> bool {
296        self.bool_inner.is_empty()
297    }
298    pub(crate) fn to_value(&self) -> Value {
299        self.bool_inner.to_value()
300    }
301}
302
303/// Anything that can become a query clause in scope `S`. A clause may be absent
304/// ([`into_query`](AsQuery::into_query) returns `None`) — that's what makes an
305/// `Option<Query<S>>` a first-class optional filter.
306///
307/// The leaf-query builders ([`TermQuery`](crate::TermQuery),
308/// [`WildcardQuery`](crate::WildcardQuery), [`MatchQuery`](crate::MatchQuery), …)
309/// implement this, so they drop straight into [`Search`](crate::Search) clauses
310/// and into `and`/`or`/`not` with no explicit `.build()`. The combinators here
311/// are *provided* methods; on a [`Query`] the inherent ones win, so a builder
312/// gains `and`/`or`/`not`/`to_value` for free while `Query`'s behavior is
313/// unchanged.
314pub trait AsQuery<S> {
315    /// The clause this produces, or `None` to contribute nothing.
316    fn into_query(self) -> Option<Query<S>>;
317
318    /// `self AND other`. An absent side is the identity.
319    #[must_use]
320    fn and(self, other: impl AsQuery<S>) -> Query<S>
321    where
322        Self: Sized,
323    {
324        combine_opt(self.into_query(), other.into_query(), Clause::Must)
325    }
326
327    /// `self OR other`. An absent side is the identity.
328    #[must_use]
329    fn or(self, other: impl AsQuery<S>) -> Query<S>
330    where
331        Self: Sized,
332    {
333        combine_opt(self.into_query(), other.into_query(), Clause::Should)
334    }
335
336    /// `NOT self` (negating an absent clause matches everything).
337    #[must_use]
338    #[allow(clippy::should_implement_trait)]
339    fn not(self) -> Query<S>
340    where
341        Self: Sized,
342    {
343        self.into_query().map_or_else(Query::match_all, Query::not)
344    }
345
346    /// Render this clause to the OpenSearch query DSL. An absent clause renders
347    /// as `match_all`. Handy for tests and debugging.
348    #[must_use]
349    fn to_value(&self) -> Value
350    where
351        Self: Sized + Clone,
352    {
353        self.clone()
354            .into_query()
355            .map_or_else(crate::handles::match_all_value, |q| q.to_value())
356    }
357}
358
359impl<S> AsQuery<S> for Query<S> {
360    fn into_query(self) -> Option<Query<S>> {
361        Some(self)
362    }
363}
364
365impl<S, T: AsQuery<S>> AsQuery<S> for Option<T> {
366    fn into_query(self) -> Option<Query<S>> {
367        self.and_then(AsQuery::into_query)
368    }
369}