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