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