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