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