Skip to main content

icydb_core/db/query/intent/
query.rs

1//! Module: query::intent::query
2//! Responsibility: typed query-intent construction and planner handoff for entity queries.
3//! Does not own: runtime execution semantics or access-path execution behavior.
4//! Boundary: exposes query APIs and emits planner-owned compiled query contracts.
5
6#[cfg(feature = "sql")]
7use crate::db::query::plan::expr::ProjectionSelection;
8use crate::{
9    db::{
10        predicate::{CompareOp, MissingRowPolicy, Predicate},
11        query::{
12            builder::AggregateExpr,
13            explain::ExplainPlan,
14            expr::FilterExpr,
15            expr::OrderTerm as FluentOrderTerm,
16            intent::{QueryError, QueryModel},
17            plan::{
18                AccessPlannedQuery, LoadSpec, OrderSpec, PreparedScalarPlanningState, QueryMode,
19                VisibleIndexes, expr::Expr,
20            },
21        },
22        schema::SchemaInfo,
23    },
24    traits::{EntityKind, KeyValueCodec, SingletonEntity},
25    value::{InputValue, Value},
26};
27use core::marker::PhantomData;
28
29///
30/// StructuralQuery
31///
32/// Generic-free query-intent core shared by typed `Query<E>` wrappers.
33/// Stores model-level key access as `Value` so only typed key-entry helpers
34/// remain entity-specific at the outer API boundary.
35///
36
37#[derive(Clone, Debug)]
38pub(in crate::db) struct StructuralQuery {
39    intent: QueryModel<'static, Value>,
40}
41
42impl StructuralQuery {
43    #[must_use]
44    pub(in crate::db) const fn new(
45        model: &'static crate::model::entity::EntityModel,
46        consistency: MissingRowPolicy,
47    ) -> Self {
48        Self {
49            intent: QueryModel::new(model, consistency),
50        }
51    }
52
53    // Rewrap one updated generic-free intent model back into the structural
54    // query shell so local transformation helpers do not rebuild `Self`
55    // ad hoc at each boundary method.
56    const fn from_intent(intent: QueryModel<'static, Value>) -> Self {
57        Self { intent }
58    }
59
60    // Apply one infallible intent transformation while preserving the
61    // structural query shell at this boundary.
62    fn map_intent(
63        self,
64        map: impl FnOnce(QueryModel<'static, Value>) -> QueryModel<'static, Value>,
65    ) -> Self {
66        Self::from_intent(map(self.intent))
67    }
68
69    // Apply one fallible intent transformation while keeping result wrapping
70    // local to the structural query boundary.
71    fn try_map_intent(
72        self,
73        map: impl FnOnce(QueryModel<'static, Value>) -> Result<QueryModel<'static, Value>, QueryError>,
74    ) -> Result<Self, QueryError> {
75        map(self.intent).map(Self::from_intent)
76    }
77
78    #[must_use]
79    const fn mode(&self) -> QueryMode {
80        self.intent.mode()
81    }
82
83    #[must_use]
84    fn has_explicit_order(&self) -> bool {
85        self.intent.has_explicit_order()
86    }
87
88    #[must_use]
89    pub(in crate::db) const fn has_grouping(&self) -> bool {
90        self.intent.has_grouping()
91    }
92
93    #[must_use]
94    const fn load_spec(&self) -> Option<LoadSpec> {
95        match self.intent.mode() {
96            QueryMode::Load(spec) => Some(spec),
97            QueryMode::Delete(_) => None,
98        }
99    }
100
101    #[must_use]
102    pub(in crate::db) fn filter_predicate(mut self, predicate: Predicate) -> Self {
103        self.intent = self.intent.filter_predicate(predicate);
104        self
105    }
106
107    #[must_use]
108    pub(in crate::db) fn filter(mut self, expr: impl Into<FilterExpr>) -> Self {
109        self.intent = self.intent.filter(expr.into());
110        self
111    }
112
113    #[must_use]
114    pub(in crate::db) fn filter_expr_with_normalized_predicate(
115        mut self,
116        expr: Expr,
117        predicate: Predicate,
118    ) -> Self {
119        self.intent = self
120            .intent
121            .filter_expr_with_normalized_predicate(expr, predicate);
122        self
123    }
124    pub(in crate::db) fn order_term(mut self, term: FluentOrderTerm) -> Self {
125        self.intent = self.intent.order_term(term);
126        self
127    }
128
129    // Keep the exact expression-owned scalar filter lane available for
130    // internal SQL lowering and parity callers that must preserve one planner
131    // expression without routing through the public typed `FilterExpr` surface.
132    #[must_use]
133    pub(in crate::db) fn filter_expr(mut self, expr: Expr) -> Self {
134        self.intent = self.intent.filter_expr(expr);
135        self
136    }
137
138    #[must_use]
139    pub(in crate::db) fn order_spec(mut self, order: OrderSpec) -> Self {
140        self.intent = self.intent.order_spec(order);
141        self
142    }
143
144    #[must_use]
145    pub(in crate::db) fn distinct(mut self) -> Self {
146        self.intent = self.intent.distinct();
147        self
148    }
149
150    #[cfg(all(test, feature = "sql"))]
151    #[must_use]
152    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
153    where
154        I: IntoIterator<Item = S>,
155        S: Into<String>,
156    {
157        self.intent = self.intent.select_fields(fields);
158        self
159    }
160
161    #[cfg(feature = "sql")]
162    #[must_use]
163    pub(in crate::db) fn projection_selection(mut self, selection: ProjectionSelection) -> Self {
164        self.intent = self.intent.projection_selection(selection);
165        self
166    }
167
168    pub(in crate::db) fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
169        self.try_map_intent(|intent| intent.push_group_field(field.as_ref()))
170    }
171
172    #[must_use]
173    pub(in crate::db) fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
174        self.intent = self.intent.push_group_aggregate(aggregate);
175        self
176    }
177
178    #[must_use]
179    fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
180        self.intent = self.intent.grouped_limits(max_groups, max_group_bytes);
181        self
182    }
183
184    pub(in crate::db) fn having_group(
185        self,
186        field: impl AsRef<str>,
187        op: CompareOp,
188        value: Value,
189    ) -> Result<Self, QueryError> {
190        let field = field.as_ref().to_owned();
191        self.try_map_intent(|intent| intent.push_having_group_clause(&field, op, value))
192    }
193
194    pub(in crate::db) fn having_aggregate(
195        self,
196        aggregate_index: usize,
197        op: CompareOp,
198        value: Value,
199    ) -> Result<Self, QueryError> {
200        self.try_map_intent(|intent| {
201            intent.push_having_aggregate_clause(aggregate_index, op, value)
202        })
203    }
204
205    #[cfg(test)]
206    pub(in crate::db) fn having_expr(self, expr: Expr) -> Result<Self, QueryError> {
207        self.try_map_intent(|intent| intent.push_having_expr(expr))
208    }
209
210    pub(in crate::db) fn having_expr_preserving_shape(
211        self,
212        expr: Expr,
213    ) -> Result<Self, QueryError> {
214        self.try_map_intent(|intent| intent.push_having_expr_preserving_shape(expr))
215    }
216
217    #[must_use]
218    fn by_id(self, id: Value) -> Self {
219        self.map_intent(|intent| intent.by_id(id))
220    }
221
222    #[must_use]
223    fn by_ids<I>(self, ids: I) -> Self
224    where
225        I: IntoIterator<Item = Value>,
226    {
227        self.map_intent(|intent| intent.by_ids(ids))
228    }
229
230    #[must_use]
231    fn only(self, id: Value) -> Self {
232        self.map_intent(|intent| intent.only(id))
233    }
234
235    #[must_use]
236    pub(in crate::db) fn delete(mut self) -> Self {
237        self.intent = self.intent.delete();
238        self
239    }
240
241    #[must_use]
242    pub(in crate::db) fn limit(mut self, limit: u32) -> Self {
243        self.intent = self.intent.limit(limit);
244        self
245    }
246
247    #[must_use]
248    pub(in crate::db) fn offset(mut self, offset: u32) -> Self {
249        self.intent = self.intent.offset(offset);
250        self
251    }
252
253    pub(in crate::db) fn build_plan(&self) -> Result<AccessPlannedQuery, QueryError> {
254        self.intent.build_plan_model()
255    }
256
257    pub(in crate::db) fn build_plan_with_visible_indexes(
258        &self,
259        visible_indexes: &VisibleIndexes<'_>,
260    ) -> Result<AccessPlannedQuery, QueryError> {
261        self.intent.build_plan_model_with_indexes(visible_indexes)
262    }
263
264    pub(in crate::db) fn prepare_scalar_planning_state_with_schema_info(
265        &self,
266        schema_info: SchemaInfo,
267    ) -> Result<PreparedScalarPlanningState<'_>, QueryError> {
268        self.intent
269            .prepare_scalar_planning_state_with_schema_info(schema_info)
270    }
271
272    pub(in crate::db) fn build_plan_with_visible_indexes_from_scalar_planning_state(
273        &self,
274        visible_indexes: &VisibleIndexes<'_>,
275        planning_state: PreparedScalarPlanningState<'_>,
276    ) -> Result<AccessPlannedQuery, QueryError> {
277        self.intent
278            .build_plan_model_with_indexes_from_scalar_planning_state(
279                visible_indexes,
280                planning_state,
281            )
282    }
283
284    pub(in crate::db) fn try_build_trivial_scalar_load_plan(
285        &self,
286    ) -> Result<Option<AccessPlannedQuery>, QueryError> {
287        self.intent.try_build_trivial_scalar_load_plan()
288    }
289
290    #[must_use]
291    pub(in crate::db) fn trivial_scalar_load_fast_path_eligible(&self) -> bool {
292        self.intent.trivial_scalar_load_fast_path_eligible()
293    }
294
295    #[must_use]
296    #[cfg(test)]
297    pub(in crate::db) fn structural_cache_key(
298        &self,
299    ) -> crate::db::query::intent::StructuralQueryCacheKey {
300        crate::db::query::intent::StructuralQueryCacheKey::from_query_model(&self.intent)
301    }
302
303    #[must_use]
304    pub(in crate::db) fn structural_cache_key_with_normalized_predicate_fingerprint(
305        &self,
306        predicate_fingerprint: Option<[u8; 32]>,
307    ) -> crate::db::query::intent::StructuralQueryCacheKey {
308        self.intent
309            .structural_cache_key_with_normalized_predicate_fingerprint(predicate_fingerprint)
310    }
311
312    // Build one access plan using either schema-owned indexes or the session
313    // visibility slice already resolved at the caller boundary.
314    fn build_plan_for_visibility(
315        &self,
316        visible_indexes: Option<&VisibleIndexes<'_>>,
317    ) -> Result<AccessPlannedQuery, QueryError> {
318        match visible_indexes {
319            Some(visible_indexes) => self.build_plan_with_visible_indexes(visible_indexes),
320            None => self.build_plan(),
321        }
322    }
323
324    #[must_use]
325    pub(in crate::db) const fn model(&self) -> &'static crate::model::entity::EntityModel {
326        self.intent.model()
327    }
328}
329
330///
331/// QueryPlanHandle
332///
333/// QueryPlanHandle stores the neutral access-planned query owned by the query
334/// layer. Executor-specific prepared-plan caching remains outside this DTO, so
335/// query values do not depend on executor runtime contracts.
336///
337
338#[derive(Clone, Debug)]
339struct QueryPlanHandle {
340    plan: Box<AccessPlannedQuery>,
341}
342
343impl QueryPlanHandle {
344    #[must_use]
345    fn from_plan(plan: AccessPlannedQuery) -> Self {
346        Self {
347            plan: Box::new(plan),
348        }
349    }
350
351    #[must_use]
352    const fn logical_plan(&self) -> &AccessPlannedQuery {
353        &self.plan
354    }
355
356    #[must_use]
357    fn into_inner(self) -> AccessPlannedQuery {
358        *self.plan
359    }
360}
361
362///
363/// PlannedQuery
364///
365/// PlannedQuery keeps the typed planning surface stable while allowing the
366/// session boundary to reuse one shared prepared-plan artifact internally.
367///
368
369#[derive(Debug)]
370pub struct PlannedQuery<E: EntityKind> {
371    plan: QueryPlanHandle,
372    _marker: PhantomData<E>,
373}
374
375impl<E: EntityKind> PlannedQuery<E> {
376    #[must_use]
377    pub(in crate::db) fn from_plan(plan: AccessPlannedQuery) -> Self {
378        Self {
379            plan: QueryPlanHandle::from_plan(plan),
380            _marker: PhantomData,
381        }
382    }
383
384    #[must_use]
385    pub fn explain(&self) -> ExplainPlan {
386        self.plan.logical_plan().explain()
387    }
388
389    /// Return the stable plan hash for this planned query.
390    #[must_use]
391    pub fn plan_hash_hex(&self) -> String {
392        self.plan.logical_plan().fingerprint().to_string()
393    }
394}
395
396///
397/// CompiledQuery
398///
399/// Typed compiled-query shell over one structural planner contract.
400/// The outer entity marker preserves executor handoff inference without
401/// carrying a second adapter object, while session-owned paths can still reuse
402/// the cached shared prepared plan directly.
403///
404
405#[derive(Clone, Debug)]
406pub struct CompiledQuery<E: EntityKind> {
407    plan: QueryPlanHandle,
408    _marker: PhantomData<E>,
409}
410
411impl<E: EntityKind> CompiledQuery<E> {
412    #[must_use]
413    pub(in crate::db) fn from_plan(plan: AccessPlannedQuery) -> Self {
414        Self {
415            plan: QueryPlanHandle::from_plan(plan),
416            _marker: PhantomData,
417        }
418    }
419
420    #[must_use]
421    pub fn explain(&self) -> ExplainPlan {
422        self.plan.logical_plan().explain()
423    }
424
425    /// Return the stable plan hash for this compiled query.
426    #[must_use]
427    pub fn plan_hash_hex(&self) -> String {
428        self.plan.logical_plan().fingerprint().to_string()
429    }
430
431    #[must_use]
432    #[cfg(test)]
433    pub(in crate::db) fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
434        self.plan.logical_plan().projection_spec(E::MODEL)
435    }
436
437    /// Convert one compiled query back into the neutral planned-query contract.
438    pub(in crate::db) fn into_plan(self) -> AccessPlannedQuery {
439        self.plan.into_inner()
440    }
441
442    #[must_use]
443    #[cfg(test)]
444    pub(in crate::db) fn into_inner(self) -> AccessPlannedQuery {
445        self.plan.into_inner()
446    }
447}
448
449///
450/// Query
451///
452/// Typed, declarative query intent for a specific entity type.
453///
454/// This intent is:
455/// - schema-agnostic at construction
456/// - normalized and validated only during planning
457/// - free of access-path decisions
458///
459
460#[derive(Debug)]
461pub struct Query<E: EntityKind> {
462    inner: StructuralQuery,
463    _marker: PhantomData<E>,
464}
465
466impl<E: EntityKind> Query<E> {
467    // Rebind one structural query core to the typed `Query<E>` surface.
468    pub(in crate::db) const fn from_inner(inner: StructuralQuery) -> Self {
469        Self {
470            inner,
471            _marker: PhantomData,
472        }
473    }
474
475    /// Create a new intent with an explicit missing-row policy.
476    /// Ignore favors idempotency and may mask index/data divergence on deletes.
477    /// Use Error to surface missing rows during scan/delete execution.
478    #[must_use]
479    pub const fn new(consistency: MissingRowPolicy) -> Self {
480        Self::from_inner(StructuralQuery::new(E::MODEL, consistency))
481    }
482
483    /// Return the intent mode (load vs delete).
484    #[must_use]
485    pub const fn mode(&self) -> QueryMode {
486        self.inner.mode()
487    }
488
489    #[cfg(test)]
490    pub(in crate::db) fn explain_with_visible_indexes(
491        &self,
492        visible_indexes: &VisibleIndexes<'_>,
493    ) -> Result<ExplainPlan, QueryError> {
494        let plan = self.build_plan_for_visibility(Some(visible_indexes))?;
495
496        Ok(plan.explain())
497    }
498
499    #[cfg(test)]
500    pub(in crate::db) fn plan_hash_hex_with_visible_indexes(
501        &self,
502        visible_indexes: &VisibleIndexes<'_>,
503    ) -> Result<String, QueryError> {
504        let plan = self.build_plan_for_visibility(Some(visible_indexes))?;
505
506        Ok(plan.fingerprint().to_string())
507    }
508
509    // Build one typed access plan using either schema-owned indexes or the
510    // visibility slice already resolved at the session boundary.
511    fn build_plan_for_visibility(
512        &self,
513        visible_indexes: Option<&VisibleIndexes<'_>>,
514    ) -> Result<AccessPlannedQuery, QueryError> {
515        self.inner.build_plan_for_visibility(visible_indexes)
516    }
517
518    // Build one structural plan for the requested visibility lane and then
519    // project it into one typed query-owned contract so planned vs compiled
520    // outputs do not each duplicate the same plan handoff shape.
521    fn map_plan_for_visibility<T>(
522        &self,
523        visible_indexes: Option<&VisibleIndexes<'_>>,
524        map: impl FnOnce(AccessPlannedQuery) -> T,
525    ) -> Result<T, QueryError> {
526        let plan = self.build_plan_for_visibility(visible_indexes)?;
527
528        Ok(map(plan))
529    }
530
531    // Wrap one built plan as the typed planned-query DTO.
532    pub(in crate::db) fn planned_query_from_plan(plan: AccessPlannedQuery) -> PlannedQuery<E> {
533        PlannedQuery::from_plan(plan)
534    }
535
536    // Wrap one built plan as the typed compiled-query DTO.
537    pub(in crate::db) fn compiled_query_from_plan(plan: AccessPlannedQuery) -> CompiledQuery<E> {
538        CompiledQuery::from_plan(plan)
539    }
540
541    #[must_use]
542    pub(in crate::db::query) fn has_explicit_order(&self) -> bool {
543        self.inner.has_explicit_order()
544    }
545
546    #[must_use]
547    pub(in crate::db) const fn structural(&self) -> &StructuralQuery {
548        &self.inner
549    }
550
551    #[must_use]
552    pub const fn has_grouping(&self) -> bool {
553        self.inner.has_grouping()
554    }
555
556    #[must_use]
557    pub(in crate::db::query) const fn load_spec(&self) -> Option<LoadSpec> {
558        self.inner.load_spec()
559    }
560
561    /// Add one typed filter expression, implicitly AND-ing with any existing filter.
562    #[must_use]
563    pub fn filter(mut self, expr: impl Into<FilterExpr>) -> Self {
564        self.inner = self.inner.filter(expr);
565        self
566    }
567
568    // Keep the internal fluent parity hook available for tests that need one
569    // exact expression-owned scalar filter shape instead of the public typed
570    // `FilterExpr` lowering path.
571    #[cfg(test)]
572    #[must_use]
573    pub(in crate::db) fn filter_expr(mut self, expr: Expr) -> Self {
574        self.inner = self.inner.filter_expr(expr);
575        self
576    }
577
578    // Keep the internal predicate-owned filter hook available for convergence
579    // tests without retaining the typed adapter in normal builds after SQL
580    // UPDATE moved to structural lowering.
581    #[cfg(test)]
582    #[must_use]
583    pub(in crate::db) fn filter_predicate(mut self, predicate: Predicate) -> Self {
584        self.inner = self.inner.filter_predicate(predicate);
585        self
586    }
587
588    /// Append one typed ORDER BY term.
589    #[must_use]
590    pub fn order_term(mut self, term: FluentOrderTerm) -> Self {
591        self.inner = self.inner.order_term(term);
592        self
593    }
594
595    /// Append multiple typed ORDER BY terms in declaration order.
596    #[must_use]
597    pub fn order_terms<I>(mut self, terms: I) -> Self
598    where
599        I: IntoIterator<Item = FluentOrderTerm>,
600    {
601        for term in terms {
602            self.inner = self.inner.order_term(term);
603        }
604
605        self
606    }
607
608    /// Enable DISTINCT semantics for this query.
609    #[must_use]
610    pub fn distinct(mut self) -> Self {
611        self.inner = self.inner.distinct();
612        self
613    }
614
615    // Keep the internal fluent SQL parity hook available for lowering tests
616    // without making generated SQL binding depend on the typed query shell.
617    #[cfg(all(test, feature = "sql"))]
618    #[must_use]
619    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
620    where
621        I: IntoIterator<Item = S>,
622        S: Into<String>,
623    {
624        self.inner = self.inner.select_fields(fields);
625        self
626    }
627
628    /// Add one GROUP BY field.
629    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
630        let Self { inner, .. } = self;
631        let inner = inner.group_by(field)?;
632
633        Ok(Self::from_inner(inner))
634    }
635
636    /// Add one aggregate terminal via composable aggregate expression.
637    #[must_use]
638    pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
639        self.inner = self.inner.aggregate(aggregate);
640        self
641    }
642
643    /// Override grouped hard limits for grouped execution budget enforcement.
644    #[must_use]
645    pub fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
646        self.inner = self.inner.grouped_limits(max_groups, max_group_bytes);
647        self
648    }
649
650    /// Add one grouped HAVING compare clause over one grouped key field.
651    pub fn having_group(
652        self,
653        field: impl AsRef<str>,
654        op: CompareOp,
655        value: InputValue,
656    ) -> Result<Self, QueryError> {
657        let Self { inner, .. } = self;
658        let inner = inner.having_group(field, op, value.into())?;
659
660        Ok(Self::from_inner(inner))
661    }
662
663    /// Add one grouped HAVING compare clause over one grouped aggregate output.
664    pub fn having_aggregate(
665        self,
666        aggregate_index: usize,
667        op: CompareOp,
668        value: InputValue,
669    ) -> Result<Self, QueryError> {
670        let Self { inner, .. } = self;
671        let inner = inner.having_aggregate(aggregate_index, op, value.into())?;
672
673        Ok(Self::from_inner(inner))
674    }
675
676    // Keep the internal fluent parity hook available for tests that need one
677    // exact grouped HAVING expression shape instead of the public grouped
678    // clause builders.
679    #[cfg(test)]
680    pub(in crate::db) fn having_expr(self, expr: Expr) -> Result<Self, QueryError> {
681        let Self { inner, .. } = self;
682        let inner = inner.having_expr(expr)?;
683
684        Ok(Self::from_inner(inner))
685    }
686
687    /// Set the access path to a single primary key lookup.
688    pub(in crate::db) fn by_id(self, id: E::Key) -> Self {
689        let Self { inner, .. } = self;
690
691        Self::from_inner(inner.by_id(id.to_key_value()))
692    }
693
694    /// Set the access path to a primary key batch lookup.
695    pub(in crate::db) fn by_ids<I>(self, ids: I) -> Self
696    where
697        I: IntoIterator<Item = E::Key>,
698    {
699        let Self { inner, .. } = self;
700
701        Self::from_inner(inner.by_ids(ids.into_iter().map(|id| id.to_key_value())))
702    }
703
704    /// Mark this intent as a delete query.
705    #[must_use]
706    pub fn delete(mut self) -> Self {
707        self.inner = self.inner.delete();
708        self
709    }
710
711    /// Apply a limit to the current mode.
712    ///
713    /// Load limits bound result size; delete limits bound mutation size.
714    /// For scalar load queries, any use of `limit` or `offset` requires an
715    /// explicit `order_term(...)` so pagination is deterministic.
716    /// GROUP BY queries use canonical grouped-key order by default.
717    #[must_use]
718    pub fn limit(mut self, limit: u32) -> Self {
719        self.inner = self.inner.limit(limit);
720        self
721    }
722
723    /// Apply an offset to the current mode.
724    ///
725    /// Scalar load pagination requires an explicit `order_term(...)`.
726    /// GROUP BY queries use canonical grouped-key order by default.
727    /// Delete mode applies this after ordering and predicate filtering.
728    #[must_use]
729    pub fn offset(mut self, offset: u32) -> Self {
730        self.inner = self.inner.offset(offset);
731        self
732    }
733
734    /// Explain this intent without executing it.
735    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
736        let plan = self.planned()?;
737
738        Ok(plan.explain())
739    }
740
741    /// Return a stable plan hash for this intent.
742    ///
743    /// The hash is derived from canonical planner contracts and is suitable
744    /// for diagnostics, explain diffing, and cache key construction.
745    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
746        let plan = self.inner.build_plan()?;
747
748        Ok(plan.fingerprint().to_string())
749    }
750
751    /// Plan this intent into a neutral planned query contract.
752    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
753        self.map_plan_for_visibility(None, Self::planned_query_from_plan)
754    }
755
756    /// Compile this intent into query-owned handoff state.
757    ///
758    /// This boundary intentionally does not expose executor runtime shape.
759    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
760        self.map_plan_for_visibility(None, Self::compiled_query_from_plan)
761    }
762
763    #[cfg(test)]
764    pub(in crate::db) fn plan_with_visible_indexes(
765        &self,
766        visible_indexes: &VisibleIndexes<'_>,
767    ) -> Result<CompiledQuery<E>, QueryError> {
768        self.map_plan_for_visibility(Some(visible_indexes), Self::compiled_query_from_plan)
769    }
770}
771
772impl<E> Query<E>
773where
774    E: EntityKind + SingletonEntity,
775    E::Key: Default,
776{
777    /// Set the access path to the singleton primary key.
778    pub(in crate::db) fn only(self) -> Self {
779        let Self { inner, .. } = self;
780
781        Self::from_inner(inner.only(E::Key::default().to_key_value()))
782    }
783}