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        executor::{
11            BytesByProjectionMode, PreparedExecutionPlan, SharedPreparedExecutionPlan,
12            assemble_aggregate_terminal_execution_descriptor,
13            assemble_load_execution_node_descriptor, assemble_load_execution_verbose_diagnostics,
14            planning::route::AggregateRouteShape,
15        },
16        predicate::{CoercionId, CompareOp, MissingRowPolicy, Predicate},
17        query::{
18            builder::{
19                AggregateExpr, PreparedFluentAggregateExplainStrategy,
20                PreparedFluentProjectionStrategy,
21            },
22            explain::{
23                ExplainAccessPath, ExplainAggregateTerminalPlan, ExplainExecutionNodeDescriptor,
24                ExplainExecutionNodeType, ExplainOrderPushdown, ExplainPlan, ExplainPredicate,
25            },
26            expr::{FilterExpr, OrderTerm as FluentOrderTerm},
27            intent::{
28                QueryError,
29                model::{PreparedScalarPlanningState, QueryModel},
30            },
31            plan::{
32                AccessPlannedQuery, LoadSpec, OrderSpec, QueryMode, VisibleIndexes, expr::Expr,
33            },
34        },
35    },
36    traits::{EntityKind, EntityValue, FieldValue, SingletonEntity},
37    value::Value,
38};
39use core::marker::PhantomData;
40
41///
42/// StructuralQuery
43///
44/// Generic-free query-intent core shared by typed `Query<E>` wrappers.
45/// Stores model-level key access as `Value` so only typed key-entry helpers
46/// remain entity-specific at the outer API boundary.
47///
48
49#[derive(Clone, Debug)]
50pub(in crate::db) struct StructuralQuery {
51    intent: QueryModel<'static, Value>,
52}
53
54impl StructuralQuery {
55    #[must_use]
56    pub(in crate::db) const fn new(
57        model: &'static crate::model::entity::EntityModel,
58        consistency: MissingRowPolicy,
59    ) -> Self {
60        Self {
61            intent: QueryModel::new(model, consistency),
62        }
63    }
64
65    // Rewrap one updated generic-free intent model back into the structural
66    // query shell so local transformation helpers do not rebuild `Self`
67    // ad hoc at each boundary method.
68    const fn from_intent(intent: QueryModel<'static, Value>) -> Self {
69        Self { intent }
70    }
71
72    // Apply one infallible intent transformation while preserving the
73    // structural query shell at this boundary.
74    fn map_intent(
75        self,
76        map: impl FnOnce(QueryModel<'static, Value>) -> QueryModel<'static, Value>,
77    ) -> Self {
78        Self::from_intent(map(self.intent))
79    }
80
81    // Apply one fallible intent transformation while keeping result wrapping
82    // local to the structural query boundary.
83    fn try_map_intent(
84        self,
85        map: impl FnOnce(QueryModel<'static, Value>) -> Result<QueryModel<'static, Value>, QueryError>,
86    ) -> Result<Self, QueryError> {
87        map(self.intent).map(Self::from_intent)
88    }
89
90    #[must_use]
91    const fn mode(&self) -> QueryMode {
92        self.intent.mode()
93    }
94
95    #[must_use]
96    fn has_explicit_order(&self) -> bool {
97        self.intent.has_explicit_order()
98    }
99
100    #[must_use]
101    pub(in crate::db) const fn has_grouping(&self) -> bool {
102        self.intent.has_grouping()
103    }
104
105    #[must_use]
106    const fn load_spec(&self) -> Option<LoadSpec> {
107        match self.intent.mode() {
108            QueryMode::Load(spec) => Some(spec),
109            QueryMode::Delete(_) => None,
110        }
111    }
112
113    #[must_use]
114    pub(in crate::db) fn filter(mut self, predicate: Predicate) -> Self {
115        self.intent = self.intent.filter(predicate);
116        self
117    }
118
119    fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
120        self.try_map_intent(|intent| intent.filter_expr(expr))
121    }
122
123    #[must_use]
124    pub(in crate::db) fn order_term(mut self, term: FluentOrderTerm) -> Self {
125        self.intent = self.intent.order_term(term);
126        self
127    }
128
129    #[must_use]
130    pub(in crate::db) fn order_spec(mut self, order: OrderSpec) -> Self {
131        self.intent = self.intent.order_spec(order);
132        self
133    }
134
135    #[must_use]
136    pub(in crate::db) fn distinct(mut self) -> Self {
137        self.intent = self.intent.distinct();
138        self
139    }
140
141    #[cfg(feature = "sql")]
142    #[must_use]
143    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
144    where
145        I: IntoIterator<Item = S>,
146        S: Into<String>,
147    {
148        self.intent = self.intent.select_fields(fields);
149        self
150    }
151
152    #[cfg(feature = "sql")]
153    #[must_use]
154    pub(in crate::db) fn projection_selection(mut self, selection: ProjectionSelection) -> Self {
155        self.intent = self.intent.projection_selection(selection);
156        self
157    }
158
159    pub(in crate::db) fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
160        self.try_map_intent(|intent| intent.push_group_field(field.as_ref()))
161    }
162
163    #[must_use]
164    pub(in crate::db) fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
165        self.intent = self.intent.push_group_aggregate(aggregate);
166        self
167    }
168
169    #[must_use]
170    fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
171        self.intent = self.intent.grouped_limits(max_groups, max_group_bytes);
172        self
173    }
174
175    pub(in crate::db) fn having_group(
176        self,
177        field: impl AsRef<str>,
178        op: CompareOp,
179        value: Value,
180    ) -> Result<Self, QueryError> {
181        let field = field.as_ref().to_owned();
182        self.try_map_intent(|intent| intent.push_having_group_clause(&field, op, value))
183    }
184
185    pub(in crate::db) fn having_aggregate(
186        self,
187        aggregate_index: usize,
188        op: CompareOp,
189        value: Value,
190    ) -> Result<Self, QueryError> {
191        self.try_map_intent(|intent| {
192            intent.push_having_aggregate_clause(aggregate_index, op, value)
193        })
194    }
195
196    pub(in crate::db) fn having_expr(self, expr: Expr) -> Result<Self, QueryError> {
197        self.try_map_intent(|intent| intent.push_having_expr(expr))
198    }
199
200    #[must_use]
201    fn by_id(self, id: Value) -> Self {
202        self.map_intent(|intent| intent.by_id(id))
203    }
204
205    #[must_use]
206    fn by_ids<I>(self, ids: I) -> Self
207    where
208        I: IntoIterator<Item = Value>,
209    {
210        self.map_intent(|intent| intent.by_ids(ids))
211    }
212
213    #[must_use]
214    fn only(self, id: Value) -> Self {
215        self.map_intent(|intent| intent.only(id))
216    }
217
218    #[must_use]
219    pub(in crate::db) fn delete(mut self) -> Self {
220        self.intent = self.intent.delete();
221        self
222    }
223
224    #[must_use]
225    pub(in crate::db) fn limit(mut self, limit: u32) -> Self {
226        self.intent = self.intent.limit(limit);
227        self
228    }
229
230    #[must_use]
231    pub(in crate::db) fn offset(mut self, offset: u32) -> Self {
232        self.intent = self.intent.offset(offset);
233        self
234    }
235
236    pub(in crate::db) fn build_plan(&self) -> Result<AccessPlannedQuery, QueryError> {
237        self.intent.build_plan_model()
238    }
239
240    pub(in crate::db) fn build_plan_with_visible_indexes(
241        &self,
242        visible_indexes: &VisibleIndexes<'_>,
243    ) -> Result<AccessPlannedQuery, QueryError> {
244        self.intent.build_plan_model_with_indexes(visible_indexes)
245    }
246
247    pub(in crate::db) fn prepare_scalar_planning_state(
248        &self,
249    ) -> Result<PreparedScalarPlanningState<'_>, QueryError> {
250        self.intent.prepare_scalar_planning_state()
251    }
252
253    pub(in crate::db) fn build_plan_with_visible_indexes_from_scalar_planning_state(
254        &self,
255        visible_indexes: &VisibleIndexes<'_>,
256        planning_state: PreparedScalarPlanningState<'_>,
257    ) -> Result<AccessPlannedQuery, QueryError> {
258        self.intent
259            .build_plan_model_with_indexes_from_scalar_planning_state(
260                visible_indexes,
261                planning_state,
262            )
263    }
264
265    #[must_use]
266    #[cfg(test)]
267    pub(in crate::db) fn structural_cache_key(
268        &self,
269    ) -> crate::db::query::intent::StructuralQueryCacheKey {
270        crate::db::query::intent::StructuralQueryCacheKey::from_query_model(&self.intent)
271    }
272
273    #[must_use]
274    pub(in crate::db) fn structural_cache_key_with_normalized_predicate_fingerprint(
275        &self,
276        predicate_fingerprint: Option<[u8; 32]>,
277    ) -> crate::db::query::intent::StructuralQueryCacheKey {
278        self.intent
279            .structural_cache_key_with_normalized_predicate_fingerprint(predicate_fingerprint)
280    }
281
282    // Build one access plan using either schema-owned indexes or the session
283    // visibility slice already resolved at the caller boundary.
284    fn build_plan_for_visibility(
285        &self,
286        visible_indexes: Option<&VisibleIndexes<'_>>,
287    ) -> Result<AccessPlannedQuery, QueryError> {
288        match visible_indexes {
289            Some(visible_indexes) => self.build_plan_with_visible_indexes(visible_indexes),
290            None => self.build_plan(),
291        }
292    }
293
294    // Assemble one canonical execution descriptor from a previously built
295    // access plan so text/json/verbose explain surfaces do not each rebuild it.
296    fn explain_execution_descriptor_from_plan(
297        &self,
298        plan: &AccessPlannedQuery,
299    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
300        assemble_load_execution_node_descriptor(
301            self.intent.model().fields(),
302            self.intent.model().primary_key().name(),
303            plan,
304        )
305        .map_err(QueryError::execute)
306    }
307
308    // Render one verbose execution explain payload from a single access plan.
309    fn explain_execution_verbose_from_plan(
310        &self,
311        plan: &AccessPlannedQuery,
312    ) -> Result<String, QueryError> {
313        let descriptor = self.explain_execution_descriptor_from_plan(plan)?;
314        let route_diagnostics = assemble_load_execution_verbose_diagnostics(
315            self.intent.model().fields(),
316            self.intent.model().primary_key().name(),
317            plan,
318        )
319        .map_err(QueryError::execute)?;
320        let explain = plan.explain();
321
322        // Phase 1: render descriptor tree with node-local metadata.
323        let mut lines = vec![descriptor.render_text_tree_verbose()];
324        lines.extend(route_diagnostics);
325
326        // Phase 2: add descriptor-stage summaries for key execution operators.
327        lines.push(format!(
328            "diag.d.has_top_n_seek={}",
329            contains_execution_node_type(&descriptor, ExplainExecutionNodeType::TopNSeek)
330        ));
331        lines.push(format!(
332            "diag.d.has_index_range_limit_pushdown={}",
333            contains_execution_node_type(
334                &descriptor,
335                ExplainExecutionNodeType::IndexRangeLimitPushdown,
336            )
337        ));
338        lines.push(format!(
339            "diag.d.has_index_predicate_prefilter={}",
340            contains_execution_node_type(
341                &descriptor,
342                ExplainExecutionNodeType::IndexPredicatePrefilter,
343            )
344        ));
345        lines.push(format!(
346            "diag.d.has_residual_predicate_filter={}",
347            contains_execution_node_type(
348                &descriptor,
349                ExplainExecutionNodeType::ResidualPredicateFilter,
350            )
351        ));
352
353        // Phase 3: append logical-plan diagnostics relevant to verbose explain.
354        lines.push(format!("diag.p.mode={:?}", explain.mode()));
355        lines.push(format!(
356            "diag.p.order_pushdown={}",
357            plan_order_pushdown_label(explain.order_pushdown())
358        ));
359        lines.push(format!(
360            "diag.p.predicate_pushdown={}",
361            plan_predicate_pushdown_label(explain.predicate(), explain.access())
362        ));
363        lines.push(format!("diag.p.distinct={}", explain.distinct()));
364        lines.push(format!("diag.p.page={:?}", explain.page()));
365        lines.push(format!("diag.p.consistency={:?}", explain.consistency()));
366
367        Ok(lines.join("\n"))
368    }
369
370    // Freeze one explain-only access-choice snapshot from the effective
371    // planner-visible index slice before building descriptor diagnostics.
372    fn finalize_explain_access_choice_for_visibility(
373        &self,
374        plan: &mut AccessPlannedQuery,
375        visible_indexes: Option<&VisibleIndexes<'_>>,
376    ) {
377        let visible_indexes = match visible_indexes {
378            Some(visible_indexes) => visible_indexes.as_slice(),
379            None => self.intent.model().indexes(),
380        };
381
382        plan.finalize_access_choice_for_model_with_indexes(self.intent.model(), visible_indexes);
383    }
384
385    // Build one execution descriptor after resolving the caller-visible index
386    // slice so text/json explain surfaces do not each duplicate plan assembly.
387    fn explain_execution_descriptor_for_visibility(
388        &self,
389        visible_indexes: Option<&VisibleIndexes<'_>>,
390    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
391        let mut plan = self.build_plan_for_visibility(visible_indexes)?;
392        self.finalize_explain_access_choice_for_visibility(&mut plan, visible_indexes);
393
394        self.explain_execution_descriptor_from_plan(&plan)
395    }
396
397    // Render one verbose execution payload after resolving the caller-visible
398    // index slice exactly once at the structural query boundary.
399    fn explain_execution_verbose_for_visibility(
400        &self,
401        visible_indexes: Option<&VisibleIndexes<'_>>,
402    ) -> Result<String, QueryError> {
403        let mut plan = self.build_plan_for_visibility(visible_indexes)?;
404        self.finalize_explain_access_choice_for_visibility(&mut plan, visible_indexes);
405
406        self.explain_execution_verbose_from_plan(&plan)
407    }
408
409    #[cfg(feature = "sql")]
410    #[must_use]
411    pub(in crate::db) const fn model(&self) -> &'static crate::model::entity::EntityModel {
412        self.intent.model()
413    }
414
415    #[inline(never)]
416    pub(in crate::db) fn explain_execution_with_visible_indexes(
417        &self,
418        visible_indexes: &VisibleIndexes<'_>,
419    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
420        self.explain_execution_descriptor_for_visibility(Some(visible_indexes))
421    }
422
423    // Explain one load execution shape through the structural query core.
424    #[inline(never)]
425    pub(in crate::db) fn explain_execution(
426        &self,
427    ) -> Result<ExplainExecutionNodeDescriptor, QueryError> {
428        self.explain_execution_descriptor_for_visibility(None)
429    }
430
431    // Render one verbose scalar load execution payload through the shared
432    // structural descriptor and route-diagnostics paths.
433    #[inline(never)]
434    pub(in crate::db) fn explain_execution_verbose(&self) -> Result<String, QueryError> {
435        self.explain_execution_verbose_for_visibility(None)
436    }
437
438    #[inline(never)]
439    pub(in crate::db) fn explain_execution_verbose_with_visible_indexes(
440        &self,
441        visible_indexes: &VisibleIndexes<'_>,
442    ) -> Result<String, QueryError> {
443        self.explain_execution_verbose_for_visibility(Some(visible_indexes))
444    }
445
446    #[inline(never)]
447    pub(in crate::db) fn explain_aggregate_terminal_with_visible_indexes(
448        &self,
449        visible_indexes: &VisibleIndexes<'_>,
450        aggregate: AggregateRouteShape<'_>,
451    ) -> Result<ExplainAggregateTerminalPlan, QueryError> {
452        let plan = self.build_plan_with_visible_indexes(visible_indexes)?;
453        let query_explain = plan.explain();
454        let terminal = aggregate.kind();
455        let execution = assemble_aggregate_terminal_execution_descriptor(&plan, aggregate);
456
457        Ok(ExplainAggregateTerminalPlan::new(
458            query_explain,
459            terminal,
460            execution,
461        ))
462    }
463
464    #[inline(never)]
465    pub(in crate::db) fn explain_prepared_aggregate_terminal_with_visible_indexes<S>(
466        &self,
467        visible_indexes: &VisibleIndexes<'_>,
468        strategy: &S,
469    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
470    where
471        S: PreparedFluentAggregateExplainStrategy,
472    {
473        let Some(kind) = strategy.explain_aggregate_kind() else {
474            return Err(QueryError::invariant(
475                "prepared fluent aggregate explain requires an explain-visible aggregate kind",
476            ));
477        };
478        let aggregate = AggregateRouteShape::new_from_fields(
479            kind,
480            strategy.explain_projected_field(),
481            self.intent.model().fields(),
482            self.intent.model().primary_key().name(),
483        );
484
485        self.explain_aggregate_terminal_with_visible_indexes(visible_indexes, aggregate)
486    }
487}
488
489///
490/// QueryPlanHandle
491///
492/// QueryPlanHandle keeps typed query DTOs compatible with both direct planner
493/// output and the shared prepared-plan cache boundary.
494/// Session-owned paths can carry the prepared artifact directly, while direct
495/// fluent builder calls can still wrap a raw logical plan without rebuilding.
496///
497
498#[derive(Clone, Debug)]
499enum QueryPlanHandle {
500    Plan(Box<AccessPlannedQuery>),
501    Prepared(SharedPreparedExecutionPlan),
502}
503
504impl QueryPlanHandle {
505    #[must_use]
506    fn from_plan(plan: AccessPlannedQuery) -> Self {
507        Self::Plan(Box::new(plan))
508    }
509
510    #[must_use]
511    const fn from_prepared(prepared_plan: SharedPreparedExecutionPlan) -> Self {
512        Self::Prepared(prepared_plan)
513    }
514
515    #[must_use]
516    fn logical_plan(&self) -> &AccessPlannedQuery {
517        match self {
518            Self::Plan(plan) => plan,
519            Self::Prepared(prepared_plan) => prepared_plan.logical_plan(),
520        }
521    }
522
523    fn into_prepared_execution_plan<E: EntityKind>(self) -> PreparedExecutionPlan<E> {
524        match self {
525            Self::Plan(plan) => PreparedExecutionPlan::new(*plan),
526            Self::Prepared(prepared_plan) => prepared_plan.typed_clone::<E>(),
527        }
528    }
529
530    #[must_use]
531    #[cfg(test)]
532    fn into_inner(self) -> AccessPlannedQuery {
533        match self {
534            Self::Plan(plan) => *plan,
535            Self::Prepared(prepared_plan) => prepared_plan.logical_plan().clone(),
536        }
537    }
538}
539
540///
541/// PlannedQuery
542///
543/// PlannedQuery keeps the typed planning surface stable while allowing the
544/// session boundary to reuse one shared prepared-plan artifact internally.
545///
546
547#[derive(Debug)]
548pub struct PlannedQuery<E: EntityKind> {
549    plan: QueryPlanHandle,
550    _marker: PhantomData<E>,
551}
552
553impl<E: EntityKind> PlannedQuery<E> {
554    #[must_use]
555    fn from_plan(plan: AccessPlannedQuery) -> Self {
556        Self {
557            plan: QueryPlanHandle::from_plan(plan),
558            _marker: PhantomData,
559        }
560    }
561
562    #[must_use]
563    pub(in crate::db) const fn from_prepared_plan(
564        prepared_plan: SharedPreparedExecutionPlan,
565    ) -> Self {
566        Self {
567            plan: QueryPlanHandle::from_prepared(prepared_plan),
568            _marker: PhantomData,
569        }
570    }
571
572    #[must_use]
573    pub fn explain(&self) -> ExplainPlan {
574        self.plan.logical_plan().explain()
575    }
576
577    /// Return the stable plan hash for this planned query.
578    #[must_use]
579    pub fn plan_hash_hex(&self) -> String {
580        self.plan.logical_plan().fingerprint().to_string()
581    }
582}
583
584///
585/// CompiledQuery
586///
587/// Typed compiled-query shell over one structural planner contract.
588/// The outer entity marker preserves executor handoff inference without
589/// carrying a second adapter object, while session-owned paths can still reuse
590/// the cached shared prepared plan directly.
591///
592
593#[derive(Clone, Debug)]
594pub struct CompiledQuery<E: EntityKind> {
595    plan: QueryPlanHandle,
596    _marker: PhantomData<E>,
597}
598
599impl<E: EntityKind> CompiledQuery<E> {
600    #[must_use]
601    fn from_plan(plan: AccessPlannedQuery) -> Self {
602        Self {
603            plan: QueryPlanHandle::from_plan(plan),
604            _marker: PhantomData,
605        }
606    }
607
608    #[must_use]
609    pub(in crate::db) const fn from_prepared_plan(
610        prepared_plan: SharedPreparedExecutionPlan,
611    ) -> Self {
612        Self {
613            plan: QueryPlanHandle::from_prepared(prepared_plan),
614            _marker: PhantomData,
615        }
616    }
617
618    #[must_use]
619    pub fn explain(&self) -> ExplainPlan {
620        self.plan.logical_plan().explain()
621    }
622
623    /// Return the stable plan hash for this compiled query.
624    #[must_use]
625    pub fn plan_hash_hex(&self) -> String {
626        self.plan.logical_plan().fingerprint().to_string()
627    }
628
629    #[must_use]
630    #[cfg(test)]
631    pub(in crate::db) fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
632        self.plan.logical_plan().projection_spec(E::MODEL)
633    }
634
635    /// Convert one structural compiled query into one prepared executor plan.
636    pub(in crate::db) fn into_prepared_execution_plan(
637        self,
638    ) -> crate::db::executor::PreparedExecutionPlan<E> {
639        self.plan.into_prepared_execution_plan::<E>()
640    }
641
642    #[must_use]
643    #[cfg(test)]
644    pub(in crate::db) fn into_inner(self) -> AccessPlannedQuery {
645        self.plan.into_inner()
646    }
647}
648
649///
650/// Query
651///
652/// Typed, declarative query intent for a specific entity type.
653///
654/// This intent is:
655/// - schema-agnostic at construction
656/// - normalized and validated only during planning
657/// - free of access-path decisions
658///
659
660#[derive(Debug)]
661pub struct Query<E: EntityKind> {
662    inner: StructuralQuery,
663    _marker: PhantomData<E>,
664}
665
666impl<E: EntityKind> Query<E> {
667    // Rebind one structural query core to the typed `Query<E>` surface.
668    pub(in crate::db) const fn from_inner(inner: StructuralQuery) -> Self {
669        Self {
670            inner,
671            _marker: PhantomData,
672        }
673    }
674
675    /// Create a new intent with an explicit missing-row policy.
676    /// Ignore favors idempotency and may mask index/data divergence on deletes.
677    /// Use Error to surface missing rows during scan/delete execution.
678    #[must_use]
679    pub const fn new(consistency: MissingRowPolicy) -> Self {
680        Self::from_inner(StructuralQuery::new(E::MODEL, consistency))
681    }
682
683    /// Return the intent mode (load vs delete).
684    #[must_use]
685    pub const fn mode(&self) -> QueryMode {
686        self.inner.mode()
687    }
688
689    pub(in crate::db) fn explain_with_visible_indexes(
690        &self,
691        visible_indexes: &VisibleIndexes<'_>,
692    ) -> Result<ExplainPlan, QueryError> {
693        let plan = self.build_plan_for_visibility(Some(visible_indexes))?;
694
695        Ok(plan.explain())
696    }
697
698    pub(in crate::db) fn plan_hash_hex_with_visible_indexes(
699        &self,
700        visible_indexes: &VisibleIndexes<'_>,
701    ) -> Result<String, QueryError> {
702        let plan = self.build_plan_for_visibility(Some(visible_indexes))?;
703
704        Ok(plan.fingerprint().to_string())
705    }
706
707    // Build one typed access plan using either schema-owned indexes or the
708    // visibility slice already resolved at the session boundary.
709    fn build_plan_for_visibility(
710        &self,
711        visible_indexes: Option<&VisibleIndexes<'_>>,
712    ) -> Result<AccessPlannedQuery, QueryError> {
713        self.inner.build_plan_for_visibility(visible_indexes)
714    }
715
716    // Build one structural plan for the requested visibility lane and then
717    // project it into one typed query-owned contract so planned vs compiled
718    // outputs do not each duplicate the same plan handoff shape.
719    fn map_plan_for_visibility<T>(
720        &self,
721        visible_indexes: Option<&VisibleIndexes<'_>>,
722        map: impl FnOnce(AccessPlannedQuery) -> T,
723    ) -> Result<T, QueryError> {
724        let plan = self.build_plan_for_visibility(visible_indexes)?;
725
726        Ok(map(plan))
727    }
728
729    // Build one typed prepared execution plan directly from the requested
730    // visibility lane so explain helpers that need executor-owned shape do not
731    // rebuild that shell through `CompiledQuery<E>`.
732    fn prepared_execution_plan_for_visibility(
733        &self,
734        visible_indexes: Option<&VisibleIndexes<'_>>,
735    ) -> Result<PreparedExecutionPlan<E>, QueryError> {
736        self.map_plan_for_visibility(visible_indexes, PreparedExecutionPlan::<E>::new)
737    }
738
739    // Wrap one built plan as the typed planned-query DTO.
740    pub(in crate::db) fn planned_query_from_plan(plan: AccessPlannedQuery) -> PlannedQuery<E> {
741        PlannedQuery::from_plan(plan)
742    }
743
744    // Wrap one built plan as the typed compiled-query DTO.
745    pub(in crate::db) fn compiled_query_from_plan(plan: AccessPlannedQuery) -> CompiledQuery<E> {
746        CompiledQuery::from_plan(plan)
747    }
748
749    #[must_use]
750    pub(crate) fn has_explicit_order(&self) -> bool {
751        self.inner.has_explicit_order()
752    }
753
754    #[must_use]
755    pub(in crate::db) const fn structural(&self) -> &StructuralQuery {
756        &self.inner
757    }
758
759    #[must_use]
760    pub const fn has_grouping(&self) -> bool {
761        self.inner.has_grouping()
762    }
763
764    #[must_use]
765    pub(crate) const fn load_spec(&self) -> Option<LoadSpec> {
766        self.inner.load_spec()
767    }
768
769    /// Add a predicate, implicitly AND-ing with any existing predicate.
770    #[must_use]
771    pub fn filter(mut self, predicate: Predicate) -> Self {
772        self.inner = self.inner.filter(predicate);
773        self
774    }
775
776    /// Apply a dynamic filter expression.
777    pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
778        let Self { inner, .. } = self;
779        let inner = inner.filter_expr(expr)?;
780
781        Ok(Self::from_inner(inner))
782    }
783
784    /// Append one typed ORDER BY term.
785    #[must_use]
786    pub fn order_term(mut self, term: FluentOrderTerm) -> Self {
787        self.inner = self.inner.order_term(term);
788        self
789    }
790
791    /// Append multiple typed ORDER BY terms in declaration order.
792    #[must_use]
793    pub fn order_terms<I>(mut self, terms: I) -> Self
794    where
795        I: IntoIterator<Item = FluentOrderTerm>,
796    {
797        for term in terms {
798            self.inner = self.inner.order_term(term);
799        }
800
801        self
802    }
803
804    /// Enable DISTINCT semantics for this query.
805    #[must_use]
806    pub fn distinct(mut self) -> Self {
807        self.inner = self.inner.distinct();
808        self
809    }
810
811    // Keep the internal fluent SQL parity hook available for lowering tests
812    // without making generated SQL binding depend on the typed query shell.
813    #[cfg(all(test, feature = "sql"))]
814    #[must_use]
815    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
816    where
817        I: IntoIterator<Item = S>,
818        S: Into<String>,
819    {
820        self.inner = self.inner.select_fields(fields);
821        self
822    }
823
824    /// Add one GROUP BY field.
825    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
826        let Self { inner, .. } = self;
827        let inner = inner.group_by(field)?;
828
829        Ok(Self::from_inner(inner))
830    }
831
832    /// Add one aggregate terminal via composable aggregate expression.
833    #[must_use]
834    pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
835        self.inner = self.inner.aggregate(aggregate);
836        self
837    }
838
839    /// Override grouped hard limits for grouped execution budget enforcement.
840    #[must_use]
841    pub fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
842        self.inner = self.inner.grouped_limits(max_groups, max_group_bytes);
843        self
844    }
845
846    /// Add one grouped HAVING compare clause over one grouped key field.
847    pub fn having_group(
848        self,
849        field: impl AsRef<str>,
850        op: CompareOp,
851        value: Value,
852    ) -> Result<Self, QueryError> {
853        let Self { inner, .. } = self;
854        let inner = inner.having_group(field, op, value)?;
855
856        Ok(Self::from_inner(inner))
857    }
858
859    /// Add one grouped HAVING compare clause over one grouped aggregate output.
860    pub fn having_aggregate(
861        self,
862        aggregate_index: usize,
863        op: CompareOp,
864        value: Value,
865    ) -> Result<Self, QueryError> {
866        let Self { inner, .. } = self;
867        let inner = inner.having_aggregate(aggregate_index, op, value)?;
868
869        Ok(Self::from_inner(inner))
870    }
871
872    /// Set the access path to a single primary key lookup.
873    pub(crate) fn by_id(self, id: E::Key) -> Self {
874        let Self { inner, .. } = self;
875
876        Self::from_inner(inner.by_id(id.to_value()))
877    }
878
879    /// Set the access path to a primary key batch lookup.
880    pub(crate) fn by_ids<I>(self, ids: I) -> Self
881    where
882        I: IntoIterator<Item = E::Key>,
883    {
884        let Self { inner, .. } = self;
885
886        Self::from_inner(inner.by_ids(ids.into_iter().map(|id| id.to_value())))
887    }
888
889    /// Mark this intent as a delete query.
890    #[must_use]
891    pub fn delete(mut self) -> Self {
892        self.inner = self.inner.delete();
893        self
894    }
895
896    /// Apply a limit to the current mode.
897    ///
898    /// Load limits bound result size; delete limits bound mutation size.
899    /// For scalar load queries, any use of `limit` or `offset` requires an
900    /// explicit `order_term(...)` so pagination is deterministic.
901    /// GROUP BY queries use canonical grouped-key order by default.
902    #[must_use]
903    pub fn limit(mut self, limit: u32) -> Self {
904        self.inner = self.inner.limit(limit);
905        self
906    }
907
908    /// Apply an offset to the current mode.
909    ///
910    /// Scalar load pagination requires an explicit `order_term(...)`.
911    /// GROUP BY queries use canonical grouped-key order by default.
912    /// Delete mode applies this after ordering and predicate filtering.
913    #[must_use]
914    pub fn offset(mut self, offset: u32) -> Self {
915        self.inner = self.inner.offset(offset);
916        self
917    }
918
919    /// Explain this intent without executing it.
920    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
921        let plan = self.planned()?;
922
923        Ok(plan.explain())
924    }
925
926    /// Return a stable plan hash for this intent.
927    ///
928    /// The hash is derived from canonical planner contracts and is suitable
929    /// for diagnostics, explain diffing, and cache key construction.
930    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
931        let plan = self.inner.build_plan()?;
932
933        Ok(plan.fingerprint().to_string())
934    }
935
936    // Resolve the structural execution descriptor through either the default
937    // schema-owned visibility lane or one caller-provided visible-index slice.
938    fn explain_execution_descriptor_for_visibility(
939        &self,
940        visible_indexes: Option<&VisibleIndexes<'_>>,
941    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
942    where
943        E: EntityValue,
944    {
945        match visible_indexes {
946            Some(visible_indexes) => self
947                .inner
948                .explain_execution_with_visible_indexes(visible_indexes),
949            None => self.inner.explain_execution(),
950        }
951    }
952
953    // Render one descriptor-derived execution surface after resolving the
954    // visibility slice once at the typed query boundary.
955    fn render_execution_descriptor_for_visibility(
956        &self,
957        visible_indexes: Option<&VisibleIndexes<'_>>,
958        render: impl FnOnce(ExplainExecutionNodeDescriptor) -> String,
959    ) -> Result<String, QueryError>
960    where
961        E: EntityValue,
962    {
963        let descriptor = self.explain_execution_descriptor_for_visibility(visible_indexes)?;
964
965        Ok(render(descriptor))
966    }
967
968    // Render one verbose execution explain payload after choosing the
969    // appropriate structural visibility lane once.
970    fn explain_execution_verbose_for_visibility(
971        &self,
972        visible_indexes: Option<&VisibleIndexes<'_>>,
973    ) -> Result<String, QueryError>
974    where
975        E: EntityValue,
976    {
977        match visible_indexes {
978            Some(visible_indexes) => self
979                .inner
980                .explain_execution_verbose_with_visible_indexes(visible_indexes),
981            None => self.inner.explain_execution_verbose(),
982        }
983    }
984
985    /// Explain executor-selected load execution shape without running it.
986    pub fn explain_execution(&self) -> Result<ExplainExecutionNodeDescriptor, QueryError>
987    where
988        E: EntityValue,
989    {
990        self.explain_execution_descriptor_for_visibility(None)
991    }
992
993    pub(in crate::db) fn explain_execution_with_visible_indexes(
994        &self,
995        visible_indexes: &VisibleIndexes<'_>,
996    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
997    where
998        E: EntityValue,
999    {
1000        self.explain_execution_descriptor_for_visibility(Some(visible_indexes))
1001    }
1002
1003    /// Explain executor-selected load execution shape as deterministic text.
1004    pub fn explain_execution_text(&self) -> Result<String, QueryError>
1005    where
1006        E: EntityValue,
1007    {
1008        self.render_execution_descriptor_for_visibility(None, |descriptor| {
1009            descriptor.render_text_tree()
1010        })
1011    }
1012
1013    /// Explain executor-selected load execution shape as canonical JSON.
1014    pub fn explain_execution_json(&self) -> Result<String, QueryError>
1015    where
1016        E: EntityValue,
1017    {
1018        self.render_execution_descriptor_for_visibility(None, |descriptor| {
1019            descriptor.render_json_canonical()
1020        })
1021    }
1022
1023    /// Explain executor-selected load execution shape with route diagnostics.
1024    #[inline(never)]
1025    pub fn explain_execution_verbose(&self) -> Result<String, QueryError>
1026    where
1027        E: EntityValue,
1028    {
1029        self.explain_execution_verbose_for_visibility(None)
1030    }
1031
1032    pub(in crate::db) fn explain_execution_verbose_with_visible_indexes(
1033        &self,
1034        visible_indexes: &VisibleIndexes<'_>,
1035    ) -> Result<String, QueryError>
1036    where
1037        E: EntityValue,
1038    {
1039        self.explain_execution_verbose_for_visibility(Some(visible_indexes))
1040    }
1041
1042    // Build one aggregate-terminal explain payload without executing the query.
1043    #[cfg(test)]
1044    #[inline(never)]
1045    pub(in crate::db) fn explain_aggregate_terminal(
1046        &self,
1047        aggregate: AggregateExpr,
1048    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
1049    where
1050        E: EntityValue,
1051    {
1052        self.inner.explain_aggregate_terminal_with_visible_indexes(
1053            &VisibleIndexes::schema_owned(E::MODEL.indexes()),
1054            AggregateRouteShape::new_from_fields(
1055                aggregate.kind(),
1056                aggregate.target_field(),
1057                E::MODEL.fields(),
1058                E::MODEL.primary_key().name(),
1059            ),
1060        )
1061    }
1062
1063    pub(in crate::db) fn explain_prepared_aggregate_terminal_with_visible_indexes<S>(
1064        &self,
1065        visible_indexes: &VisibleIndexes<'_>,
1066        strategy: &S,
1067    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
1068    where
1069        E: EntityValue,
1070        S: PreparedFluentAggregateExplainStrategy,
1071    {
1072        self.inner
1073            .explain_prepared_aggregate_terminal_with_visible_indexes(visible_indexes, strategy)
1074    }
1075
1076    pub(in crate::db) fn explain_bytes_by_with_visible_indexes(
1077        &self,
1078        visible_indexes: &VisibleIndexes<'_>,
1079        target_field: &str,
1080    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
1081    where
1082        E: EntityValue,
1083    {
1084        let executable = self.prepared_execution_plan_for_visibility(Some(visible_indexes))?;
1085        let mut descriptor = executable
1086            .explain_load_execution_node_descriptor()
1087            .map_err(QueryError::execute)?;
1088        let projection_mode = executable.bytes_by_projection_mode(target_field);
1089        let projection_mode_label =
1090            PreparedExecutionPlan::<E>::bytes_by_projection_mode_label(projection_mode);
1091
1092        descriptor
1093            .node_properties
1094            .insert("terminal", Value::from("bytes_by"));
1095        descriptor
1096            .node_properties
1097            .insert("terminal_field", Value::from(target_field.to_string()));
1098        descriptor.node_properties.insert(
1099            "terminal_projection_mode",
1100            Value::from(projection_mode_label),
1101        );
1102        descriptor.node_properties.insert(
1103            "terminal_index_only",
1104            Value::from(matches!(
1105                projection_mode,
1106                BytesByProjectionMode::CoveringIndex | BytesByProjectionMode::CoveringConstant
1107            )),
1108        );
1109
1110        Ok(descriptor)
1111    }
1112
1113    pub(in crate::db) fn explain_prepared_projection_terminal_with_visible_indexes(
1114        &self,
1115        visible_indexes: &VisibleIndexes<'_>,
1116        strategy: &PreparedFluentProjectionStrategy,
1117    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
1118    where
1119        E: EntityValue,
1120    {
1121        let executable = self.prepared_execution_plan_for_visibility(Some(visible_indexes))?;
1122        let mut descriptor = executable
1123            .explain_load_execution_node_descriptor()
1124            .map_err(QueryError::execute)?;
1125        let projection_descriptor = strategy.explain_descriptor();
1126
1127        descriptor.node_properties.insert(
1128            "terminal",
1129            Value::from(projection_descriptor.terminal_label()),
1130        );
1131        descriptor.node_properties.insert(
1132            "terminal_field",
1133            Value::from(projection_descriptor.field_label().to_string()),
1134        );
1135        descriptor.node_properties.insert(
1136            "terminal_output",
1137            Value::from(projection_descriptor.output_label()),
1138        );
1139
1140        Ok(descriptor)
1141    }
1142
1143    /// Plan this intent into a neutral planned query contract.
1144    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
1145        self.map_plan_for_visibility(None, Self::planned_query_from_plan)
1146    }
1147
1148    /// Compile this intent into query-owned handoff state.
1149    ///
1150    /// This boundary intentionally does not expose executor runtime shape.
1151    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
1152        self.map_plan_for_visibility(None, Self::compiled_query_from_plan)
1153    }
1154
1155    #[cfg(test)]
1156    pub(in crate::db) fn plan_with_visible_indexes(
1157        &self,
1158        visible_indexes: &VisibleIndexes<'_>,
1159    ) -> Result<CompiledQuery<E>, QueryError> {
1160        self.map_plan_for_visibility(Some(visible_indexes), Self::compiled_query_from_plan)
1161    }
1162}
1163
1164fn contains_execution_node_type(
1165    descriptor: &ExplainExecutionNodeDescriptor,
1166    target: ExplainExecutionNodeType,
1167) -> bool {
1168    descriptor.node_type() == target
1169        || descriptor
1170            .children()
1171            .iter()
1172            .any(|child| contains_execution_node_type(child, target))
1173}
1174
1175fn plan_order_pushdown_label(order_pushdown: &ExplainOrderPushdown) -> String {
1176    match order_pushdown {
1177        ExplainOrderPushdown::MissingModelContext => "missing_model_context".to_string(),
1178        ExplainOrderPushdown::EligibleSecondaryIndex { index, prefix_len } => {
1179            format!("eligible(index={index},prefix_len={prefix_len})",)
1180        }
1181        ExplainOrderPushdown::Rejected(reason) => format!("rejected({reason:?})"),
1182    }
1183}
1184
1185fn plan_predicate_pushdown_label(
1186    predicate: &ExplainPredicate,
1187    access: &ExplainAccessPath,
1188) -> String {
1189    let access_label = match access {
1190        ExplainAccessPath::ByKey { .. } => "by_key",
1191        ExplainAccessPath::ByKeys { keys } if keys.is_empty() => "empty_access_contract",
1192        ExplainAccessPath::ByKeys { .. } => "by_keys",
1193        ExplainAccessPath::KeyRange { .. } => "key_range",
1194        ExplainAccessPath::IndexPrefix { .. } => "index_prefix",
1195        ExplainAccessPath::IndexMultiLookup { .. } => "index_multi_lookup",
1196        ExplainAccessPath::IndexRange { .. } => "index_range",
1197        ExplainAccessPath::FullScan => "full_scan",
1198        ExplainAccessPath::Union(_) => "union",
1199        ExplainAccessPath::Intersection(_) => "intersection",
1200    };
1201    if matches!(predicate, ExplainPredicate::None) {
1202        return "none".to_string();
1203    }
1204    if matches!(access, ExplainAccessPath::FullScan) {
1205        if explain_predicate_contains_non_strict_compare(predicate) {
1206            return "fallback(non_strict_compare_coercion)".to_string();
1207        }
1208        if explain_predicate_contains_empty_prefix_starts_with(predicate) {
1209            return "fallback(starts_with_empty_prefix)".to_string();
1210        }
1211        if explain_predicate_contains_is_null(predicate) {
1212            return "fallback(is_null_full_scan)".to_string();
1213        }
1214        if explain_predicate_contains_text_scan_operator(predicate) {
1215            return "fallback(text_operator_full_scan)".to_string();
1216        }
1217
1218        return format!("fallback({access_label})");
1219    }
1220
1221    format!("applied({access_label})")
1222}
1223
1224fn explain_predicate_contains_non_strict_compare(predicate: &ExplainPredicate) -> bool {
1225    match predicate {
1226        ExplainPredicate::Compare { coercion, .. }
1227        | ExplainPredicate::CompareFields { coercion, .. } => coercion.id != CoercionId::Strict,
1228        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
1229            .iter()
1230            .any(explain_predicate_contains_non_strict_compare),
1231        ExplainPredicate::Not(inner) => explain_predicate_contains_non_strict_compare(inner),
1232        ExplainPredicate::None
1233        | ExplainPredicate::True
1234        | ExplainPredicate::False
1235        | ExplainPredicate::IsNull { .. }
1236        | ExplainPredicate::IsNotNull { .. }
1237        | ExplainPredicate::IsMissing { .. }
1238        | ExplainPredicate::IsEmpty { .. }
1239        | ExplainPredicate::IsNotEmpty { .. }
1240        | ExplainPredicate::TextContains { .. }
1241        | ExplainPredicate::TextContainsCi { .. } => false,
1242    }
1243}
1244
1245fn explain_predicate_contains_is_null(predicate: &ExplainPredicate) -> bool {
1246    match predicate {
1247        ExplainPredicate::IsNull { .. } => true,
1248        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => {
1249            children.iter().any(explain_predicate_contains_is_null)
1250        }
1251        ExplainPredicate::Not(inner) => explain_predicate_contains_is_null(inner),
1252        ExplainPredicate::None
1253        | ExplainPredicate::True
1254        | ExplainPredicate::False
1255        | ExplainPredicate::Compare { .. }
1256        | ExplainPredicate::CompareFields { .. }
1257        | ExplainPredicate::IsNotNull { .. }
1258        | ExplainPredicate::IsMissing { .. }
1259        | ExplainPredicate::IsEmpty { .. }
1260        | ExplainPredicate::IsNotEmpty { .. }
1261        | ExplainPredicate::TextContains { .. }
1262        | ExplainPredicate::TextContainsCi { .. } => false,
1263    }
1264}
1265
1266fn explain_predicate_contains_empty_prefix_starts_with(predicate: &ExplainPredicate) -> bool {
1267    match predicate {
1268        ExplainPredicate::Compare {
1269            op: CompareOp::StartsWith,
1270            value: Value::Text(prefix),
1271            ..
1272        } => prefix.is_empty(),
1273        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
1274            .iter()
1275            .any(explain_predicate_contains_empty_prefix_starts_with),
1276        ExplainPredicate::Not(inner) => explain_predicate_contains_empty_prefix_starts_with(inner),
1277        ExplainPredicate::None
1278        | ExplainPredicate::True
1279        | ExplainPredicate::False
1280        | ExplainPredicate::Compare { .. }
1281        | ExplainPredicate::CompareFields { .. }
1282        | ExplainPredicate::IsNull { .. }
1283        | ExplainPredicate::IsNotNull { .. }
1284        | ExplainPredicate::IsMissing { .. }
1285        | ExplainPredicate::IsEmpty { .. }
1286        | ExplainPredicate::IsNotEmpty { .. }
1287        | ExplainPredicate::TextContains { .. }
1288        | ExplainPredicate::TextContainsCi { .. } => false,
1289    }
1290}
1291
1292fn explain_predicate_contains_text_scan_operator(predicate: &ExplainPredicate) -> bool {
1293    match predicate {
1294        ExplainPredicate::Compare {
1295            op: CompareOp::EndsWith,
1296            ..
1297        }
1298        | ExplainPredicate::TextContains { .. }
1299        | ExplainPredicate::TextContainsCi { .. } => true,
1300        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
1301            .iter()
1302            .any(explain_predicate_contains_text_scan_operator),
1303        ExplainPredicate::Not(inner) => explain_predicate_contains_text_scan_operator(inner),
1304        ExplainPredicate::Compare { .. }
1305        | ExplainPredicate::CompareFields { .. }
1306        | ExplainPredicate::None
1307        | ExplainPredicate::True
1308        | ExplainPredicate::False
1309        | ExplainPredicate::IsNull { .. }
1310        | ExplainPredicate::IsNotNull { .. }
1311        | ExplainPredicate::IsMissing { .. }
1312        | ExplainPredicate::IsEmpty { .. }
1313        | ExplainPredicate::IsNotEmpty { .. } => false,
1314    }
1315}
1316
1317impl<E> Query<E>
1318where
1319    E: EntityKind + SingletonEntity,
1320    E::Key: Default,
1321{
1322    /// Set the access path to the singleton primary key.
1323    pub(crate) fn only(self) -> Self {
1324        let Self { inner, .. } = self;
1325
1326        Self::from_inner(inner.only(E::Key::default().to_value()))
1327    }
1328}