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