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