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