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