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