Skip to main content

icydb_core/db/query/plan/
executable.rs

1use crate::{
2    db::query::{
3        QueryMode,
4        plan::{
5            ContinuationSignature, CursorBoundary, ExplainPlan, LogicalPlan, PlanError,
6            PlanFingerprint,
7            continuation::{decode_primary_key_cursor_slot, decode_validated_cursor_boundary},
8        },
9        policy,
10        predicate::SchemaInfo,
11    },
12    traits::{EntityKind, FieldValue},
13};
14use std::marker::PhantomData;
15
16///
17/// ExecutablePlan
18///
19/// Executor-ready plan bound to a specific entity type.
20///
21
22#[derive(Debug)]
23pub struct ExecutablePlan<E: EntityKind> {
24    plan: LogicalPlan<E::Key>,
25    _marker: PhantomData<E>,
26}
27
28impl<E: EntityKind> ExecutablePlan<E> {
29    pub(crate) const fn new(plan: LogicalPlan<E::Key>) -> Self {
30        Self {
31            plan,
32            _marker: PhantomData,
33        }
34    }
35
36    /// Explain this plan without executing it.
37    #[must_use]
38    pub fn explain(&self) -> ExplainPlan {
39        self.plan.explain()
40    }
41
42    /// Compute a stable fingerprint for this plan.
43    #[must_use]
44    pub fn fingerprint(&self) -> PlanFingerprint {
45        self.plan.fingerprint()
46    }
47
48    /// Compute a stable continuation signature for cursor compatibility checks.
49    ///
50    /// Unlike `fingerprint()`, this excludes window state such as `limit`/`offset`.
51    #[must_use]
52    pub fn continuation_signature(&self) -> ContinuationSignature {
53        self.plan.continuation_signature(E::PATH)
54    }
55
56    /// Validate and decode a continuation cursor against this canonical plan.
57    ///
58    /// This is a planning-boundary validation step. Executors receive only a
59    /// typed boundary and must not parse or validate cursor bytes.
60    pub(crate) fn plan_cursor_boundary(
61        &self,
62        cursor: Option<&[u8]>,
63    ) -> Result<Option<CursorBoundary>, PlanError>
64    where
65        E::Key: FieldValue,
66    {
67        let Some(cursor) = cursor else {
68            return Ok(None);
69        };
70        let order =
71            policy::require_cursor_order(self.plan.order.as_ref()).map_err(PlanError::from)?;
72
73        let boundary = decode_validated_cursor_boundary(
74            cursor,
75            E::PATH,
76            E::MODEL,
77            order,
78            self.continuation_signature(),
79        )?;
80
81        // Typed key decode is the final authority for PK cursor slots.
82        let pk_field = E::MODEL.primary_key.name;
83        let pk_index = order
84            .fields
85            .iter()
86            .position(|(field, _)| field == pk_field)
87            .ok_or_else(|| PlanError::MissingPrimaryKeyTieBreak {
88                field: pk_field.to_string(),
89            })?;
90        let expected = SchemaInfo::from_entity_model(E::MODEL)
91            .map_err(PlanError::PredicateInvalid)?
92            .field(pk_field)
93            .expect("primary key exists by model contract")
94            .to_string();
95        let pk_slot = &boundary.slots[pk_index];
96        let _pk_key = decode_primary_key_cursor_slot::<E::Key>(pk_slot).map_err(|err| {
97            PlanError::ContinuationCursorPrimaryKeyTypeMismatch {
98                field: pk_field.to_string(),
99                expected,
100                value: err.into_mismatch_value(),
101            }
102        })?;
103
104        Ok(Some(boundary))
105    }
106
107    /// Return the plan mode (load vs delete).
108    #[must_use]
109    pub(crate) const fn mode(&self) -> QueryMode {
110        self.plan.mode
111    }
112
113    pub(crate) const fn access(&self) -> &crate::db::query::plan::AccessPlan<E::Key> {
114        &self.plan.access
115    }
116
117    pub(crate) fn into_inner(self) -> LogicalPlan<E::Key> {
118        self.plan
119    }
120}