Skip to main content

icydb_core/db/executor/
trace.rs

1//! Executor query tracing boundary.
2//!
3//! Tracing is optional, injected by the caller, and must not affect execution semantics.
4
5use crate::{
6    db::query::plan::{AccessPath, AccessPlan, ExecutablePlan, PlanFingerprint},
7    error::{ErrorClass, ErrorOrigin, InternalError},
8    traits::EntityKind,
9};
10use sha2::{Digest, Sha256};
11
12///
13/// QueryTraceSink
14///
15
16pub trait QueryTraceSink: Send + Sync {
17    fn on_event(&self, event: QueryTraceEvent);
18}
19
20///
21/// TraceExecutorKind
22///
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum TraceExecutorKind {
26    Load,
27    Save,
28    Delete,
29}
30
31///
32/// TraceAccess
33///
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum TraceAccess {
37    ByKey,
38    ByKeys { count: u32 },
39    KeyRange,
40    IndexPrefix { name: &'static str, prefix_len: u32 },
41    FullScan,
42    Union { branches: u32 },
43    Intersection { branches: u32 },
44}
45
46///
47/// TracePhase
48///
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum TracePhase {
52    Access,
53    Filter,
54    Order,
55    Page,
56    DeleteLimit,
57}
58
59///
60/// QueryTraceEvent
61///
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum QueryTraceEvent {
65    Start {
66        fingerprint: PlanFingerprint,
67        executor: TraceExecutorKind,
68        access: Option<TraceAccess>,
69    },
70    Phase {
71        fingerprint: PlanFingerprint,
72        executor: TraceExecutorKind,
73        access: Option<TraceAccess>,
74        phase: TracePhase,
75        rows: u64,
76    },
77    Finish {
78        fingerprint: PlanFingerprint,
79        executor: TraceExecutorKind,
80        access: Option<TraceAccess>,
81        rows: u64,
82    },
83    Error {
84        fingerprint: PlanFingerprint,
85        executor: TraceExecutorKind,
86        access: Option<TraceAccess>,
87        class: ErrorClass,
88        origin: ErrorOrigin,
89    },
90}
91
92///
93/// TraceScope
94///
95
96pub struct TraceScope {
97    sink: &'static dyn QueryTraceSink,
98    fingerprint: PlanFingerprint,
99    executor: TraceExecutorKind,
100    access: Option<TraceAccess>,
101}
102
103impl TraceScope {
104    fn new(
105        sink: &'static dyn QueryTraceSink,
106        fingerprint: PlanFingerprint,
107        executor: TraceExecutorKind,
108        access: Option<TraceAccess>,
109    ) -> Self {
110        sink.on_event(QueryTraceEvent::Start {
111            fingerprint,
112            executor,
113            access,
114        });
115        Self {
116            sink,
117            fingerprint,
118            executor,
119            access,
120        }
121    }
122
123    pub(crate) fn finish(self, rows: u64) {
124        self.sink.on_event(QueryTraceEvent::Finish {
125            fingerprint: self.fingerprint,
126            executor: self.executor,
127            access: self.access,
128            rows,
129        });
130    }
131
132    pub(crate) fn phase(&self, phase: TracePhase, rows: u64) {
133        self.sink.on_event(QueryTraceEvent::Phase {
134            fingerprint: self.fingerprint,
135            executor: self.executor,
136            access: self.access,
137            phase,
138            rows,
139        });
140    }
141
142    pub(crate) fn error(self, err: &InternalError) {
143        self.sink.on_event(QueryTraceEvent::Error {
144            fingerprint: self.fingerprint,
145            executor: self.executor,
146            access: self.access,
147            class: err.class,
148            origin: err.origin,
149        });
150    }
151}
152
153pub fn start_plan_trace<E: EntityKind>(
154    sink: Option<&'static dyn QueryTraceSink>,
155    executor: TraceExecutorKind,
156    plan: &ExecutablePlan<E>,
157) -> Option<TraceScope> {
158    let sink = sink?;
159    let access = Some(trace_access_from_plan(plan.access()));
160    let fingerprint = plan.fingerprint();
161    Some(TraceScope::new(sink, fingerprint, executor, access))
162}
163
164pub fn start_exec_trace(
165    sink: Option<&'static dyn QueryTraceSink>,
166    executor: TraceExecutorKind,
167    entity_path: &'static str,
168    access: Option<TraceAccess>,
169    detail: Option<&'static str>,
170) -> Option<TraceScope> {
171    let sink = sink?;
172    let fingerprint = exec_fingerprint(executor, entity_path, detail);
173    Some(TraceScope::new(sink, fingerprint, executor, access))
174}
175
176fn trace_access_from_plan<K>(plan: &AccessPlan<K>) -> TraceAccess {
177    match plan {
178        AccessPlan::Path(path) => trace_access_from_path(path),
179        AccessPlan::Union(children) => {
180            // NOTE: Diagnostics are best-effort; overflow saturates to preserve determinism.
181            TraceAccess::Union {
182                branches: u32::try_from(children.len()).unwrap_or(u32::MAX),
183            }
184        }
185        AccessPlan::Intersection(children) => {
186            // NOTE: Diagnostics are best-effort; overflow saturates to preserve determinism.
187            TraceAccess::Intersection {
188                branches: u32::try_from(children.len()).unwrap_or(u32::MAX),
189            }
190        }
191    }
192}
193
194fn trace_access_from_path<K>(path: &AccessPath<K>) -> TraceAccess {
195    match path {
196        AccessPath::ByKey(_) => TraceAccess::ByKey,
197        AccessPath::ByKeys(keys) => {
198            // NOTE: Diagnostics are best-effort; overflow saturates to preserve determinism.
199            TraceAccess::ByKeys {
200                count: u32::try_from(keys.len()).unwrap_or(u32::MAX),
201            }
202        }
203        AccessPath::KeyRange { .. } => TraceAccess::KeyRange,
204        AccessPath::IndexPrefix { index, values } => {
205            // NOTE: Diagnostics are best-effort; overflow saturates to preserve determinism.
206            TraceAccess::IndexPrefix {
207                name: index.name,
208                prefix_len: u32::try_from(values.len()).unwrap_or(u32::MAX),
209            }
210        }
211        AccessPath::FullScan => TraceAccess::FullScan,
212    }
213}
214
215fn exec_fingerprint(
216    executor: TraceExecutorKind,
217    entity_path: &'static str,
218    detail: Option<&'static str>,
219) -> PlanFingerprint {
220    let mut hasher = Sha256::new();
221    hasher.update(b"execfp:v1");
222    hasher.update([executor_tag(executor)]);
223    write_str(&mut hasher, entity_path);
224    match detail {
225        Some(detail) => {
226            hasher.update([1u8]);
227            write_str(&mut hasher, detail);
228        }
229        None => {
230            hasher.update([0u8]);
231        }
232    }
233    let digest = hasher.finalize();
234    let mut out = [0u8; 32];
235    out.copy_from_slice(&digest);
236    PlanFingerprint::from_bytes(out)
237}
238
239const fn executor_tag(executor: TraceExecutorKind) -> u8 {
240    match executor {
241        TraceExecutorKind::Load => 0x01,
242        TraceExecutorKind::Save => 0x02,
243        TraceExecutorKind::Delete => 0x03,
244    }
245}
246
247fn write_str(hasher: &mut Sha256, value: &str) {
248    // NOTE: Diagnostics-only fingerprinting saturates on overflow to avoid panics.
249    let len = u32::try_from(value.len()).unwrap_or(u32::MAX);
250    hasher.update(len.to_be_bytes());
251    hasher.update(value.as_bytes());
252}