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