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