Skip to main content

drive/query/drive_document_count_query/
index_picker.rs

1//! Index pickers for the count query.
2//!
3//! Pure functions on the document type's index map + where clauses;
4//! no Drive, no proof. Picks a covering index for a given query
5//! shape, returning `None` if no index can serve the query.
6
7use super::super::conditions::WhereClause;
8use super::DriveDocumentCountQuery;
9use crate::query::index_admissible_for_resolved_time_range;
10use crate::query::ResolvedTimeRange;
11use dpp::data_contract::document_type::Index;
12use std::collections::{BTreeMap, BTreeSet};
13
14impl DriveDocumentCountQuery<'_> {
15    /// Finds a `countable: true` index whose properties **exactly match** the
16    /// indexable (Equal/In) where-clause fields — every index property has a
17    /// corresponding clause AND every clause's field appears in the index.
18    ///
19    /// Exact coverage is the contract for both no-proof and prove count
20    /// paths: a countable index counts exactly what it indexes, and queries
21    /// against partially-covered indexes are rejected with a clear error
22    /// directing the caller at the index-design fix. This avoids the
23    /// product-of-uncovered-branching-factors walk that a prefix-match
24    /// approach would silently fall through to, and keeps the storage's
25    /// "count maintained only at the terminal level" trade-off intact (no
26    /// need to maintain counts at intermediate index levels just to serve
27    /// partial-coverage queries cheaply).
28    ///
29    /// Returns `None` if:
30    /// - Any where clause uses an operator other than `Equal` / `In`.
31    /// - The set of indexable where-clause fields doesn't exactly equal the
32    ///   set of properties of any single `countable: true` index.
33    ///
34    /// For the `documents_countable: true` case (total count with no where
35    /// clauses), the dispatcher reads the document-type primary-key tree's
36    /// CountTree directly — that path doesn't use this picker because no
37    /// index is involved.
38    ///
39    /// `resolved_time_ranges` names the fields whose equality clause was
40    /// produced by `IN_TIME_RANGE` resolution (see
41    /// [`crate::query::resolve_time_range_bucket_clause`]); it gates which
42    /// indexes are candidates at all — see
43    /// [`index_admissible_for_resolved_time_range`].
44    pub fn find_countable_index_for_where_clauses<'b>(
45        indexes: &'b BTreeMap<String, Index>,
46        where_clauses: &[WhereClause],
47        resolved_time_ranges: &[ResolvedTimeRange],
48    ) -> Option<&'b Index> {
49        if Self::has_unsupported_operator(where_clauses) {
50            return None;
51        }
52
53        let indexable_fields: BTreeSet<&str> = where_clauses
54            .iter()
55            .filter(|wc| Self::is_indexable_for_count(wc.operator))
56            .map(|wc| wc.field.as_str())
57            .collect();
58
59        // Need a clause for every property of the index, so empty
60        // `indexable_fields` only matches an empty-properties index
61        // (which doesn't exist — indexes always have at least one
62        // property — so empty where clauses never match here).
63        if indexable_fields.is_empty() {
64            return None;
65        }
66
67        for index in indexes.values() {
68            // A time-range index holds one entry per bucket containing the
69            // document, keyed by bucket start: counting over it multi-counts
70            // every document unless the query pins a single bucket, and only
71            // a resolution-produced equality does that. Conversely a raw
72            // clause must never bind to bucket keys.
73            if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) {
74                continue;
75            }
76            if !index.countable.is_countable() {
77                continue;
78            }
79            if index.properties.len() != indexable_fields.len() {
80                continue;
81            }
82            // Every index property must have a matching where-clause
83            // field. Because lengths match, this also implies every
84            // where-clause field appears in the index (no orphan
85            // clauses).
86            let all_covered = index
87                .properties
88                .iter()
89                .all(|prop| indexable_fields.contains(prop.name.as_str()));
90            if all_covered {
91                return Some(index);
92            }
93        }
94
95        None
96    }
97
98    /// Finds a `range_countable` index that can serve a range-count query.
99    ///
100    /// Match criteria:
101    /// - All `Equal`/`In` where-clause fields form a prefix of the index
102    ///   properties.
103    /// - There is exactly one range-operator where-clause, on a property
104    ///   that is the *last* property of the index (the IndexLevel
105    ///   terminator). This is the property whose values get walked.
106    /// - The index has `range_countable = true` and `countable.is_countable()`.
107    ///
108    /// Returns `None` if no such index exists or if there's more than one
109    /// range operator in the where clauses (which would require nested range
110    /// walks the current model doesn't support). Pure point-lookup queries
111    /// (no range operator) should fall back to
112    /// [`Self::find_countable_index_for_where_clauses`].
113    ///
114    /// `resolved_time_ranges` gates the candidate set exactly as in
115    /// [`Self::find_countable_index_for_where_clauses`]. A resolved field
116    /// never arrives as a range clause — resolution always produces an
117    /// equality — so with a non-empty list the only bucketed index this can
118    /// return is one whose resolved equality is a prefix property and whose
119    /// range terminator is a different property. That is the intended shape:
120    /// a range over one property within a single time bucket.
121    pub fn find_range_countable_index_for_where_clauses<'b>(
122        indexes: &'b BTreeMap<String, Index>,
123        where_clauses: &[WhereClause],
124        resolved_time_ranges: &[ResolvedTimeRange],
125    ) -> Option<&'b Index> {
126        let range_clauses: Vec<&WhereClause> = where_clauses
127            .iter()
128            .filter(|wc| Self::is_range_operator(wc.operator))
129            .collect();
130        // Accept either:
131        // - 1 range clause (Q7 / G4 / G5 / G7 — the range is the
132        //   terminator; prefix props use `==` or `In`).
133        // - 2 range clauses on distinct fields (G8 — outer range on
134        //   an index prefix property, inner range on the terminator;
135        //   the carrier-aggregate proof shape introduced by grovedb
136        //   PR #664).
137        let (outer_range_field, terminator_range_clause) = match range_clauses.len() {
138            1 => (None, range_clauses[0]),
139            2 => {
140                // The two ranges must be on different fields — same-
141                // field two-sided ranges are flattened by the parser
142                // into `between*` and arrive with one clause.
143                if range_clauses[0].field == range_clauses[1].field {
144                    return None;
145                }
146                // One of the two must be on an index's terminator; the
147                // other becomes the outer carrier dimension. We pick
148                // the terminator below by walking each candidate
149                // index's property order — defer choosing here.
150                (
151                    Some((
152                        range_clauses[0].field.as_str(),
153                        range_clauses[1].field.as_str(),
154                    )),
155                    range_clauses[0], // placeholder, refined per-index below
156                )
157            }
158            _ => return None,
159        };
160
161        // Reject any operator that's neither indexable (Equal/In) nor a
162        // range operator — anything else has no defined count semantics.
163        if where_clauses.iter().any(|wc| {
164            !Self::is_indexable_for_count(wc.operator) && !Self::is_range_operator(wc.operator)
165        }) {
166            return None;
167        }
168
169        let prefix_fields: BTreeSet<&str> = where_clauses
170            .iter()
171            .filter(|wc| Self::is_indexable_for_count(wc.operator))
172            .map(|wc| wc.field.as_str())
173            .collect();
174
175        for index in indexes.values() {
176            // Same admissibility rule as the point-lookup picker: bucketed
177            // indexes store one entry per containing bucket, so only a query
178            // pinned to a single bucket by a resolution-produced equality may
179            // walk them, and raw clauses may never bind to bucket keys.
180            if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) {
181                continue;
182            }
183            if !index.range_countable || !index.countable.is_countable() {
184                continue;
185            }
186
187            // For the two-range case, the terminator's field must be
188            // one of the two range fields, and the other range field
189            // must be the index's first property (the carrier
190            // dimension).
191            if let Some((field_a, field_b)) = outer_range_field {
192                let terminator = index.properties.last()?;
193                let first = index.properties.first()?;
194                // Determine which range field is the terminator.
195                let (outer_field, _terminator_field) = if terminator.name == field_a {
196                    (field_b, field_a)
197                } else if terminator.name == field_b {
198                    (field_a, field_b)
199                } else {
200                    continue;
201                };
202                if first.name != outer_field {
203                    continue;
204                }
205                // Any Equal/In prefix clauses must sit between the
206                // first (outer-range) and last (terminator-range)
207                // properties. For the widget contract there are no
208                // such middle properties on byBrandColor, but the
209                // builder handles the general case.
210                let intermediate_props = &index.properties[1..index.properties.len() - 1];
211                let mut intermediate_props_ok = true;
212                for prop in intermediate_props {
213                    if !prefix_fields.contains(prop.name.as_str()) {
214                        intermediate_props_ok = false;
215                        break;
216                    }
217                }
218                // Strict-coverage check, mirroring sum's picker: every
219                // Equal/In prefix field must appear in the index's
220                // intermediate properties. Without this
221                // `intermediate_props.len() == prefix_fields.len()` guard,
222                // a query with extra prefix fields would silently pick an
223                // index that *doesn't* cover them — the carrier path-query
224                // builder iterates only index properties, so the uncovered
225                // clause would simply be dropped and the per-group counts
226                // would span all its values (an over-broad result that
227                // even verifies, since the verifier rebuilds the same
228                // path query from the same picker).
229                if intermediate_props_ok && intermediate_props.len() == prefix_fields.len() {
230                    return Some(index);
231                }
232                continue;
233            }
234
235            // Single-range case (the original logic): prefix matches
236            // must come first, followed by the range property as the
237            // LAST element.
238            let mut prefix_len = 0usize;
239            for prop in &index.properties {
240                if prefix_fields.contains(prop.name.as_str()) {
241                    prefix_len += 1;
242                } else {
243                    break;
244                }
245            }
246            if prefix_len < prefix_fields.len() {
247                continue;
248            }
249            if prefix_len + 1 != index.properties.len() {
250                // Range property must be the terminator (last property).
251                continue;
252            }
253            let range_prop = &index.properties[prefix_len];
254            if range_prop.name == terminator_range_clause.field {
255                return Some(index);
256            }
257        }
258
259        None
260    }
261}