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
6use crate::{
7    db::{
8        executor::{
9            assemble_aggregate_terminal_execution_descriptor_with_model,
10            assemble_load_execution_node_descriptor_with_model,
11            assemble_load_execution_verbose_diagnostics_with_model,
12        },
13        predicate::{CoercionId, CompareOp, MissingRowPolicy, Predicate},
14        query::{
15            builder::aggregate::AggregateExpr,
16            explain::{
17                ExplainAccessPath, ExplainAggregateTerminalPlan, ExplainExecutionNodeDescriptor,
18                ExplainExecutionNodeType, ExplainOrderPushdown, ExplainPlan, ExplainPredicate,
19            },
20            expr::{FilterExpr, SortExpr},
21            intent::{QueryError, model::QueryModel},
22            plan::{AccessPlannedQuery, LoadSpec, QueryMode},
23        },
24    },
25    traits::{EntityKind, EntityValue, FieldValue, SingletonEntity},
26    value::Value,
27};
28use core::marker::PhantomData;
29
30///
31/// StructuralQuery
32///
33/// Generic-free query-intent core shared by typed `Query<E>` wrappers.
34/// Stores model-level key access as `Value` so only typed key-entry helpers
35/// remain entity-specific at the outer API boundary.
36///
37
38#[derive(Debug)]
39pub(in crate::db) struct StructuralQuery {
40    intent: QueryModel<'static, Value>,
41}
42
43impl StructuralQuery {
44    #[must_use]
45    pub(in crate::db) const fn new(
46        model: &'static crate::model::entity::EntityModel,
47        consistency: MissingRowPolicy,
48    ) -> Self {
49        Self {
50            intent: QueryModel::new(model, consistency),
51        }
52    }
53
54    #[must_use]
55    const fn mode(&self) -> QueryMode {
56        self.intent.mode()
57    }
58
59    #[must_use]
60    fn has_explicit_order(&self) -> bool {
61        self.intent.has_explicit_order()
62    }
63
64    #[must_use]
65    const fn has_grouping(&self) -> bool {
66        self.intent.has_grouping()
67    }
68
69    #[must_use]
70    const fn load_spec(&self) -> Option<LoadSpec> {
71        match self.intent.mode() {
72            QueryMode::Load(spec) => Some(spec),
73            QueryMode::Delete(_) => None,
74        }
75    }
76
77    #[must_use]
78    pub(in crate::db) fn filter(mut self, predicate: Predicate) -> Self {
79        self.intent = self.intent.filter(predicate);
80        self
81    }
82
83    fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
84        let Self { intent } = self;
85        let intent = intent.filter_expr(expr)?;
86
87        Ok(Self { intent })
88    }
89
90    fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
91        let Self { intent } = self;
92        let intent = intent.sort_expr(expr)?;
93
94        Ok(Self { intent })
95    }
96
97    #[must_use]
98    pub(in crate::db) fn order_by(mut self, field: impl AsRef<str>) -> Self {
99        self.intent = self.intent.order_by(field);
100        self
101    }
102
103    #[must_use]
104    pub(in crate::db) fn order_by_desc(mut self, field: impl AsRef<str>) -> Self {
105        self.intent = self.intent.order_by_desc(field);
106        self
107    }
108
109    #[must_use]
110    pub(in crate::db) fn distinct(mut self) -> Self {
111        self.intent = self.intent.distinct();
112        self
113    }
114
115    #[cfg(feature = "sql")]
116    #[must_use]
117    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
118    where
119        I: IntoIterator<Item = S>,
120        S: Into<String>,
121    {
122        self.intent = self.intent.select_fields(fields);
123        self
124    }
125
126    pub(in crate::db) fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
127        let Self { intent } = self;
128        let intent = intent.push_group_field(field.as_ref())?;
129
130        Ok(Self { intent })
131    }
132
133    #[must_use]
134    pub(in crate::db) fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
135        self.intent = self.intent.push_group_aggregate(aggregate);
136        self
137    }
138
139    #[must_use]
140    fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
141        self.intent = self.intent.grouped_limits(max_groups, max_group_bytes);
142        self
143    }
144
145    pub(in crate::db) fn having_group(
146        self,
147        field: impl AsRef<str>,
148        op: CompareOp,
149        value: Value,
150    ) -> Result<Self, QueryError> {
151        let field = field.as_ref().to_owned();
152        let Self { intent } = self;
153        let intent = intent.push_having_group_clause(&field, op, value)?;
154
155        Ok(Self { intent })
156    }
157
158    pub(in crate::db) fn having_aggregate(
159        self,
160        aggregate_index: usize,
161        op: CompareOp,
162        value: Value,
163    ) -> Result<Self, QueryError> {
164        let Self { intent } = self;
165        let intent = intent.push_having_aggregate_clause(aggregate_index, op, value)?;
166
167        Ok(Self { intent })
168    }
169
170    #[must_use]
171    fn by_id(self, id: Value) -> Self {
172        let Self { intent } = self;
173        Self {
174            intent: intent.by_id(id),
175        }
176    }
177
178    #[must_use]
179    fn by_ids<I>(self, ids: I) -> Self
180    where
181        I: IntoIterator<Item = Value>,
182    {
183        let Self { intent } = self;
184        Self {
185            intent: intent.by_ids(ids),
186        }
187    }
188
189    #[must_use]
190    fn only(self, id: Value) -> Self {
191        let Self { intent } = self;
192
193        Self {
194            intent: intent.only(id),
195        }
196    }
197
198    #[must_use]
199    pub(in crate::db) fn delete(mut self) -> Self {
200        self.intent = self.intent.delete();
201        self
202    }
203
204    #[must_use]
205    pub(in crate::db) fn limit(mut self, limit: u32) -> Self {
206        self.intent = self.intent.limit(limit);
207        self
208    }
209
210    #[must_use]
211    pub(in crate::db) fn offset(mut self, offset: u32) -> Self {
212        self.intent = self.intent.offset(offset);
213        self
214    }
215
216    pub(in crate::db) fn build_plan(&self) -> Result<AccessPlannedQuery, QueryError> {
217        self.intent.build_plan_model()
218    }
219
220    #[must_use]
221    pub(in crate::db) const fn model(&self) -> &'static crate::model::entity::EntityModel {
222        self.intent.model()
223    }
224
225    // Explain one scalar load execution shape through the structural query core.
226    pub(in crate::db) fn explain_execution(
227        &self,
228    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
229        let plan = self.build_plan()?;
230
231        assemble_load_execution_node_descriptor_with_model(self.intent.model(), &plan)
232            .map_err(QueryError::execute)
233    }
234
235    // Render one deterministic scalar load execution tree through the shared
236    // structural descriptor path.
237    pub(in crate::db) fn explain_execution_text(&self) -> Result<String, QueryError> {
238        Ok(self.explain_execution()?.render_text_tree())
239    }
240
241    // Render one canonical scalar load execution JSON payload through the shared
242    // structural descriptor path.
243    pub(in crate::db) fn explain_execution_json(&self) -> Result<String, QueryError> {
244        Ok(self.explain_execution()?.render_json_canonical())
245    }
246
247    // Render one verbose scalar load execution payload through the shared
248    // structural descriptor and route-diagnostics paths.
249    pub(in crate::db) fn explain_execution_verbose(&self) -> Result<String, QueryError> {
250        let plan = self.build_plan()?;
251        let descriptor =
252            assemble_load_execution_node_descriptor_with_model(self.intent.model(), &plan)
253                .map_err(QueryError::execute)?;
254        let route_diagnostics =
255            assemble_load_execution_verbose_diagnostics_with_model(self.intent.model(), &plan)
256                .map_err(QueryError::execute)?;
257        let explain = plan.explain_with_model(self.intent.model());
258
259        // Phase 1: render descriptor tree with node-local metadata.
260        let mut lines = vec![descriptor.render_text_tree_verbose()];
261        lines.extend(route_diagnostics);
262
263        // Phase 2: add descriptor-stage summaries for key execution operators.
264        lines.push(format!(
265            "diagnostic.descriptor.has_top_n_seek={}",
266            contains_execution_node_type(&descriptor, ExplainExecutionNodeType::TopNSeek)
267        ));
268        lines.push(format!(
269            "diagnostic.descriptor.has_index_range_limit_pushdown={}",
270            contains_execution_node_type(
271                &descriptor,
272                ExplainExecutionNodeType::IndexRangeLimitPushdown,
273            )
274        ));
275        lines.push(format!(
276            "diagnostic.descriptor.has_index_predicate_prefilter={}",
277            contains_execution_node_type(
278                &descriptor,
279                ExplainExecutionNodeType::IndexPredicatePrefilter,
280            )
281        ));
282        lines.push(format!(
283            "diagnostic.descriptor.has_residual_predicate_filter={}",
284            contains_execution_node_type(
285                &descriptor,
286                ExplainExecutionNodeType::ResidualPredicateFilter,
287            )
288        ));
289
290        // Phase 3: append logical-plan diagnostics relevant to verbose explain.
291        lines.push(format!("diagnostic.plan.mode={:?}", explain.mode()));
292        lines.push(format!(
293            "diagnostic.plan.order_pushdown={}",
294            plan_order_pushdown_label(explain.order_pushdown())
295        ));
296        lines.push(format!(
297            "diagnostic.plan.predicate_pushdown={}",
298            plan_predicate_pushdown_label(explain.predicate(), explain.access())
299        ));
300        lines.push(format!("diagnostic.plan.distinct={}", explain.distinct()));
301        lines.push(format!("diagnostic.plan.page={:?}", explain.page()));
302        lines.push(format!(
303            "diagnostic.plan.consistency={:?}",
304            explain.consistency()
305        ));
306
307        Ok(lines.join("\n"))
308    }
309
310    // Build one aggregate-terminal explain payload through the shared structural
311    // query and execution-descriptor path.
312    pub(in crate::db) fn explain_aggregate_terminal(
313        &self,
314        aggregate: AggregateExpr,
315    ) -> Result<ExplainAggregateTerminalPlan, QueryError> {
316        let plan = self.build_plan()?;
317        let query_explain = plan.explain_with_model(self.intent.model());
318        let terminal = aggregate.kind();
319        let execution = assemble_aggregate_terminal_execution_descriptor_with_model(
320            self.intent.model(),
321            &plan,
322            aggregate,
323        );
324
325        Ok(ExplainAggregateTerminalPlan::new(
326            query_explain,
327            terminal,
328            execution,
329        ))
330    }
331}
332
333///
334/// PlannedQueryCore
335///
336/// Generic-free planned-query payload shared by typed planned-query wrappers
337/// so explain and plan-hash logic stay structural while public callers retain
338/// entity-specific type inference.
339///
340
341#[derive(Debug)]
342struct PlannedQueryCore {
343    model: &'static crate::model::entity::EntityModel,
344    plan: AccessPlannedQuery,
345}
346
347impl PlannedQueryCore {
348    #[must_use]
349    const fn new(
350        model: &'static crate::model::entity::EntityModel,
351        plan: AccessPlannedQuery,
352    ) -> Self {
353        Self { model, plan }
354    }
355
356    #[must_use]
357    fn explain(&self) -> ExplainPlan {
358        self.plan.explain_with_model(self.model)
359    }
360
361    /// Return the stable plan hash for this planned query.
362    #[must_use]
363    fn plan_hash_hex(&self) -> String {
364        self.plan.fingerprint().to_string()
365    }
366}
367
368///
369/// PlannedQuery
370///
371/// Typed planned-query shell over one generic-free planner contract.
372/// This preserves caller-side entity inference while keeping the stored plan
373/// payload and explain/hash logic structural.
374///
375
376#[derive(Debug)]
377pub struct PlannedQuery<E: EntityKind> {
378    inner: PlannedQueryCore,
379    _marker: PhantomData<E>,
380}
381
382impl<E: EntityKind> PlannedQuery<E> {
383    #[must_use]
384    const fn from_inner(inner: PlannedQueryCore) -> Self {
385        Self {
386            inner,
387            _marker: PhantomData,
388        }
389    }
390
391    #[must_use]
392    pub fn explain(&self) -> ExplainPlan {
393        self.inner.explain()
394    }
395
396    /// Return the stable plan hash for this planned query.
397    #[must_use]
398    pub fn plan_hash_hex(&self) -> String {
399        self.inner.plan_hash_hex()
400    }
401}
402
403///
404/// CompiledQueryCore
405///
406/// Generic-free compiled-query payload shared by typed compiled-query wrappers
407/// so executor handoff state remains structural until the final typed adapter
408/// boundary.
409///
410
411#[derive(Clone, Debug)]
412struct CompiledQueryCore {
413    model: &'static crate::model::entity::EntityModel,
414    entity_path: &'static str,
415    plan: AccessPlannedQuery,
416}
417
418impl CompiledQueryCore {
419    #[must_use]
420    const fn new(
421        model: &'static crate::model::entity::EntityModel,
422        entity_path: &'static str,
423        plan: AccessPlannedQuery,
424    ) -> Self {
425        Self {
426            model,
427            entity_path,
428            plan,
429        }
430    }
431
432    #[must_use]
433    fn explain(&self) -> ExplainPlan {
434        self.plan.explain_with_model(self.model)
435    }
436
437    /// Return the stable plan hash for this compiled query.
438    #[must_use]
439    fn plan_hash_hex(&self) -> String {
440        self.plan.fingerprint().to_string()
441    }
442
443    #[must_use]
444    #[cfg(feature = "sql")]
445    fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
446        self.plan.projection_spec(self.model)
447    }
448
449    #[must_use]
450    fn into_inner(self) -> AccessPlannedQuery {
451        self.plan
452    }
453}
454
455///
456/// CompiledQuery
457///
458/// Typed compiled-query shell over one generic-free planner contract.
459/// The outer entity marker restores inference for executor handoff sites
460/// while the stored execution payload remains structural.
461///
462
463#[derive(Clone, Debug)]
464pub struct CompiledQuery<E: EntityKind> {
465    inner: CompiledQueryCore,
466    _marker: PhantomData<E>,
467}
468
469impl<E: EntityKind> CompiledQuery<E> {
470    #[must_use]
471    const fn from_inner(inner: CompiledQueryCore) -> Self {
472        Self {
473            inner,
474            _marker: PhantomData,
475        }
476    }
477
478    #[must_use]
479    pub fn explain(&self) -> ExplainPlan {
480        self.inner.explain()
481    }
482
483    /// Return the stable plan hash for this compiled query.
484    #[must_use]
485    pub fn plan_hash_hex(&self) -> String {
486        self.inner.plan_hash_hex()
487    }
488
489    #[must_use]
490    #[cfg(feature = "sql")]
491    pub(in crate::db) fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
492        self.inner.projection_spec()
493    }
494
495    /// Convert one structural compiled query into an executor-ready typed plan.
496    pub(in crate::db) fn into_executable(self) -> crate::db::executor::ExecutablePlan<E> {
497        assert!(
498            self.inner.entity_path == E::PATH,
499            "compiled query entity mismatch: compiled for '{}', requested '{}'",
500            self.inner.entity_path,
501            E::PATH,
502        );
503
504        crate::db::executor::ExecutablePlan::new(self.into_inner())
505    }
506
507    #[must_use]
508    pub(in crate::db) fn into_inner(self) -> AccessPlannedQuery {
509        self.inner.into_inner()
510    }
511}
512
513///
514/// Query
515///
516/// Typed, declarative query intent for a specific entity type.
517///
518/// This intent is:
519/// - schema-agnostic at construction
520/// - normalized and validated only during planning
521/// - free of access-path decisions
522///
523
524#[derive(Debug)]
525pub struct Query<E: EntityKind> {
526    inner: StructuralQuery,
527    _marker: PhantomData<E>,
528}
529
530impl<E: EntityKind> Query<E> {
531    // Rebind one structural query core to the typed `Query<E>` surface.
532    pub(in crate::db) const fn from_inner(inner: StructuralQuery) -> Self {
533        Self {
534            inner,
535            _marker: PhantomData,
536        }
537    }
538
539    /// Create a new intent with an explicit missing-row policy.
540    /// Ignore favors idempotency and may mask index/data divergence on deletes.
541    /// Use Error to surface missing rows during scan/delete execution.
542    #[must_use]
543    pub const fn new(consistency: MissingRowPolicy) -> Self {
544        Self::from_inner(StructuralQuery::new(E::MODEL, consistency))
545    }
546
547    /// Return the intent mode (load vs delete).
548    #[must_use]
549    pub const fn mode(&self) -> QueryMode {
550        self.inner.mode()
551    }
552
553    #[must_use]
554    pub(crate) fn has_explicit_order(&self) -> bool {
555        self.inner.has_explicit_order()
556    }
557
558    #[must_use]
559    pub(crate) const fn has_grouping(&self) -> bool {
560        self.inner.has_grouping()
561    }
562
563    #[must_use]
564    pub(crate) const fn load_spec(&self) -> Option<LoadSpec> {
565        self.inner.load_spec()
566    }
567
568    /// Add a predicate, implicitly AND-ing with any existing predicate.
569    #[must_use]
570    pub fn filter(mut self, predicate: Predicate) -> Self {
571        self.inner = self.inner.filter(predicate);
572        self
573    }
574
575    /// Apply a dynamic filter expression.
576    pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
577        let Self { inner, .. } = self;
578        let inner = inner.filter_expr(expr)?;
579
580        Ok(Self::from_inner(inner))
581    }
582
583    /// Apply a dynamic sort expression.
584    pub fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
585        let Self { inner, .. } = self;
586        let inner = inner.sort_expr(expr)?;
587
588        Ok(Self::from_inner(inner))
589    }
590
591    /// Append an ascending sort key.
592    #[must_use]
593    pub fn order_by(mut self, field: impl AsRef<str>) -> Self {
594        self.inner = self.inner.order_by(field);
595        self
596    }
597
598    /// Append a descending sort key.
599    #[must_use]
600    pub fn order_by_desc(mut self, field: impl AsRef<str>) -> Self {
601        self.inner = self.inner.order_by_desc(field);
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(feature = "sql")]
615    #[cfg_attr(not(test), allow(dead_code))]
616    #[must_use]
617    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
618    where
619        I: IntoIterator<Item = S>,
620        S: Into<String>,
621    {
622        self.inner = self.inner.select_fields(fields);
623        self
624    }
625
626    /// Add one GROUP BY field.
627    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
628        let Self { inner, .. } = self;
629        let inner = inner.group_by(field)?;
630
631        Ok(Self::from_inner(inner))
632    }
633
634    /// Add one aggregate terminal via composable aggregate expression.
635    #[must_use]
636    pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
637        self.inner = self.inner.aggregate(aggregate);
638        self
639    }
640
641    /// Override grouped hard limits for grouped execution budget enforcement.
642    #[must_use]
643    pub fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
644        self.inner = self.inner.grouped_limits(max_groups, max_group_bytes);
645        self
646    }
647
648    /// Add one grouped HAVING compare clause over one grouped key field.
649    pub fn having_group(
650        self,
651        field: impl AsRef<str>,
652        op: CompareOp,
653        value: Value,
654    ) -> Result<Self, QueryError> {
655        let Self { inner, .. } = self;
656        let inner = inner.having_group(field, op, value)?;
657
658        Ok(Self::from_inner(inner))
659    }
660
661    /// Add one grouped HAVING compare clause over one grouped aggregate output.
662    pub fn having_aggregate(
663        self,
664        aggregate_index: usize,
665        op: CompareOp,
666        value: Value,
667    ) -> Result<Self, QueryError> {
668        let Self { inner, .. } = self;
669        let inner = inner.having_aggregate(aggregate_index, op, value)?;
670
671        Ok(Self::from_inner(inner))
672    }
673
674    /// Set the access path to a single primary key lookup.
675    pub(crate) fn by_id(self, id: E::Key) -> Self {
676        let Self { inner, .. } = self;
677
678        Self::from_inner(inner.by_id(id.to_value()))
679    }
680
681    /// Set the access path to a primary key batch lookup.
682    pub(crate) fn by_ids<I>(self, ids: I) -> Self
683    where
684        I: IntoIterator<Item = E::Key>,
685    {
686        let Self { inner, .. } = self;
687
688        Self::from_inner(inner.by_ids(ids.into_iter().map(|id| id.to_value())))
689    }
690
691    /// Mark this intent as a delete query.
692    #[must_use]
693    pub fn delete(mut self) -> Self {
694        self.inner = self.inner.delete();
695        self
696    }
697
698    /// Apply a limit to the current mode.
699    ///
700    /// Load limits bound result size; delete limits bound mutation size.
701    /// For scalar load queries, any use of `limit` or `offset` requires an
702    /// explicit `order_by(...)` so pagination is deterministic.
703    /// GROUP BY queries use canonical grouped-key order by default.
704    #[must_use]
705    pub fn limit(mut self, limit: u32) -> Self {
706        self.inner = self.inner.limit(limit);
707        self
708    }
709
710    /// Apply an offset to a load intent.
711    ///
712    /// Scalar pagination requires an explicit `order_by(...)`.
713    /// GROUP BY queries use canonical grouped-key order by default.
714    /// Delete intents reject `offset(...)` during planning.
715    #[must_use]
716    pub fn offset(mut self, offset: u32) -> Self {
717        self.inner = self.inner.offset(offset);
718        self
719    }
720
721    /// Explain this intent without executing it.
722    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
723        let plan = self.planned()?;
724
725        Ok(plan.explain())
726    }
727
728    /// Return a stable plan hash for this intent.
729    ///
730    /// The hash is derived from canonical planner contracts and is suitable
731    /// for diagnostics, explain diffing, and cache key construction.
732    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
733        let plan = self.inner.build_plan()?;
734
735        Ok(plan.fingerprint().to_string())
736    }
737
738    /// Explain executor-selected scalar load execution shape without running it.
739    pub fn explain_execution(&self) -> Result<ExplainExecutionNodeDescriptor, QueryError>
740    where
741        E: EntityValue,
742    {
743        self.inner.explain_execution()
744    }
745
746    /// Explain executor-selected scalar load execution shape as deterministic text.
747    pub fn explain_execution_text(&self) -> Result<String, QueryError>
748    where
749        E: EntityValue,
750    {
751        self.inner.explain_execution_text()
752    }
753
754    /// Explain executor-selected scalar load execution shape as canonical JSON.
755    pub fn explain_execution_json(&self) -> Result<String, QueryError>
756    where
757        E: EntityValue,
758    {
759        self.inner.explain_execution_json()
760    }
761
762    /// Explain executor-selected scalar load execution shape with route diagnostics.
763    pub fn explain_execution_verbose(&self) -> Result<String, QueryError>
764    where
765        E: EntityValue,
766    {
767        self.inner.explain_execution_verbose()
768    }
769
770    /// Plan this intent into a neutral planned query contract.
771    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
772        let plan = self.inner.build_plan()?;
773        let _projection = plan.projection_spec(E::MODEL);
774
775        Ok(PlannedQuery::from_inner(PlannedQueryCore::new(
776            E::MODEL,
777            plan,
778        )))
779    }
780
781    /// Compile this intent into query-owned handoff state.
782    ///
783    /// This boundary intentionally does not expose executor runtime shape.
784    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
785        let plan = self.inner.build_plan()?;
786        let _projection = plan.projection_spec(E::MODEL);
787
788        Ok(CompiledQuery::from_inner(CompiledQueryCore::new(
789            E::MODEL,
790            E::PATH,
791            plan,
792        )))
793    }
794}
795
796fn contains_execution_node_type(
797    descriptor: &ExplainExecutionNodeDescriptor,
798    target: ExplainExecutionNodeType,
799) -> bool {
800    descriptor.node_type() == target
801        || descriptor
802            .children()
803            .iter()
804            .any(|child| contains_execution_node_type(child, target))
805}
806
807fn plan_order_pushdown_label(order_pushdown: &ExplainOrderPushdown) -> String {
808    match order_pushdown {
809        ExplainOrderPushdown::MissingModelContext => "missing_model_context".to_string(),
810        ExplainOrderPushdown::EligibleSecondaryIndex { index, prefix_len } => {
811            format!("eligible(index={index},prefix_len={prefix_len})",)
812        }
813        ExplainOrderPushdown::Rejected(reason) => format!("rejected({reason:?})"),
814    }
815}
816
817fn plan_predicate_pushdown_label(
818    predicate: &ExplainPredicate,
819    access: &ExplainAccessPath,
820) -> String {
821    let access_label = match access {
822        ExplainAccessPath::ByKey { .. } => "by_key",
823        ExplainAccessPath::ByKeys { keys } if keys.is_empty() => "empty_access_contract",
824        ExplainAccessPath::ByKeys { .. } => "by_keys",
825        ExplainAccessPath::KeyRange { .. } => "key_range",
826        ExplainAccessPath::IndexPrefix { .. } => "index_prefix",
827        ExplainAccessPath::IndexMultiLookup { .. } => "index_multi_lookup",
828        ExplainAccessPath::IndexRange { .. } => "index_range",
829        ExplainAccessPath::FullScan => "full_scan",
830        ExplainAccessPath::Union(_) => "union",
831        ExplainAccessPath::Intersection(_) => "intersection",
832    };
833    if matches!(predicate, ExplainPredicate::None) {
834        return "none".to_string();
835    }
836    if matches!(access, ExplainAccessPath::FullScan) {
837        if explain_predicate_contains_non_strict_compare(predicate) {
838            return "fallback(non_strict_compare_coercion)".to_string();
839        }
840        if explain_predicate_contains_empty_prefix_starts_with(predicate) {
841            return "fallback(starts_with_empty_prefix)".to_string();
842        }
843        if explain_predicate_contains_is_null(predicate) {
844            return "fallback(is_null_full_scan)".to_string();
845        }
846        if explain_predicate_contains_text_scan_operator(predicate) {
847            return "fallback(text_operator_full_scan)".to_string();
848        }
849
850        return format!("fallback({access_label})");
851    }
852
853    format!("applied({access_label})")
854}
855
856fn explain_predicate_contains_non_strict_compare(predicate: &ExplainPredicate) -> bool {
857    match predicate {
858        ExplainPredicate::Compare { coercion, .. } => coercion.id != CoercionId::Strict,
859        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
860            .iter()
861            .any(explain_predicate_contains_non_strict_compare),
862        ExplainPredicate::Not(inner) => explain_predicate_contains_non_strict_compare(inner),
863        ExplainPredicate::None
864        | ExplainPredicate::True
865        | ExplainPredicate::False
866        | ExplainPredicate::IsNull { .. }
867        | ExplainPredicate::IsNotNull { .. }
868        | ExplainPredicate::IsMissing { .. }
869        | ExplainPredicate::IsEmpty { .. }
870        | ExplainPredicate::IsNotEmpty { .. }
871        | ExplainPredicate::TextContains { .. }
872        | ExplainPredicate::TextContainsCi { .. } => false,
873    }
874}
875
876fn explain_predicate_contains_is_null(predicate: &ExplainPredicate) -> bool {
877    match predicate {
878        ExplainPredicate::IsNull { .. } => true,
879        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => {
880            children.iter().any(explain_predicate_contains_is_null)
881        }
882        ExplainPredicate::Not(inner) => explain_predicate_contains_is_null(inner),
883        ExplainPredicate::None
884        | ExplainPredicate::True
885        | ExplainPredicate::False
886        | ExplainPredicate::Compare { .. }
887        | ExplainPredicate::IsNotNull { .. }
888        | ExplainPredicate::IsMissing { .. }
889        | ExplainPredicate::IsEmpty { .. }
890        | ExplainPredicate::IsNotEmpty { .. }
891        | ExplainPredicate::TextContains { .. }
892        | ExplainPredicate::TextContainsCi { .. } => false,
893    }
894}
895
896fn explain_predicate_contains_empty_prefix_starts_with(predicate: &ExplainPredicate) -> bool {
897    match predicate {
898        ExplainPredicate::Compare {
899            op: CompareOp::StartsWith,
900            value: Value::Text(prefix),
901            ..
902        } => prefix.is_empty(),
903        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
904            .iter()
905            .any(explain_predicate_contains_empty_prefix_starts_with),
906        ExplainPredicate::Not(inner) => explain_predicate_contains_empty_prefix_starts_with(inner),
907        ExplainPredicate::None
908        | ExplainPredicate::True
909        | ExplainPredicate::False
910        | ExplainPredicate::Compare { .. }
911        | ExplainPredicate::IsNull { .. }
912        | ExplainPredicate::IsNotNull { .. }
913        | ExplainPredicate::IsMissing { .. }
914        | ExplainPredicate::IsEmpty { .. }
915        | ExplainPredicate::IsNotEmpty { .. }
916        | ExplainPredicate::TextContains { .. }
917        | ExplainPredicate::TextContainsCi { .. } => false,
918    }
919}
920
921fn explain_predicate_contains_text_scan_operator(predicate: &ExplainPredicate) -> bool {
922    match predicate {
923        ExplainPredicate::Compare {
924            op: CompareOp::EndsWith,
925            ..
926        }
927        | ExplainPredicate::TextContains { .. }
928        | ExplainPredicate::TextContainsCi { .. } => true,
929        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
930            .iter()
931            .any(explain_predicate_contains_text_scan_operator),
932        ExplainPredicate::Not(inner) => explain_predicate_contains_text_scan_operator(inner),
933        ExplainPredicate::Compare { .. }
934        | ExplainPredicate::None
935        | ExplainPredicate::True
936        | ExplainPredicate::False
937        | ExplainPredicate::IsNull { .. }
938        | ExplainPredicate::IsNotNull { .. }
939        | ExplainPredicate::IsMissing { .. }
940        | ExplainPredicate::IsEmpty { .. }
941        | ExplainPredicate::IsNotEmpty { .. } => false,
942    }
943}
944
945impl<E> Query<E>
946where
947    E: EntityKind + SingletonEntity,
948    E::Key: Default,
949{
950    /// Set the access path to the singleton primary key.
951    pub(crate) fn only(self) -> Self {
952        let Self { inner, .. } = self;
953
954        Self::from_inner(inner.only(E::Key::default().to_value()))
955    }
956}