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