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