Skip to main content

icydb_core/db/query/intent/
query.rs

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