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