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