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