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            LoweredAccessError, 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            admission::input::validate_dynamic_query_input,
19            expr::{CompareOperator, FilterExpr, SetOperator},
20            intent::StructuralQuery,
21            plan::VisibleIndexes,
22            preparation::PreparationWork,
23        },
24        schema::SchemaInfo,
25        session::AcceptedSchemaCatalogContext,
26    },
27    traits::CanisterKind,
28};
29use icydb_diagnostic_code::DiagnosticExecutionLane;
30
31enum ExactCountPlan {
32    Entity,
33    UserIndexPrefixes(Vec<UserIndexPrefixCardinalityKey>),
34}
35
36impl<C: CanisterKind> DbSession<C> {
37    pub(in crate::db::session) fn exact_count_cardinality_prefix_keys_for_accepted_authority(
38        &self,
39        authority: &EntityAuthority,
40        query: &StructuralQuery,
41        visible_indexes: &VisibleIndexes,
42        schema_info: &SchemaInfo,
43        lane: DiagnosticExecutionLane,
44    ) -> Result<Option<Vec<UserIndexPrefixCardinalityKey>>, QueryError> {
45        // Metadata shortcuts and explain borrow the current request authority;
46        // neither creates an execution root nor needs an active row executor.
47        PreparationWork::run(self.db.request_execution_scope(), lane, |work| {
48            let Some(access) = query.try_build_count_cardinality_prefix_access_with_schema_info(
49                visible_indexes,
50                schema_info,
51                work,
52            )?
53            else {
54                return Ok(None);
55            };
56            let prefix_keys = lower_exact_user_index_prefix_cardinality_keys_for_prefix_access(
57                authority.entity_tag(),
58                &access,
59                schema_info,
60                work,
61            )
62            .map_err(|error| match error {
63                LoweredAccessError::Construction(error) => QueryError::execute(error),
64                LoweredAccessError::IndexPrefix | LoweredAccessError::IndexRange => {
65                    QueryError::invariant()
66                }
67            })?;
68
69            Ok((!prefix_keys.is_empty()).then_some(prefix_keys))
70        })
71    }
72
73    fn exact_count_request_is_scalar_metadata_shape(request: &DynamicQuery) -> bool {
74        if !request.order_terms().is_empty()
75            || !request.selected_fields().is_empty()
76            || request.row_limit().is_some()
77            || request.has_grouping()
78            || request.grouped_execution_limits().is_some()
79            || request.continuation_cursor().is_some()
80        {
81            return false;
82        }
83        #[cfg(test)]
84        if request.projection_is_distinct() {
85            return false;
86        }
87
88        true
89    }
90
91    fn exact_count_plan_against_catalog(
92        &self,
93        request: &DynamicQuery,
94        catalog: &AcceptedSchemaCatalogContext,
95    ) -> Result<ExactCountPlan, QueryError> {
96        if !Self::exact_count_request_is_scalar_metadata_shape(request) {
97            return Err(QueryError::unsupported_query());
98        }
99        match request.filter_expr() {
100            None => {
101                // This metadata-only route bypasses structural conversion.
102                validate_dynamic_query_input(request)?;
103                return Ok(ExactCountPlan::Entity);
104            }
105            Some(FilterExpr::Compare {
106                operator: CompareOperator::Eq,
107                ..
108            }) => {}
109            Some(FilterExpr::Set {
110                operator: SetOperator::In,
111                values,
112                ..
113            }) if !values.is_empty() && values.len() <= MAX_INDEX_BRANCH_SET_VALUES => {}
114            Some(_) => return Err(QueryError::unsupported_query()),
115        }
116
117        let query = self.structural_query_from_dynamic_request(
118            request,
119            catalog,
120            icydb_diagnostic_code::DiagnosticExecutionLane::PublicRead,
121        )?;
122        let schema_info = catalog.accepted_schema_info();
123        let authority = catalog.accepted_entity_authority();
124        let visible_indexes =
125            self.visible_indexes_for_store_accepted_schema(authority.store_path(), schema_info)?;
126        let prefix_keys = self
127            .exact_count_cardinality_prefix_keys_for_accepted_authority(
128                &authority,
129                &query,
130                &visible_indexes,
131                schema_info,
132                DiagnosticExecutionLane::PublicRead,
133            )?
134            .ok_or_else(QueryError::unsupported_query)?;
135
136        Ok(ExactCountPlan::UserIndexPrefixes(prefix_keys))
137    }
138
139    fn execute_exact_count_against_catalog(
140        &self,
141        request: &DynamicQuery,
142        catalog: AcceptedSchemaCatalogContext,
143    ) -> Result<u64, QueryError> {
144        let plan = self.exact_count_plan_against_catalog(request, &catalog)?;
145        let authority = catalog.accepted_entity_authority();
146        let target = match &plan {
147            ExactCountPlan::Entity => ExactCardinalityTarget::Entity,
148            ExactCountPlan::UserIndexPrefixes(prefix_keys) => {
149                ExactCardinalityTarget::UserIndexPrefixes(prefix_keys)
150            }
151        };
152        execute_exact_cardinality_for_canister(
153            &self.db,
154            authority,
155            DiagnosticExecutionLane::PublicRead,
156            target,
157        )
158        .map_err(QueryError::execute)?
159        .ok_or_else(QueryError::exact_count_metadata_unavailable)
160    }
161
162    /// Return exact visible cardinality without entering row execution.
163    ///
164    /// The request must be a bare entity count or one strict equality/`IN`
165    /// filter over the leading field of an accepted unfiltered field-path user
166    /// index. Every component, including trailing fields, must be present for
167    /// all matching rows. Metadata that is not ready fails closed; this terminal
168    /// never falls back to a scan. Unavailable metadata reports
169    /// `QUERY_EXACT_COUNT_METADATA_UNAVAILABLE`; unsupported count shapes report
170    /// `RUNTIME_UNSUPPORTED`.
171    pub fn execute_public_exact_count(&self, request: &DynamicQuery) -> Result<u64, QueryError> {
172        let catalog = self
173            .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
174            .map_err(QueryError::execute)?;
175        self.execute_exact_count_against_catalog(request, catalog)
176    }
177
178    /// Execute one exact count through a typed binding's immutable accepted
179    /// entity identity. `None` means the opaque binding is stale.
180    #[doc(hidden)]
181    pub fn execute_public_exact_count_for_typed_binding(
182        &self,
183        binding: &DynamicTypedEntityBinding,
184        request: &DynamicQuery,
185    ) -> Result<Option<u64>, QueryError> {
186        let Some(catalog) = self
187            .current_typed_entity_binding_catalog(binding)
188            .map_err(QueryError::execute)?
189        else {
190            return Ok(None);
191        };
192        self.execute_exact_count_against_catalog(request, catalog)
193            .map(Some)
194    }
195}