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