Skip to main content

drive/query/drive_document_sum_query/
index_picker.rs

1//! Sum-index pickers. Parallels count's `index_picker.rs`.
2//!
3//! Two strict-coverage pickers:
4//! - [`find_summable_index_for_where_clauses`]: returns the `summable: "<prop>"`
5//!   index whose properties *exactly* match the Equal/In where-clause fields
6//!   and whose summed property equals the request's `sum_property`. None on
7//!   miss (no partial coverage).
8//! - [`find_range_summable_index_for_where_clauses`]: returns the
9//!   `rangeSummable: true` index whose Equal/In prefix covers the
10//!   non-range clauses AND whose last property is the range
11//!   terminator. None on miss.
12//!
13//! Reject-on-miss is the load-bearing contract: callers landing in
14//! "no covering index" return `WhereClauseOnNonIndexedProperty` so the
15//! prover and verifier reject the same set of inputs (same as count).
16
17use crate::query::drive_document_sum_query::{is_indexable_for_sum, is_range_operator};
18use crate::query::{WhereClause, WhereOperator};
19use dpp::data_contract::document_type::Index;
20use std::collections::{BTreeMap, BTreeSet};
21
22/// Find a `summable: "<prop>"` index whose properties exactly cover
23/// the Equal/In where-clause fields AND whose summed property name
24/// equals the request's `sum_property`.
25///
26/// Mirror of count's `find_countable_index_for_where_clauses` with the
27/// additional `summable == Some(sum_property)` predicate on top of the
28/// strict-coverage match.
29pub fn find_summable_index_for_where_clauses<'b>(
30    indexes: &'b BTreeMap<String, Index>,
31    where_clauses: &[WhereClause],
32    sum_property: &str,
33) -> Option<&'b Index> {
34    // Defense-in-depth: any non-indexable operator immediately disqualifies
35    // — the sum point-lookup path can only serve Equal/In.
36    if where_clauses
37        .iter()
38        .any(|wc| !is_indexable_for_sum(wc.operator))
39    {
40        return None;
41    }
42
43    let indexable_fields: BTreeSet<&str> = where_clauses
44        .iter()
45        .filter(|wc| matches!(wc.operator, WhereOperator::Equal | WhereOperator::In))
46        .map(|wc| wc.field.as_str())
47        .collect();
48
49    if indexable_fields.is_empty() {
50        return None;
51    }
52
53    for index in indexes.values() {
54        // Skip if not summable OR if summable property doesn't match.
55        match &index.summable {
56            Some(prop) if prop == sum_property => {}
57            _ => continue,
58        }
59        if index.properties.len() != indexable_fields.len() {
60            continue;
61        }
62        let all_covered = index
63            .properties
64            .iter()
65            .all(|prop| indexable_fields.contains(prop.name.as_str()));
66        if all_covered {
67            return Some(index);
68        }
69    }
70
71    None
72}
73
74/// Find a `rangeSummable: true` index whose properties cover the
75/// non-range Equal/In clauses as a prefix AND whose last property is
76/// the range terminator. The summed property must match
77/// `sum_property`.
78///
79/// Mirror of count's `find_range_countable_index_for_where_clauses`.
80pub fn find_range_summable_index_for_where_clauses<'b>(
81    indexes: &'b BTreeMap<String, Index>,
82    where_clauses: &[WhereClause],
83    sum_property: &str,
84) -> Option<&'b Index> {
85    let range_clauses: Vec<&WhereClause> = where_clauses
86        .iter()
87        .filter(|wc| is_range_operator(wc.operator))
88        .collect();
89    let (outer_range_field, terminator_range_clause) = match range_clauses.len() {
90        1 => (None, range_clauses[0]),
91        2 => {
92            // Same-field two-sided ranges are flattened into `between*`
93            // and arrive as one clause; reject if same-field anyway.
94            if range_clauses[0].field == range_clauses[1].field {
95                return None;
96            }
97            (
98                Some((
99                    range_clauses[0].field.as_str(),
100                    range_clauses[1].field.as_str(),
101                )),
102                range_clauses[0],
103            )
104        }
105        _ => return None,
106    };
107
108    // Reject any operator that's neither indexable (Equal/In) nor a
109    // range operator — anything else has no defined sum semantics.
110    if where_clauses
111        .iter()
112        .any(|wc| !is_indexable_for_sum(wc.operator) && !is_range_operator(wc.operator))
113    {
114        return None;
115    }
116
117    let prefix_fields: BTreeSet<&str> = where_clauses
118        .iter()
119        .filter(|wc| matches!(wc.operator, WhereOperator::Equal | WhereOperator::In))
120        .map(|wc| wc.field.as_str())
121        .collect();
122
123    for index in indexes.values() {
124        if !index.range_summable {
125            continue;
126        }
127        // `range_summable: true` requires `summable: Some(_)` per the DPP
128        // schema; verify it matches the caller's sum_property.
129        match &index.summable {
130            Some(prop) if prop == sum_property => {}
131            _ => continue,
132        }
133
134        if let Some((field_a, field_b)) = outer_range_field {
135            let terminator = index.properties.last()?;
136            let first = index.properties.first()?;
137            let (outer_field, _terminator_field) = if terminator.name == field_a {
138                (field_b, field_a)
139            } else if terminator.name == field_b {
140                (field_a, field_b)
141            } else {
142                continue;
143            };
144            if first.name != outer_field {
145                continue;
146            }
147            let intermediate_props = &index.properties[1..index.properties.len() - 1];
148            let mut intermediate_props_ok = true;
149            for prop in intermediate_props {
150                if !prefix_fields.contains(prop.name.as_str()) {
151                    intermediate_props_ok = false;
152                    break;
153                }
154            }
155            // Strict-coverage check: every Equal/In prefix field must
156            // appear in the index's intermediate properties. Without
157            // this `intermediate_props.len() == prefix_fields.len()`
158            // guard, a query with extra prefix fields would silently
159            // pick an index that *doesn't* cover them, producing an
160            // over-broad result.
161            if intermediate_props_ok && intermediate_props.len() == prefix_fields.len() {
162                return Some(index);
163            }
164            continue;
165        }
166
167        // Single-range case.
168        let mut prefix_len = 0usize;
169        for prop in &index.properties {
170            if prefix_fields.contains(prop.name.as_str()) {
171                prefix_len += 1;
172            } else {
173                break;
174            }
175        }
176        if prefix_len < prefix_fields.len() {
177            continue;
178        }
179        if prefix_len + 1 != index.properties.len() {
180            continue;
181        }
182        let range_prop = &index.properties[prefix_len];
183        if range_prop.name == terminator_range_clause.field {
184            return Some(index);
185        }
186    }
187
188    None
189}