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(all(test, feature = "sql"))]
785    #[must_use]
786    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
787    where
788        I: IntoIterator<Item = S>,
789        S: Into<String>,
790    {
791        self.inner = self.inner.select_fields(fields);
792        self
793    }
794
795    /// Add one GROUP BY field.
796    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
797        let Self { inner, .. } = self;
798        let inner = inner.group_by(field)?;
799
800        Ok(Self::from_inner(inner))
801    }
802
803    /// Add one aggregate terminal via composable aggregate expression.
804    #[must_use]
805    pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
806        self.inner = self.inner.aggregate(aggregate);
807        self
808    }
809
810    /// Override grouped hard limits for grouped execution budget enforcement.
811    #[must_use]
812    pub fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
813        self.inner = self.inner.grouped_limits(max_groups, max_group_bytes);
814        self
815    }
816
817    /// Add one grouped HAVING compare clause over one grouped key field.
818    pub fn having_group(
819        self,
820        field: impl AsRef<str>,
821        op: CompareOp,
822        value: Value,
823    ) -> Result<Self, QueryError> {
824        let Self { inner, .. } = self;
825        let inner = inner.having_group(field, op, value)?;
826
827        Ok(Self::from_inner(inner))
828    }
829
830    /// Add one grouped HAVING compare clause over one grouped aggregate output.
831    pub fn having_aggregate(
832        self,
833        aggregate_index: usize,
834        op: CompareOp,
835        value: Value,
836    ) -> Result<Self, QueryError> {
837        let Self { inner, .. } = self;
838        let inner = inner.having_aggregate(aggregate_index, op, value)?;
839
840        Ok(Self::from_inner(inner))
841    }
842
843    /// Set the access path to a single primary key lookup.
844    pub(crate) fn by_id(self, id: E::Key) -> Self {
845        let Self { inner, .. } = self;
846
847        Self::from_inner(inner.by_id(id.to_value()))
848    }
849
850    /// Set the access path to a primary key batch lookup.
851    pub(crate) fn by_ids<I>(self, ids: I) -> Self
852    where
853        I: IntoIterator<Item = E::Key>,
854    {
855        let Self { inner, .. } = self;
856
857        Self::from_inner(inner.by_ids(ids.into_iter().map(|id| id.to_value())))
858    }
859
860    /// Mark this intent as a delete query.
861    #[must_use]
862    pub fn delete(mut self) -> Self {
863        self.inner = self.inner.delete();
864        self
865    }
866
867    /// Apply a limit to the current mode.
868    ///
869    /// Load limits bound result size; delete limits bound mutation size.
870    /// For scalar load queries, any use of `limit` or `offset` requires an
871    /// explicit `order_by(...)` so pagination is deterministic.
872    /// GROUP BY queries use canonical grouped-key order by default.
873    #[must_use]
874    pub fn limit(mut self, limit: u32) -> Self {
875        self.inner = self.inner.limit(limit);
876        self
877    }
878
879    /// Apply an offset to a load intent.
880    ///
881    /// Scalar pagination requires an explicit `order_by(...)`.
882    /// GROUP BY queries use canonical grouped-key order by default.
883    /// Delete intents reject `offset(...)` during planning.
884    #[must_use]
885    pub fn offset(mut self, offset: u32) -> Self {
886        self.inner = self.inner.offset(offset);
887        self
888    }
889
890    /// Explain this intent without executing it.
891    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
892        let plan = self.planned()?;
893
894        Ok(plan.explain())
895    }
896
897    /// Return a stable plan hash for this intent.
898    ///
899    /// The hash is derived from canonical planner contracts and is suitable
900    /// for diagnostics, explain diffing, and cache key construction.
901    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
902        let plan = self.inner.build_plan()?;
903
904        Ok(plan.fingerprint().to_string())
905    }
906
907    /// Explain executor-selected load execution shape without running it.
908    pub fn explain_execution(&self) -> Result<ExplainExecutionNodeDescriptor, QueryError>
909    where
910        E: EntityValue,
911    {
912        self.inner.explain_execution()
913    }
914
915    pub(in crate::db) fn explain_execution_with_visible_indexes(
916        &self,
917        visible_indexes: &VisibleIndexes<'_>,
918    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
919    where
920        E: EntityValue,
921    {
922        self.inner
923            .explain_execution_with_visible_indexes(visible_indexes)
924    }
925
926    /// Explain executor-selected load execution shape as deterministic text.
927    pub fn explain_execution_text(&self) -> Result<String, QueryError>
928    where
929        E: EntityValue,
930    {
931        self.inner.explain_execution_text()
932    }
933
934    pub(in crate::db) fn explain_execution_text_with_visible_indexes(
935        &self,
936        visible_indexes: &VisibleIndexes<'_>,
937    ) -> Result<String, QueryError>
938    where
939        E: EntityValue,
940    {
941        self.inner
942            .explain_execution_text_with_visible_indexes(visible_indexes)
943    }
944
945    /// Explain executor-selected load execution shape as canonical JSON.
946    pub fn explain_execution_json(&self) -> Result<String, QueryError>
947    where
948        E: EntityValue,
949    {
950        self.inner.explain_execution_json()
951    }
952
953    pub(in crate::db) fn explain_execution_json_with_visible_indexes(
954        &self,
955        visible_indexes: &VisibleIndexes<'_>,
956    ) -> Result<String, QueryError>
957    where
958        E: EntityValue,
959    {
960        self.inner
961            .explain_execution_json_with_visible_indexes(visible_indexes)
962    }
963
964    /// Explain executor-selected load execution shape with route diagnostics.
965    #[inline(never)]
966    pub fn explain_execution_verbose(&self) -> Result<String, QueryError>
967    where
968        E: EntityValue,
969    {
970        self.inner.explain_execution_verbose()
971    }
972
973    // Build one aggregate-terminal explain payload without executing the query.
974    #[cfg(test)]
975    #[inline(never)]
976    pub(in crate::db) fn explain_aggregate_terminal(
977        &self,
978        aggregate: AggregateExpr,
979    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
980    where
981        E: EntityValue,
982    {
983        self.inner.explain_aggregate_terminal_with_visible_indexes(
984            &VisibleIndexes::schema_owned(E::MODEL.indexes()),
985            AggregateRouteShape::new_from_fields(
986                aggregate.kind(),
987                aggregate.target_field(),
988                E::MODEL.fields(),
989                E::MODEL.primary_key().name(),
990            ),
991        )
992    }
993
994    pub(in crate::db) fn explain_execution_verbose_with_visible_indexes(
995        &self,
996        visible_indexes: &VisibleIndexes<'_>,
997    ) -> Result<String, QueryError>
998    where
999        E: EntityValue,
1000    {
1001        self.inner
1002            .explain_execution_verbose_with_visible_indexes(visible_indexes)
1003    }
1004
1005    pub(in crate::db) fn explain_prepared_aggregate_terminal_with_visible_indexes<S>(
1006        &self,
1007        visible_indexes: &VisibleIndexes<'_>,
1008        strategy: &S,
1009    ) -> Result<ExplainAggregateTerminalPlan, QueryError>
1010    where
1011        E: EntityValue,
1012        S: PreparedFluentAggregateExplainStrategy,
1013    {
1014        self.inner
1015            .explain_prepared_aggregate_terminal_with_visible_indexes(visible_indexes, strategy)
1016    }
1017
1018    pub(in crate::db) fn explain_bytes_by_with_visible_indexes(
1019        &self,
1020        visible_indexes: &VisibleIndexes<'_>,
1021        target_field: &str,
1022    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
1023    where
1024        E: EntityValue,
1025    {
1026        let executable = self
1027            .plan_with_visible_indexes(visible_indexes)?
1028            .into_executable();
1029        let mut descriptor = executable
1030            .explain_load_execution_node_descriptor()
1031            .map_err(QueryError::execute)?;
1032        let projection_mode = executable.bytes_by_projection_mode(target_field);
1033        let projection_mode_label =
1034            ExecutablePlan::<E>::bytes_by_projection_mode_label(projection_mode);
1035
1036        descriptor
1037            .node_properties
1038            .insert("terminal", Value::from("bytes_by"));
1039        descriptor
1040            .node_properties
1041            .insert("terminal_field", Value::from(target_field.to_string()));
1042        descriptor.node_properties.insert(
1043            "terminal_projection_mode",
1044            Value::from(projection_mode_label),
1045        );
1046        descriptor.node_properties.insert(
1047            "terminal_index_only",
1048            Value::from(matches!(
1049                projection_mode,
1050                BytesByProjectionMode::CoveringIndex | BytesByProjectionMode::CoveringConstant
1051            )),
1052        );
1053
1054        Ok(descriptor)
1055    }
1056
1057    pub(in crate::db) fn explain_prepared_projection_terminal_with_visible_indexes(
1058        &self,
1059        visible_indexes: &VisibleIndexes<'_>,
1060        strategy: &PreparedFluentProjectionStrategy,
1061    ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
1062    where
1063        E: EntityValue,
1064    {
1065        let executable = self
1066            .plan_with_visible_indexes(visible_indexes)?
1067            .into_executable();
1068        let mut descriptor = executable
1069            .explain_load_execution_node_descriptor()
1070            .map_err(QueryError::execute)?;
1071        let projection_descriptor = strategy.explain_descriptor();
1072
1073        descriptor.node_properties.insert(
1074            "terminal",
1075            Value::from(projection_descriptor.terminal_label()),
1076        );
1077        descriptor.node_properties.insert(
1078            "terminal_field",
1079            Value::from(projection_descriptor.field_label().to_string()),
1080        );
1081        descriptor.node_properties.insert(
1082            "terminal_output",
1083            Value::from(projection_descriptor.output_label()),
1084        );
1085
1086        Ok(descriptor)
1087    }
1088
1089    /// Plan this intent into a neutral planned query contract.
1090    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
1091        let plan = self.inner.build_plan()?;
1092        let _projection = plan.projection_spec(E::MODEL);
1093
1094        Ok(PlannedQuery::from_inner(PlannedQueryCore::new(
1095            E::MODEL,
1096            plan,
1097        )))
1098    }
1099
1100    pub(in crate::db) fn planned_with_visible_indexes(
1101        &self,
1102        visible_indexes: &VisibleIndexes<'_>,
1103    ) -> Result<PlannedQuery<E>, QueryError> {
1104        let plan = self
1105            .inner
1106            .build_plan_with_visible_indexes(visible_indexes)?;
1107        let _projection = plan.projection_spec(E::MODEL);
1108
1109        Ok(PlannedQuery::from_inner(PlannedQueryCore::new(
1110            E::MODEL,
1111            plan,
1112        )))
1113    }
1114
1115    /// Compile this intent into query-owned handoff state.
1116    ///
1117    /// This boundary intentionally does not expose executor runtime shape.
1118    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
1119        let plan = self.inner.build_plan()?;
1120        let _projection = plan.projection_spec(E::MODEL);
1121
1122        Ok(CompiledQuery::from_inner(CompiledQueryCore::new(
1123            E::MODEL,
1124            E::PATH,
1125            plan,
1126        )))
1127    }
1128
1129    pub(in crate::db) fn plan_with_visible_indexes(
1130        &self,
1131        visible_indexes: &VisibleIndexes<'_>,
1132    ) -> Result<CompiledQuery<E>, QueryError> {
1133        let plan = self
1134            .inner
1135            .build_plan_with_visible_indexes(visible_indexes)?;
1136        let _projection = plan.projection_spec(E::MODEL);
1137
1138        Ok(CompiledQuery::from_inner(CompiledQueryCore::new(
1139            E::MODEL,
1140            E::PATH,
1141            plan,
1142        )))
1143    }
1144}
1145
1146fn contains_execution_node_type(
1147    descriptor: &ExplainExecutionNodeDescriptor,
1148    target: ExplainExecutionNodeType,
1149) -> bool {
1150    descriptor.node_type() == target
1151        || descriptor
1152            .children()
1153            .iter()
1154            .any(|child| contains_execution_node_type(child, target))
1155}
1156
1157fn plan_order_pushdown_label(order_pushdown: &ExplainOrderPushdown) -> String {
1158    match order_pushdown {
1159        ExplainOrderPushdown::MissingModelContext => "missing_model_context".to_string(),
1160        ExplainOrderPushdown::EligibleSecondaryIndex { index, prefix_len } => {
1161            format!("eligible(index={index},prefix_len={prefix_len})",)
1162        }
1163        ExplainOrderPushdown::Rejected(reason) => format!("rejected({reason:?})"),
1164    }
1165}
1166
1167fn plan_predicate_pushdown_label(
1168    predicate: &ExplainPredicate,
1169    access: &ExplainAccessPath,
1170) -> String {
1171    let access_label = match access {
1172        ExplainAccessPath::ByKey { .. } => "by_key",
1173        ExplainAccessPath::ByKeys { keys } if keys.is_empty() => "empty_access_contract",
1174        ExplainAccessPath::ByKeys { .. } => "by_keys",
1175        ExplainAccessPath::KeyRange { .. } => "key_range",
1176        ExplainAccessPath::IndexPrefix { .. } => "index_prefix",
1177        ExplainAccessPath::IndexMultiLookup { .. } => "index_multi_lookup",
1178        ExplainAccessPath::IndexRange { .. } => "index_range",
1179        ExplainAccessPath::FullScan => "full_scan",
1180        ExplainAccessPath::Union(_) => "union",
1181        ExplainAccessPath::Intersection(_) => "intersection",
1182    };
1183    if matches!(predicate, ExplainPredicate::None) {
1184        return "none".to_string();
1185    }
1186    if matches!(access, ExplainAccessPath::FullScan) {
1187        if explain_predicate_contains_non_strict_compare(predicate) {
1188            return "fallback(non_strict_compare_coercion)".to_string();
1189        }
1190        if explain_predicate_contains_empty_prefix_starts_with(predicate) {
1191            return "fallback(starts_with_empty_prefix)".to_string();
1192        }
1193        if explain_predicate_contains_is_null(predicate) {
1194            return "fallback(is_null_full_scan)".to_string();
1195        }
1196        if explain_predicate_contains_text_scan_operator(predicate) {
1197            return "fallback(text_operator_full_scan)".to_string();
1198        }
1199
1200        return format!("fallback({access_label})");
1201    }
1202
1203    format!("applied({access_label})")
1204}
1205
1206fn explain_predicate_contains_non_strict_compare(predicate: &ExplainPredicate) -> bool {
1207    match predicate {
1208        ExplainPredicate::Compare { coercion, .. } => coercion.id != CoercionId::Strict,
1209        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
1210            .iter()
1211            .any(explain_predicate_contains_non_strict_compare),
1212        ExplainPredicate::Not(inner) => explain_predicate_contains_non_strict_compare(inner),
1213        ExplainPredicate::None
1214        | ExplainPredicate::True
1215        | ExplainPredicate::False
1216        | ExplainPredicate::IsNull { .. }
1217        | ExplainPredicate::IsNotNull { .. }
1218        | ExplainPredicate::IsMissing { .. }
1219        | ExplainPredicate::IsEmpty { .. }
1220        | ExplainPredicate::IsNotEmpty { .. }
1221        | ExplainPredicate::TextContains { .. }
1222        | ExplainPredicate::TextContainsCi { .. } => false,
1223    }
1224}
1225
1226fn explain_predicate_contains_is_null(predicate: &ExplainPredicate) -> bool {
1227    match predicate {
1228        ExplainPredicate::IsNull { .. } => true,
1229        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => {
1230            children.iter().any(explain_predicate_contains_is_null)
1231        }
1232        ExplainPredicate::Not(inner) => explain_predicate_contains_is_null(inner),
1233        ExplainPredicate::None
1234        | ExplainPredicate::True
1235        | ExplainPredicate::False
1236        | ExplainPredicate::Compare { .. }
1237        | ExplainPredicate::IsNotNull { .. }
1238        | ExplainPredicate::IsMissing { .. }
1239        | ExplainPredicate::IsEmpty { .. }
1240        | ExplainPredicate::IsNotEmpty { .. }
1241        | ExplainPredicate::TextContains { .. }
1242        | ExplainPredicate::TextContainsCi { .. } => false,
1243    }
1244}
1245
1246fn explain_predicate_contains_empty_prefix_starts_with(predicate: &ExplainPredicate) -> bool {
1247    match predicate {
1248        ExplainPredicate::Compare {
1249            op: CompareOp::StartsWith,
1250            value: Value::Text(prefix),
1251            ..
1252        } => prefix.is_empty(),
1253        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
1254            .iter()
1255            .any(explain_predicate_contains_empty_prefix_starts_with),
1256        ExplainPredicate::Not(inner) => explain_predicate_contains_empty_prefix_starts_with(inner),
1257        ExplainPredicate::None
1258        | ExplainPredicate::True
1259        | ExplainPredicate::False
1260        | ExplainPredicate::Compare { .. }
1261        | ExplainPredicate::IsNull { .. }
1262        | ExplainPredicate::IsNotNull { .. }
1263        | ExplainPredicate::IsMissing { .. }
1264        | ExplainPredicate::IsEmpty { .. }
1265        | ExplainPredicate::IsNotEmpty { .. }
1266        | ExplainPredicate::TextContains { .. }
1267        | ExplainPredicate::TextContainsCi { .. } => false,
1268    }
1269}
1270
1271fn explain_predicate_contains_text_scan_operator(predicate: &ExplainPredicate) -> bool {
1272    match predicate {
1273        ExplainPredicate::Compare {
1274            op: CompareOp::EndsWith,
1275            ..
1276        }
1277        | ExplainPredicate::TextContains { .. }
1278        | ExplainPredicate::TextContainsCi { .. } => true,
1279        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
1280            .iter()
1281            .any(explain_predicate_contains_text_scan_operator),
1282        ExplainPredicate::Not(inner) => explain_predicate_contains_text_scan_operator(inner),
1283        ExplainPredicate::Compare { .. }
1284        | ExplainPredicate::None
1285        | ExplainPredicate::True
1286        | ExplainPredicate::False
1287        | ExplainPredicate::IsNull { .. }
1288        | ExplainPredicate::IsNotNull { .. }
1289        | ExplainPredicate::IsMissing { .. }
1290        | ExplainPredicate::IsEmpty { .. }
1291        | ExplainPredicate::IsNotEmpty { .. } => false,
1292    }
1293}
1294
1295impl<E> Query<E>
1296where
1297    E: EntityKind + SingletonEntity,
1298    E::Key: Default,
1299{
1300    /// Set the access path to the singleton primary key.
1301    pub(crate) fn only(self) -> Self {
1302        let Self { inner, .. } = self;
1303
1304        Self::from_inner(inner.only(E::Key::default().to_value()))
1305    }
1306}