Skip to main content

icydb_core/db/session/query/
attribution.rs

1//! Module: db::session::query::attribution
2//! Responsibility: bounded operation-local attribution for ordinary reads.
3//! Does not own: planning, admission, execution, retained metrics, or query identity.
4//! Boundary: projects one completed live-page execution into fixed enums and counters.
5
6use crate::db::{
7    access::{AccessPath, AccessPlan},
8    executor::{SharedPreparedExecutionPlan, StructuralProjectionExecutionRoute},
9    session::query::QueryPlanCacheAttribution,
10};
11use candid::CandidType;
12use serde::Deserialize;
13
14/// One normal read result paired with bounded operation-local attribution.
15#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
16pub struct AttributedRead<T> {
17    /// The unchanged result produced by the ordinary read path.
18    pub result: T,
19    /// Fixed-size operational attribution for this call only.
20    pub attribution: OperationReadAttribution,
21}
22
23/// Coarse accepted access route selected for one read.
24///
25/// The enum intentionally excludes entity, field, index, predicate and literal
26/// identity so an attributed query cannot create an unbounded label surface.
27#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
28pub enum ReadAccessRoute {
29    /// One direct primary-key lookup.
30    PrimaryKey,
31    /// One bounded set of direct primary-key lookups.
32    PrimaryKeySet,
33    /// One inclusive primary-key range.
34    PrimaryKeyRange,
35    /// One accepted secondary-index prefix.
36    SecondaryIndexPrefix,
37    /// One bounded secondary-index multi-lookup.
38    SecondaryIndexMultiLookup,
39    /// One bounded secondary-index branch set.
40    SecondaryIndexBranchSet,
41    /// One accepted secondary-index range.
42    SecondaryIndexRange,
43    /// One authoritative primary-store scan.
44    FullScan,
45    /// One canonical union of bounded child access routes.
46    Union,
47    /// One canonical intersection of bounded child access routes.
48    Intersection,
49}
50
51/// Physical execution route used to produce one read result.
52#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
53pub enum ReadExecutionRoute {
54    /// Projection was satisfied from accepted index components.
55    Covering,
56    /// Scalar execution streamed the selected access route.
57    Streaming,
58    /// Scalar execution materialized candidates before final output.
59    Materialized,
60}
61
62/// Shared plan-cache outcome for the selected read plan.
63#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
64pub enum ReadPlanCacheOutcome {
65    /// The selected prepared plan was reused.
66    Hit,
67    /// The selected prepared plan was built for this call.
68    Miss,
69    /// The selected path did not report a cache lookup outcome.
70    Bypassed,
71}
72
73/// Fixed-size operation-local cost and route attribution for one read.
74#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
75pub struct OperationReadAttribution {
76    /// Complete local instructions observed by the outward read terminal.
77    pub total_local_instructions: u64,
78    /// Local instructions observed inside the accepted dynamic execution path.
79    pub engine_local_instructions: u64,
80    /// Local instructions used to decode accepted rows into typed output.
81    /// Dynamic reads report zero because their structural result is final.
82    pub response_decode_local_instructions: u64,
83    /// Coarse accepted access route selected by the planner.
84    pub access_route: ReadAccessRoute,
85    /// Physical route used by the executor.
86    pub execution_route: ReadExecutionRoute,
87    /// Cache outcome for the selected prepared plan.
88    pub plan_cache: ReadPlanCacheOutcome,
89    /// Physical keys or index entries visited while producing this page.
90    pub rows_scanned: u64,
91    /// Logical rows returned by this page.
92    pub rows_emitted: u64,
93}
94
95pub(in crate::db::session) struct OperationReadAttributionBuilder {
96    access_route: Option<ReadAccessRoute>,
97    execution_route: Option<ReadExecutionRoute>,
98    plan_cache: Option<ReadPlanCacheOutcome>,
99    rows_scanned: u64,
100    rows_emitted: u64,
101}
102
103impl OperationReadAttributionBuilder {
104    #[must_use]
105    pub(in crate::db::session) const fn new() -> Self {
106        Self {
107            access_route: None,
108            execution_route: None,
109            plan_cache: None,
110            rows_scanned: 0,
111            rows_emitted: 0,
112        }
113    }
114
115    pub(in crate::db::session) fn record_plan(
116        &mut self,
117        prepared_plan: &SharedPreparedExecutionPlan,
118        cache: QueryPlanCacheAttribution,
119    ) {
120        self.access_route = Some(ReadAccessRoute::from_plan(
121            &prepared_plan.logical_plan().access,
122        ));
123        self.plan_cache = Some(ReadPlanCacheOutcome::from_cache(cache));
124    }
125
126    pub(in crate::db::session) fn record_execution(
127        &mut self,
128        route: StructuralProjectionExecutionRoute,
129        rows_scanned: usize,
130        rows_emitted: u32,
131    ) {
132        self.execution_route = Some(ReadExecutionRoute::from_projection(route));
133        self.rows_scanned = u64::try_from(rows_scanned).unwrap_or(u64::MAX);
134        self.rows_emitted = u64::from(rows_emitted);
135    }
136
137    pub(in crate::db::session) fn finish(
138        self,
139        engine_local_instructions: u64,
140    ) -> Result<OperationReadAttribution, crate::db::QueryError> {
141        Ok(OperationReadAttribution {
142            total_local_instructions: engine_local_instructions,
143            engine_local_instructions,
144            response_decode_local_instructions: 0,
145            access_route: self
146                .access_route
147                .ok_or_else(crate::db::QueryError::invariant)?,
148            execution_route: self
149                .execution_route
150                .ok_or_else(crate::db::QueryError::invariant)?,
151            plan_cache: self
152                .plan_cache
153                .ok_or_else(crate::db::QueryError::invariant)?,
154            rows_scanned: self.rows_scanned,
155            rows_emitted: self.rows_emitted,
156        })
157    }
158}
159
160impl ReadAccessRoute {
161    fn from_plan(plan: &AccessPlan<crate::value::Value>) -> Self {
162        match plan {
163            AccessPlan::Path(path) => match path.as_ref() {
164                AccessPath::ByKey(_) => Self::PrimaryKey,
165                AccessPath::ByKeys(_) => Self::PrimaryKeySet,
166                AccessPath::KeyRange { .. } => Self::PrimaryKeyRange,
167                AccessPath::IndexPrefix { .. } => Self::SecondaryIndexPrefix,
168                AccessPath::IndexMultiLookup { .. } => Self::SecondaryIndexMultiLookup,
169                AccessPath::IndexBranchSet { .. } => Self::SecondaryIndexBranchSet,
170                AccessPath::IndexRange { .. } => Self::SecondaryIndexRange,
171                AccessPath::FullScan => Self::FullScan,
172            },
173            AccessPlan::Union(_) => Self::Union,
174            AccessPlan::Intersection(_) => Self::Intersection,
175        }
176    }
177}
178
179impl ReadExecutionRoute {
180    const fn from_projection(route: StructuralProjectionExecutionRoute) -> Self {
181        match route {
182            StructuralProjectionExecutionRoute::Covering => Self::Covering,
183            StructuralProjectionExecutionRoute::Streaming => Self::Streaming,
184            StructuralProjectionExecutionRoute::Materialized => Self::Materialized,
185        }
186    }
187}
188
189impl ReadPlanCacheOutcome {
190    const fn from_cache(cache: QueryPlanCacheAttribution) -> Self {
191        if cache.hits > 0 && cache.misses == 0 {
192            Self::Hit
193        } else if cache.misses > 0 && cache.hits == 0 {
194            Self::Miss
195        } else {
196            Self::Bypassed
197        }
198    }
199}
200
201#[must_use]
202#[cfg(target_arch = "wasm32")]
203pub(in crate::db::session) fn read_operation_local_instruction_counter() -> u64 {
204    crate::runtime::performance_counter(1)
205}
206
207#[must_use]
208#[cfg(not(target_arch = "wasm32"))]
209pub(in crate::db::session) const fn read_operation_local_instruction_counter() -> u64 {
210    0
211}
212
213#[cfg(test)]
214mod tests {
215    use super::{
216        OperationReadAttribution, ReadAccessRoute, ReadExecutionRoute, ReadPlanCacheOutcome,
217    };
218
219    #[test]
220    fn operation_read_attribution_has_a_fixed_small_candid_envelope() {
221        let attribution = OperationReadAttribution {
222            total_local_instructions: u64::MAX,
223            engine_local_instructions: u64::MAX,
224            response_decode_local_instructions: u64::MAX,
225            access_route: ReadAccessRoute::SecondaryIndexBranchSet,
226            execution_route: ReadExecutionRoute::Materialized,
227            plan_cache: ReadPlanCacheOutcome::Bypassed,
228            rows_scanned: u64::MAX,
229            rows_emitted: u64::MAX,
230        };
231        let encoded = candid::encode_one(attribution)
232            .expect("fixed operation-local attribution should encode");
233
234        assert_eq!(encoded.len(), 200);
235        assert!(encoded.len() <= 256);
236    }
237}