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