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