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