Skip to main content

icydb_core/db/query/intent/
query.rs

1//! Module: query::intent::query
2//! Responsibility: typed query-intent construction and planner handoff for entity queries.
3//! Does not own: runtime execution semantics or access-path execution behavior.
4//! Boundary: exposes query APIs and emits planner-owned compiled query contracts.
5
6#[cfg(feature = "sql")]
7use crate::db::query::plan::expr::ProjectionSelection;
8use crate::{
9    db::{
10        predicate::{CompareOp, MissingRowPolicy, Predicate},
11        query::{
12            builder::AggregateExpr,
13            explain::ExplainPlan,
14            expr::FilterExpr,
15            expr::OrderTerm as FluentOrderTerm,
16            intent::{AccessRequirements, QueryError, QueryModel, RequiredAccessPath},
17            plan::{
18                AccessPlannedQuery, LoadSpec, OrderSpec, PreparedScalarPlanningState, QueryMode,
19                VisibleIndexes, expr::Expr,
20            },
21        },
22        schema::SchemaInfo,
23    },
24    traits::{EntityKind, KeyValueCodec, SingletonEntity},
25    value::{InputValue, Value},
26};
27use core::marker::PhantomData;
28
29///
30/// StructuralQuery
31///
32/// Generic-free query-intent core shared by typed `Query<E>` wrappers.
33/// Stores model-level key access as `Value` so only typed key-entry helpers
34/// remain entity-specific at the outer API boundary.
35///
36
37#[derive(Clone, Debug)]
38pub(in crate::db) struct StructuralQuery {
39    intent: QueryModel<'static, Value>,
40    access_requirements: AccessRequirements,
41}
42
43impl StructuralQuery {
44    #[must_use]
45    pub(in crate::db) const fn new(
46        model: &'static crate::model::entity::EntityModel,
47        consistency: MissingRowPolicy,
48    ) -> Self {
49        Self {
50            intent: QueryModel::new(model, consistency),
51            access_requirements: AccessRequirements::new(),
52        }
53    }
54
55    // Rewrap one updated generic-free intent model back into the structural
56    // query shell so local transformation helpers do not rebuild `Self`
57    // ad hoc at each boundary method.
58    const fn from_intent_and_access_requirements(
59        intent: QueryModel<'static, Value>,
60        access_requirements: AccessRequirements,
61    ) -> Self {
62        Self {
63            intent,
64            access_requirements,
65        }
66    }
67
68    // Apply one infallible intent transformation while preserving the
69    // structural query shell at this boundary.
70    fn map_intent(
71        self,
72        map: impl FnOnce(QueryModel<'static, Value>) -> QueryModel<'static, Value>,
73    ) -> Self {
74        let Self {
75            intent,
76            access_requirements,
77        } = self;
78
79        Self::from_intent_and_access_requirements(map(intent), access_requirements)
80    }
81
82    // Apply one fallible intent transformation while keeping result wrapping
83    // local to the structural query boundary.
84    fn try_map_intent(
85        self,
86        map: impl FnOnce(QueryModel<'static, Value>) -> Result<QueryModel<'static, Value>, QueryError>,
87    ) -> Result<Self, QueryError> {
88        let Self {
89            intent,
90            access_requirements,
91        } = self;
92
93        map(intent)
94            .map(|intent| Self::from_intent_and_access_requirements(intent, access_requirements))
95    }
96
97    #[must_use]
98    const fn mode(&self) -> QueryMode {
99        self.intent.mode()
100    }
101
102    #[must_use]
103    fn has_explicit_order(&self) -> bool {
104        self.intent.has_explicit_order()
105    }
106
107    #[must_use]
108    pub(in crate::db) const fn has_grouping(&self) -> bool {
109        self.intent.has_grouping()
110    }
111
112    #[must_use]
113    const fn load_spec(&self) -> Option<LoadSpec> {
114        match self.intent.mode() {
115            QueryMode::Load(spec) => Some(spec),
116            QueryMode::Delete(_) => None,
117        }
118    }
119
120    #[must_use]
121    pub(in crate::db) fn filter_predicate(mut self, predicate: Predicate) -> Self {
122        self.intent = self.intent.filter_predicate(predicate);
123        self
124    }
125
126    #[must_use]
127    pub(in crate::db) fn filter(mut self, expr: impl Into<FilterExpr>) -> Self {
128        self.intent = self.intent.filter(expr.into());
129        self
130    }
131
132    #[must_use]
133    pub(in crate::db) fn filter_expr_with_normalized_predicate(
134        mut self,
135        expr: Expr,
136        predicate: Predicate,
137    ) -> Self {
138        self.intent = self
139            .intent
140            .filter_expr_with_normalized_predicate(expr, predicate);
141        self
142    }
143    pub(in crate::db) fn order_term(mut self, term: FluentOrderTerm) -> Self {
144        self.intent = self.intent.order_term(term);
145        self
146    }
147
148    // Keep the exact expression-owned scalar filter lane available for
149    // internal SQL lowering and parity callers that must preserve one planner
150    // expression without routing through the public typed `FilterExpr` surface.
151    #[must_use]
152    pub(in crate::db) fn filter_expr(mut self, expr: Expr) -> Self {
153        self.intent = self.intent.filter_expr(expr);
154        self
155    }
156
157    #[must_use]
158    pub(in crate::db) fn order_spec(mut self, order: OrderSpec) -> Self {
159        self.intent = self.intent.order_spec(order);
160        self
161    }
162
163    #[must_use]
164    pub(in crate::db) fn distinct(mut self) -> Self {
165        self.intent = self.intent.distinct();
166        self
167    }
168
169    #[cfg(feature = "sql")]
170    #[must_use]
171    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
172    where
173        I: IntoIterator<Item = S>,
174        S: Into<String>,
175    {
176        self.intent = self.intent.select_fields(fields);
177        self
178    }
179
180    #[cfg(feature = "sql")]
181    #[must_use]
182    pub(in crate::db) fn projection_selection(mut self, selection: ProjectionSelection) -> Self {
183        self.intent = self.intent.projection_selection(selection);
184        self
185    }
186
187    pub(in crate::db) fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
188        self.try_map_intent(|intent| intent.push_group_field(field.as_ref()))
189    }
190
191    pub(in crate::db) fn group_by_with_schema(
192        self,
193        field: impl AsRef<str>,
194        schema: &SchemaInfo,
195    ) -> Result<Self, QueryError> {
196        self.try_map_intent(|intent| intent.push_group_field_with_schema(field.as_ref(), schema))
197    }
198
199    #[must_use]
200    pub(in crate::db) fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
201        self.intent = self.intent.push_group_aggregate(aggregate);
202        self
203    }
204
205    #[must_use]
206    fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
207        self.intent = self.intent.grouped_limits(max_groups, max_group_bytes);
208        self
209    }
210
211    pub(in crate::db) fn having_group(
212        self,
213        field: impl AsRef<str>,
214        op: CompareOp,
215        value: Value,
216    ) -> Result<Self, QueryError> {
217        let field = field.as_ref().to_owned();
218        self.try_map_intent(|intent| intent.push_having_group_clause(&field, op, value))
219    }
220
221    pub(in crate::db) fn having_group_with_schema(
222        self,
223        field: impl AsRef<str>,
224        schema: &SchemaInfo,
225        op: CompareOp,
226        value: Value,
227    ) -> Result<Self, QueryError> {
228        let field = field.as_ref().to_owned();
229        self.try_map_intent(|intent| {
230            intent.push_having_group_clause_with_schema(&field, schema, op, value)
231        })
232    }
233
234    pub(in crate::db) fn having_aggregate(
235        self,
236        aggregate_index: usize,
237        op: CompareOp,
238        value: Value,
239    ) -> Result<Self, QueryError> {
240        self.try_map_intent(|intent| {
241            intent.push_having_aggregate_clause(aggregate_index, op, value)
242        })
243    }
244
245    #[cfg(test)]
246    pub(in crate::db) fn having_expr(self, expr: Expr) -> Result<Self, QueryError> {
247        self.try_map_intent(|intent| intent.push_having_expr(expr))
248    }
249
250    pub(in crate::db) fn having_expr_preserving_shape(
251        self,
252        expr: Expr,
253    ) -> Result<Self, QueryError> {
254        self.try_map_intent(|intent| intent.push_having_expr_preserving_shape(expr))
255    }
256
257    #[must_use]
258    fn by_id(self, id: Value) -> Self {
259        self.map_intent(|intent| intent.by_id(id))
260    }
261
262    #[must_use]
263    fn by_ids<I>(self, ids: I) -> Self
264    where
265        I: IntoIterator<Item = Value>,
266    {
267        self.map_intent(|intent| intent.by_ids(ids))
268    }
269
270    #[must_use]
271    fn only(self, id: Value) -> Self {
272        self.map_intent(|intent| intent.only(id))
273    }
274
275    #[must_use]
276    pub(in crate::db) fn delete(mut self) -> Self {
277        self.intent = self.intent.delete();
278        self
279    }
280
281    #[must_use]
282    pub(in crate::db) fn limit(mut self, limit: u32) -> Self {
283        self.intent = self.intent.limit(limit);
284        self
285    }
286
287    #[must_use]
288    pub(in crate::db) fn offset(mut self, offset: u32) -> Self {
289        self.intent = self.intent.offset(offset);
290        self
291    }
292
293    pub(in crate::db) fn build_plan(&self) -> Result<AccessPlannedQuery, QueryError> {
294        let mut plan = self.intent.build_plan_model()?;
295        self.validate_access_requirements_for_visibility(&mut plan, None)?;
296
297        Ok(plan)
298    }
299
300    pub(in crate::db) fn build_plan_with_visible_indexes(
301        &self,
302        visible_indexes: &VisibleIndexes<'_>,
303    ) -> Result<AccessPlannedQuery, QueryError> {
304        let mut plan = self.intent.build_plan_model_with_indexes(visible_indexes)?;
305        self.validate_access_requirements_for_visibility(&mut plan, Some(visible_indexes))?;
306
307        Ok(plan)
308    }
309
310    pub(in crate::db) fn prepare_scalar_planning_state_with_schema_info(
311        &self,
312        schema_info: SchemaInfo,
313    ) -> Result<PreparedScalarPlanningState<'_>, QueryError> {
314        self.intent
315            .prepare_scalar_planning_state_with_schema_info(schema_info)
316    }
317
318    pub(in crate::db) fn build_plan_with_visible_indexes_from_scalar_planning_state(
319        &self,
320        visible_indexes: &VisibleIndexes<'_>,
321        planning_state: PreparedScalarPlanningState<'_>,
322    ) -> Result<AccessPlannedQuery, QueryError> {
323        let mut plan = self
324            .intent
325            .build_plan_model_with_indexes_from_scalar_planning_state(
326                visible_indexes,
327                planning_state,
328            )?;
329        self.validate_access_requirements_for_visibility(&mut plan, Some(visible_indexes))?;
330
331        Ok(plan)
332    }
333
334    pub(in crate::db) fn try_build_trivial_scalar_load_plan_with_schema_info(
335        &self,
336        schema_info: SchemaInfo,
337    ) -> Result<Option<AccessPlannedQuery>, QueryError> {
338        let mut plan = self
339            .intent
340            .try_build_trivial_scalar_load_plan_with_schema_info(schema_info)?;
341        if let Some(plan) = &mut plan {
342            self.validate_access_requirements_for_visibility(plan, None)?;
343        }
344
345        Ok(plan)
346    }
347
348    #[must_use]
349    pub(in crate::db) fn trivial_scalar_load_fast_path_eligible_with_schema(
350        &self,
351        schema_info: &SchemaInfo,
352    ) -> bool {
353        self.intent
354            .trivial_scalar_load_fast_path_eligible_with_schema(schema_info)
355    }
356
357    #[must_use]
358    #[cfg(test)]
359    pub(in crate::db) fn structural_cache_key(
360        &self,
361    ) -> crate::db::query::intent::StructuralQueryCacheKey {
362        crate::db::query::intent::StructuralQueryCacheKey::from_query_model(&self.intent)
363    }
364
365    #[must_use]
366    pub(in crate::db) fn structural_cache_key_with_normalized_predicate_fingerprint(
367        &self,
368        predicate_fingerprint: Option<[u8; 32]>,
369    ) -> crate::db::query::intent::StructuralQueryCacheKey {
370        self.intent
371            .structural_cache_key_with_normalized_predicate_fingerprint(predicate_fingerprint)
372    }
373
374    // Build one access plan using either schema-owned indexes or the session
375    // visibility slice already resolved at the caller boundary.
376    fn build_plan_for_visibility(
377        &self,
378        visible_indexes: Option<&VisibleIndexes<'_>>,
379    ) -> Result<AccessPlannedQuery, QueryError> {
380        match visible_indexes {
381            Some(visible_indexes) => self.build_plan_with_visible_indexes(visible_indexes),
382            None => self.build_plan(),
383        }
384    }
385
386    fn finalize_access_choice_for_visibility(
387        &self,
388        plan: &mut AccessPlannedQuery,
389        visible_indexes: Option<&VisibleIndexes<'_>>,
390    ) {
391        match visible_indexes {
392            Some(visible_indexes) => {
393                if let Some(schema_info) = visible_indexes.accepted_schema_info() {
394                    plan.finalize_access_choice_for_model_with_accepted_indexes_and_schema(
395                        self.intent.model(),
396                        visible_indexes.accepted_field_path_indexes(),
397                        visible_indexes.accepted_expression_indexes(),
398                        schema_info,
399                    );
400                } else {
401                    plan.finalize_access_choice_for_model_only_with_indexes(
402                        self.intent.model(),
403                        visible_indexes.generated_model_only_indexes(),
404                    );
405                }
406            }
407            None => {
408                plan.finalize_access_choice_for_model_only_with_indexes(
409                    self.intent.model(),
410                    self.intent.model().indexes(),
411                );
412            }
413        }
414    }
415
416    fn validate_access_requirements_for_visibility(
417        &self,
418        plan: &mut AccessPlannedQuery,
419        visible_indexes: Option<&VisibleIndexes<'_>>,
420    ) -> Result<(), QueryError> {
421        if self.access_requirements.is_empty() {
422            return Ok(());
423        }
424
425        self.finalize_access_choice_for_visibility(plan, visible_indexes);
426        self.access_requirements.validate(plan)
427    }
428
429    const fn require_index(mut self) -> Self {
430        self.access_requirements.require_index();
431        self
432    }
433
434    fn require_index_named(mut self, index_name: impl Into<String>) -> Self {
435        self.access_requirements.require_index_named(index_name);
436        self
437    }
438
439    const fn require_access_path(mut self, path: RequiredAccessPath) -> Self {
440        self.access_requirements.require_access_path(path);
441        self
442    }
443
444    const fn require_no_residual_filter(mut self) -> Self {
445        self.access_requirements.require_no_residual_filter();
446        self
447    }
448
449    #[must_use]
450    pub(in crate::db) const fn model(&self) -> &'static crate::model::entity::EntityModel {
451        self.intent.model()
452    }
453}
454
455///
456/// QueryPlanHandle
457///
458/// QueryPlanHandle stores the neutral access-planned query owned by the query
459/// layer. Executor-specific prepared-plan caching remains outside this DTO, so
460/// query values do not depend on executor runtime contracts.
461///
462
463#[derive(Clone, Debug)]
464struct QueryPlanHandle {
465    plan: Box<AccessPlannedQuery>,
466}
467
468impl QueryPlanHandle {
469    #[must_use]
470    fn from_plan(plan: AccessPlannedQuery) -> Self {
471        Self {
472            plan: Box::new(plan),
473        }
474    }
475
476    #[must_use]
477    const fn logical_plan(&self) -> &AccessPlannedQuery {
478        &self.plan
479    }
480
481    #[must_use]
482    #[cfg(test)]
483    fn into_inner(self) -> AccessPlannedQuery {
484        *self.plan
485    }
486}
487
488///
489/// PlannedQuery
490///
491/// PlannedQuery keeps the typed planning surface stable while allowing the
492/// session boundary to reuse one shared prepared-plan artifact internally.
493///
494
495#[derive(Debug)]
496pub struct PlannedQuery<E: EntityKind> {
497    plan: QueryPlanHandle,
498    _marker: PhantomData<E>,
499}
500
501impl<E: EntityKind> PlannedQuery<E> {
502    #[must_use]
503    pub(in crate::db) fn from_plan(plan: AccessPlannedQuery) -> Self {
504        Self {
505            plan: QueryPlanHandle::from_plan(plan),
506            _marker: PhantomData,
507        }
508    }
509
510    #[must_use]
511    pub fn explain(&self) -> ExplainPlan {
512        self.plan.logical_plan().explain()
513    }
514
515    /// Return the stable plan hash for this planned query.
516    #[must_use]
517    pub fn plan_hash_hex(&self) -> String {
518        self.plan.logical_plan().fingerprint().to_string()
519    }
520}
521
522///
523/// CompiledQuery
524///
525/// Typed compiled-query shell over one structural planner contract.
526/// The outer entity marker preserves executor handoff inference without
527/// carrying a second adapter object, while session-owned paths can still reuse
528/// the cached shared prepared plan directly.
529///
530
531#[derive(Clone, Debug)]
532pub struct CompiledQuery<E: EntityKind> {
533    plan: QueryPlanHandle,
534    _marker: PhantomData<E>,
535}
536
537impl<E: EntityKind> CompiledQuery<E> {
538    #[must_use]
539    pub(in crate::db) fn from_plan(plan: AccessPlannedQuery) -> Self {
540        Self {
541            plan: QueryPlanHandle::from_plan(plan),
542            _marker: PhantomData,
543        }
544    }
545
546    #[must_use]
547    pub fn explain(&self) -> ExplainPlan {
548        self.plan.logical_plan().explain()
549    }
550
551    /// Return the stable plan hash for this compiled query.
552    #[must_use]
553    pub fn plan_hash_hex(&self) -> String {
554        self.plan.logical_plan().fingerprint().to_string()
555    }
556
557    #[must_use]
558    #[cfg(test)]
559    pub(in crate::db) fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
560        self.plan.logical_plan().projection_spec(E::MODEL)
561    }
562
563    /// Convert one compiled query back into the neutral planned-query contract.
564    #[cfg(test)]
565    pub(in crate::db) fn into_plan(self) -> AccessPlannedQuery {
566        self.plan.into_inner()
567    }
568
569    #[must_use]
570    #[cfg(test)]
571    pub(in crate::db) fn into_inner(self) -> AccessPlannedQuery {
572        self.plan.into_inner()
573    }
574}
575
576///
577/// Query
578///
579/// Typed, declarative query intent for a specific entity type.
580///
581/// This intent is:
582/// - schema-agnostic at construction
583/// - normalized and validated only during planning
584/// - free of access-path decisions
585///
586
587#[derive(Debug)]
588pub struct Query<E: EntityKind> {
589    inner: StructuralQuery,
590    _marker: PhantomData<E>,
591}
592
593impl<E: EntityKind> Query<E> {
594    // Rebind one structural query core to the typed `Query<E>` surface.
595    pub(in crate::db) const fn from_inner(inner: StructuralQuery) -> Self {
596        Self {
597            inner,
598            _marker: PhantomData,
599        }
600    }
601
602    /// Create a new intent with an explicit missing-row policy.
603    /// Ignore favors idempotency and may mask index/data divergence on deletes.
604    /// Use Error to surface missing rows during scan/delete execution.
605    #[must_use]
606    pub const fn new(consistency: MissingRowPolicy) -> Self {
607        Self::from_inner(StructuralQuery::new(E::MODEL, consistency))
608    }
609
610    /// Return the intent mode (load vs delete).
611    #[must_use]
612    pub const fn mode(&self) -> QueryMode {
613        self.inner.mode()
614    }
615
616    #[cfg(test)]
617    pub(in crate::db) fn explain_with_visible_indexes(
618        &self,
619        visible_indexes: &VisibleIndexes<'_>,
620    ) -> Result<ExplainPlan, QueryError> {
621        let mut plan = self.build_plan_for_visibility(Some(visible_indexes))?;
622        self.inner
623            .finalize_access_choice_for_visibility(&mut plan, Some(visible_indexes));
624
625        Ok(plan.explain())
626    }
627
628    #[cfg(test)]
629    pub(in crate::db) fn plan_hash_hex_with_visible_indexes(
630        &self,
631        visible_indexes: &VisibleIndexes<'_>,
632    ) -> Result<String, QueryError> {
633        let plan = self.build_plan_for_visibility(Some(visible_indexes))?;
634
635        Ok(plan.fingerprint().to_string())
636    }
637
638    // Build one typed access plan using either schema-owned indexes or the
639    // visibility slice already resolved at the session boundary.
640    fn build_plan_for_visibility(
641        &self,
642        visible_indexes: Option<&VisibleIndexes<'_>>,
643    ) -> Result<AccessPlannedQuery, QueryError> {
644        self.inner.build_plan_for_visibility(visible_indexes)
645    }
646
647    // Build one structural plan for the requested visibility lane and then
648    // project it into one typed query-owned contract so planned vs compiled
649    // outputs do not each duplicate the same plan handoff shape.
650    fn map_plan_for_visibility<T>(
651        &self,
652        visible_indexes: Option<&VisibleIndexes<'_>>,
653        map: impl FnOnce(AccessPlannedQuery) -> T,
654    ) -> Result<T, QueryError> {
655        let plan = self.build_plan_for_visibility(visible_indexes)?;
656
657        Ok(map(plan))
658    }
659
660    // Wrap one built plan as the typed planned-query DTO.
661    pub(in crate::db) fn planned_query_from_plan(plan: AccessPlannedQuery) -> PlannedQuery<E> {
662        PlannedQuery::from_plan(plan)
663    }
664
665    // Wrap one built plan as the typed compiled-query DTO.
666    pub(in crate::db) fn compiled_query_from_plan(plan: AccessPlannedQuery) -> CompiledQuery<E> {
667        CompiledQuery::from_plan(plan)
668    }
669
670    #[must_use]
671    pub(in crate::db::query) fn has_explicit_order(&self) -> bool {
672        self.inner.has_explicit_order()
673    }
674
675    #[must_use]
676    pub(in crate::db) const fn structural(&self) -> &StructuralQuery {
677        &self.inner
678    }
679
680    #[must_use]
681    pub const fn has_grouping(&self) -> bool {
682        self.inner.has_grouping()
683    }
684
685    #[must_use]
686    pub(in crate::db::query) const fn load_spec(&self) -> Option<LoadSpec> {
687        self.inner.load_spec()
688    }
689
690    /// Add one typed filter expression, implicitly AND-ing with any existing filter.
691    #[must_use]
692    pub fn filter(mut self, expr: impl Into<FilterExpr>) -> Self {
693        self.inner = self.inner.filter(expr);
694        self
695    }
696
697    // Keep the internal fluent parity hook available for tests that need one
698    // exact expression-owned scalar filter shape instead of the public typed
699    // `FilterExpr` lowering path.
700    #[cfg(test)]
701    #[must_use]
702    pub(in crate::db) fn filter_expr(mut self, expr: Expr) -> Self {
703        self.inner = self.inner.filter_expr(expr);
704        self
705    }
706
707    // Keep the internal predicate-owned filter hook available for convergence
708    // tests without retaining the typed adapter in normal builds after SQL
709    // UPDATE moved to structural lowering.
710    #[cfg(test)]
711    #[must_use]
712    pub(in crate::db) fn filter_predicate(mut self, predicate: Predicate) -> Self {
713        self.inner = self.inner.filter_predicate(predicate);
714        self
715    }
716
717    /// Append one typed ORDER BY term.
718    #[must_use]
719    pub fn order_term(mut self, term: FluentOrderTerm) -> Self {
720        self.inner = self.inner.order_term(term);
721        self
722    }
723
724    /// Append multiple typed ORDER BY terms in declaration order.
725    #[must_use]
726    pub fn order_terms<I>(mut self, terms: I) -> Self
727    where
728        I: IntoIterator<Item = FluentOrderTerm>,
729    {
730        for term in terms {
731            self.inner = self.inner.order_term(term);
732        }
733
734        self
735    }
736
737    /// Enable DISTINCT semantics for this query.
738    #[must_use]
739    pub fn distinct(mut self) -> Self {
740        self.inner = self.inner.distinct();
741        self
742    }
743
744    // Keep the internal fluent SQL parity hook available for lowering tests
745    // without making generated SQL binding depend on the typed query shell.
746    #[cfg(all(test, feature = "sql"))]
747    #[must_use]
748    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
749    where
750        I: IntoIterator<Item = S>,
751        S: Into<String>,
752    {
753        self.inner = self.inner.select_fields(fields);
754        self
755    }
756
757    /// Add one GROUP BY field.
758    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
759        let Self { inner, .. } = self;
760        let inner = inner.group_by(field)?;
761
762        Ok(Self::from_inner(inner))
763    }
764
765    pub(in crate::db) fn group_by_with_schema(
766        self,
767        field: impl AsRef<str>,
768        schema: &SchemaInfo,
769    ) -> Result<Self, QueryError> {
770        let Self { inner, .. } = self;
771        let inner = inner.group_by_with_schema(field, schema)?;
772
773        Ok(Self::from_inner(inner))
774    }
775
776    /// Add one aggregate terminal via composable aggregate expression.
777    #[must_use]
778    pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
779        self.inner = self.inner.aggregate(aggregate);
780        self
781    }
782
783    /// Override grouped hard limits for grouped execution budget enforcement.
784    #[must_use]
785    pub fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
786        self.inner = self.inner.grouped_limits(max_groups, max_group_bytes);
787        self
788    }
789
790    /// Add one grouped HAVING compare clause over one grouped key field.
791    pub fn having_group(
792        self,
793        field: impl AsRef<str>,
794        op: CompareOp,
795        value: InputValue,
796    ) -> Result<Self, QueryError> {
797        let Self { inner, .. } = self;
798        let inner = inner.having_group(field, op, value.into())?;
799
800        Ok(Self::from_inner(inner))
801    }
802
803    pub(in crate::db) fn having_group_with_schema(
804        self,
805        field: impl AsRef<str>,
806        schema: &SchemaInfo,
807        op: CompareOp,
808        value: InputValue,
809    ) -> Result<Self, QueryError> {
810        let Self { inner, .. } = self;
811        let inner = inner.having_group_with_schema(field, schema, op, value.into())?;
812
813        Ok(Self::from_inner(inner))
814    }
815
816    /// Add one grouped HAVING compare clause over one grouped aggregate output.
817    pub fn having_aggregate(
818        self,
819        aggregate_index: usize,
820        op: CompareOp,
821        value: InputValue,
822    ) -> Result<Self, QueryError> {
823        let Self { inner, .. } = self;
824        let inner = inner.having_aggregate(aggregate_index, op, value.into())?;
825
826        Ok(Self::from_inner(inner))
827    }
828
829    // Keep the internal fluent parity hook available for tests that need one
830    // exact grouped HAVING expression shape instead of the public grouped
831    // clause builders.
832    #[cfg(test)]
833    pub(in crate::db) fn having_expr(self, expr: Expr) -> Result<Self, QueryError> {
834        let Self { inner, .. } = self;
835        let inner = inner.having_expr(expr)?;
836
837        Ok(Self::from_inner(inner))
838    }
839
840    /// Set the access path to a single primary key lookup.
841    pub(in crate::db) fn by_id(self, id: E::Key) -> Self {
842        let Self { inner, .. } = self;
843
844        Self::from_inner(inner.by_id(id.to_key_value()))
845    }
846
847    /// Set the access path to a primary key batch lookup.
848    pub(in crate::db) fn by_ids<I>(self, ids: I) -> Self
849    where
850        I: IntoIterator<Item = E::Key>,
851    {
852        let Self { inner, .. } = self;
853
854        Self::from_inner(inner.by_ids(ids.into_iter().map(|id| id.to_key_value())))
855    }
856
857    /// Mark this intent as a delete query.
858    #[must_use]
859    pub fn delete(mut self) -> Self {
860        self.inner = self.inner.delete();
861        self
862    }
863
864    /// Apply a limit to the current mode.
865    ///
866    /// Load limits bound result size; delete limits bound mutation size.
867    /// For scalar load queries, any use of `limit` or `offset` requires an
868    /// explicit `order_term(...)` so pagination is deterministic.
869    /// GROUP BY queries use canonical grouped-key order by default.
870    #[must_use]
871    pub fn limit(mut self, limit: u32) -> Self {
872        self.inner = self.inner.limit(limit);
873        self
874    }
875
876    /// Apply an offset to the current mode.
877    ///
878    /// Scalar load pagination requires an explicit `order_term(...)`.
879    /// GROUP BY queries use canonical grouped-key order by default.
880    /// Delete mode applies this after ordering and predicate filtering.
881    #[must_use]
882    pub fn offset(mut self, offset: u32) -> Self {
883        self.inner = self.inner.offset(offset);
884        self
885    }
886
887    /// Require the planner-selected access path to use a secondary index.
888    ///
889    /// This is a fail-closed assertion evaluated after planning. It does not
890    /// hint, rank, or force index selection.
891    #[must_use]
892    pub fn require_index(mut self) -> Self {
893        self.inner = self.inner.require_index();
894        self
895    }
896
897    /// Require the planner-selected access path to use one semantic index name.
898    ///
899    /// This is intended for hot-path regression checks. It validates the
900    /// selected runtime index contract after planning and never changes ranking.
901    #[must_use]
902    pub fn require_index_named(mut self, index_name: impl Into<String>) -> Self {
903        self.inner = self.inner.require_index_named(index_name);
904        self
905    }
906
907    /// Require one selected access path kind after planning.
908    ///
909    /// This assertion does not act as an optimizer hint.
910    #[must_use]
911    pub fn require_access_path(mut self, path: RequiredAccessPath) -> Self {
912        self.inner = self.inner.require_access_path(path);
913        self
914    }
915
916    /// Require the selected plan to leave no residual filter work.
917    ///
918    /// Access-bound predicates are allowed. Remaining scalar or predicate
919    /// filters after access selection fail this requirement.
920    #[must_use]
921    pub fn require_no_residual_filter(mut self) -> Self {
922        self.inner = self.inner.require_no_residual_filter();
923        self
924    }
925
926    /// Explain this intent without executing it.
927    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
928        let mut plan = self.build_plan_for_visibility(None)?;
929        self.inner
930            .finalize_access_choice_for_visibility(&mut plan, None);
931
932        Ok(plan.explain())
933    }
934
935    /// Return a stable plan hash for this intent.
936    ///
937    /// The hash is derived from canonical planner contracts and is suitable
938    /// for diagnostics, explain diffing, and cache key construction.
939    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
940        let plan = self.inner.build_plan()?;
941
942        Ok(plan.fingerprint().to_string())
943    }
944
945    /// Plan this intent into a neutral planned query contract.
946    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
947        self.map_plan_for_visibility(None, Self::planned_query_from_plan)
948    }
949
950    /// Compile this intent into query-owned handoff state.
951    ///
952    /// This boundary intentionally does not expose executor runtime shape.
953    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
954        self.map_plan_for_visibility(None, Self::compiled_query_from_plan)
955    }
956
957    #[cfg(test)]
958    pub(in crate::db) fn plan_with_visible_indexes(
959        &self,
960        visible_indexes: &VisibleIndexes<'_>,
961    ) -> Result<CompiledQuery<E>, QueryError> {
962        self.map_plan_for_visibility(Some(visible_indexes), Self::compiled_query_from_plan)
963    }
964}
965
966impl<E> Query<E>
967where
968    E: EntityKind + SingletonEntity,
969    E::Key: Default,
970{
971    /// Set the access path to the singleton primary key.
972    pub(in crate::db) fn only(self) -> Self {
973        let Self { inner, .. } = self;
974
975        Self::from_inner(inner.only(E::Key::default().to_key_value()))
976    }
977}