Skip to main content

icydb_core/db/session/query/
exact_count.rs

1//! Module: db::session::query::exact_count
2//! Responsibility: fail-closed exact-cardinality planning and public query adaptation.
3//! Does not own: cardinality maintenance, accepted schema, or row execution.
4//! Boundary: admits only whole-entity or bounded strict indexed-prefix metadata proofs.
5
6use crate::{
7    db::{
8        DbSession, DynamicQuery, DynamicTypedEntityBinding, QueryError,
9        access::{
10            MAX_INDEX_BRANCH_SET_VALUES,
11            lower_exact_user_index_prefix_cardinality_keys_for_prefix_access,
12        },
13        executor::{
14            EntityAuthority, ExactCardinalityTarget, execute_exact_cardinality_for_canister,
15        },
16        index::UserIndexPrefixCardinalityKey,
17        query::{
18            expr::{CompareOperator, FilterExpr, SetOperator},
19            intent::StructuralQuery,
20            plan::VisibleIndexes,
21        },
22        schema::SchemaInfo,
23        session::AcceptedSchemaCatalogContext,
24    },
25    traits::CanisterKind,
26};
27use icydb_diagnostic_code::DiagnosticExecutionLane;
28
29enum ExactCountPlan {
30    Entity,
31    UserIndexPrefixes(Vec<UserIndexPrefixCardinalityKey>),
32}
33
34pub(in crate::db::session) fn exact_count_cardinality_prefix_keys_for_accepted_authority(
35    authority: &EntityAuthority,
36    query: &StructuralQuery,
37    visible_indexes: &VisibleIndexes,
38    schema_info: &SchemaInfo,
39) -> Result<Option<Vec<UserIndexPrefixCardinalityKey>>, QueryError> {
40    let Some(access) = query
41        .try_build_count_cardinality_prefix_access_with_schema_info(visible_indexes, schema_info)?
42    else {
43        return Ok(None);
44    };
45    let prefix_keys = lower_exact_user_index_prefix_cardinality_keys_for_prefix_access(
46        authority.entity_tag(),
47        &access,
48        schema_info,
49    )
50    .map_err(|_err| QueryError::invariant())?;
51
52    Ok((!prefix_keys.is_empty()).then_some(prefix_keys))
53}
54
55impl<C: CanisterKind> DbSession<C> {
56    fn exact_count_request_is_scalar_metadata_shape(request: &DynamicQuery) -> bool {
57        if !request.order_terms().is_empty()
58            || !request.selected_fields().is_empty()
59            || request.row_limit().is_some()
60            || request.has_grouping()
61            || request.grouped_execution_limits().is_some()
62            || request.continuation_cursor().is_some()
63        {
64            return false;
65        }
66        #[cfg(test)]
67        if request.projection_is_distinct() {
68            return false;
69        }
70
71        true
72    }
73
74    fn exact_count_plan_against_catalog(
75        &self,
76        request: &DynamicQuery,
77        catalog: &AcceptedSchemaCatalogContext,
78    ) -> Result<ExactCountPlan, QueryError> {
79        if !Self::exact_count_request_is_scalar_metadata_shape(request) {
80            return Err(QueryError::unsupported_query());
81        }
82        match request.filter_expr() {
83            None => return Ok(ExactCountPlan::Entity),
84            Some(FilterExpr::Compare {
85                operator: CompareOperator::Eq,
86                ..
87            }) => {}
88            Some(FilterExpr::Set {
89                operator: SetOperator::In,
90                values,
91                ..
92            }) if !values.is_empty() && values.len() <= MAX_INDEX_BRANCH_SET_VALUES => {}
93            Some(_) => return Err(QueryError::unsupported_query()),
94        }
95
96        let query = Self::structural_query_from_dynamic_request(request, catalog)?;
97        let schema_info = catalog.accepted_schema_info();
98        let authority = catalog.accepted_entity_authority();
99        let visible_indexes =
100            self.visible_indexes_for_store_accepted_schema(authority.store_path(), schema_info)?;
101        let prefix_keys = exact_count_cardinality_prefix_keys_for_accepted_authority(
102            &authority,
103            &query,
104            &visible_indexes,
105            schema_info,
106        )?
107        .ok_or_else(QueryError::unsupported_query)?;
108
109        Ok(ExactCountPlan::UserIndexPrefixes(prefix_keys))
110    }
111
112    fn execute_exact_count_against_catalog(
113        &self,
114        request: &DynamicQuery,
115        catalog: AcceptedSchemaCatalogContext,
116    ) -> Result<u64, QueryError> {
117        let plan = self.exact_count_plan_against_catalog(request, &catalog)?;
118        let authority = catalog.accepted_entity_authority();
119        let target = match &plan {
120            ExactCountPlan::Entity => ExactCardinalityTarget::Entity,
121            ExactCountPlan::UserIndexPrefixes(prefix_keys) => {
122                ExactCardinalityTarget::UserIndexPrefixes(prefix_keys)
123            }
124        };
125        execute_exact_cardinality_for_canister(
126            &self.db,
127            authority,
128            DiagnosticExecutionLane::PublicRead,
129            target,
130        )
131        .map_err(QueryError::execute)?
132        .ok_or_else(QueryError::unsupported_query)
133    }
134
135    /// Return exact visible cardinality without entering row execution.
136    ///
137    /// The request must be a bare entity count or one strict equality/`IN`
138    /// filter over the leading field of an accepted unfiltered field-path user
139    /// index. The index may contain additional trailing fields. Metadata that
140    /// is not ready fails closed; this terminal never falls back to a scan.
141    pub fn execute_public_exact_count(&self, request: &DynamicQuery) -> Result<u64, QueryError> {
142        let catalog = self
143            .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
144            .map_err(QueryError::execute)?;
145        self.execute_exact_count_against_catalog(request, catalog)
146    }
147
148    /// Execute one exact count through a typed binding's immutable accepted
149    /// entity identity. `None` means the opaque binding is stale.
150    #[doc(hidden)]
151    pub fn execute_public_exact_count_for_typed_binding(
152        &self,
153        binding: &DynamicTypedEntityBinding,
154        request: &DynamicQuery,
155    ) -> Result<Option<u64>, QueryError> {
156        let Some(catalog) = self
157            .current_typed_entity_binding_catalog(binding)
158            .map_err(QueryError::execute)?
159        else {
160            return Ok(None);
161        };
162        self.execute_exact_count_against_catalog(request, catalog)
163            .map(Some)
164    }
165}