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    #[must_use]
184    pub(in crate::db) fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
185        self.intent = self.intent.push_group_aggregate(aggregate);
186        self
187    }
188
189    #[must_use]
190    fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
191        self.intent = self.intent.grouped_limits(max_groups, max_group_bytes);
192        self
193    }
194
195    pub(in crate::db) fn having_group(
196        self,
197        field: impl AsRef<str>,
198        op: CompareOp,
199        value: Value,
200    ) -> Result<Self, QueryError> {
201        let field = field.as_ref().to_owned();
202        self.try_map_intent(|intent| intent.push_having_group_clause(&field, op, value))
203    }
204
205    pub(in crate::db) fn having_aggregate(
206        self,
207        aggregate_index: usize,
208        op: CompareOp,
209        value: Value,
210    ) -> Result<Self, QueryError> {
211        self.try_map_intent(|intent| {
212            intent.push_having_aggregate_clause(aggregate_index, op, value)
213        })
214    }
215
216    #[cfg(test)]
217    pub(in crate::db) fn having_expr(self, expr: Expr) -> Result<Self, QueryError> {
218        self.try_map_intent(|intent| intent.push_having_expr(expr))
219    }
220
221    pub(in crate::db) fn having_expr_preserving_shape(
222        self,
223        expr: Expr,
224    ) -> Result<Self, QueryError> {
225        self.try_map_intent(|intent| intent.push_having_expr_preserving_shape(expr))
226    }
227
228    #[must_use]
229    fn by_id(self, id: Value) -> Self {
230        self.map_intent(|intent| intent.by_id(id))
231    }
232
233    #[must_use]
234    fn by_ids<I>(self, ids: I) -> Self
235    where
236        I: IntoIterator<Item = Value>,
237    {
238        self.map_intent(|intent| intent.by_ids(ids))
239    }
240
241    #[must_use]
242    fn only(self, id: Value) -> Self {
243        self.map_intent(|intent| intent.only(id))
244    }
245
246    #[must_use]
247    pub(in crate::db) fn delete(mut self) -> Self {
248        self.intent = self.intent.delete();
249        self
250    }
251
252    #[must_use]
253    pub(in crate::db) fn limit(mut self, limit: u32) -> Self {
254        self.intent = self.intent.limit(limit);
255        self
256    }
257
258    #[must_use]
259    pub(in crate::db) fn offset(mut self, offset: u32) -> Self {
260        self.intent = self.intent.offset(offset);
261        self
262    }
263
264    pub(in crate::db) fn build_plan(&self) -> Result<AccessPlannedQuery, QueryError> {
265        self.intent.build_plan_model()
266    }
267
268    pub(in crate::db) fn build_plan_with_visible_indexes(
269        &self,
270        visible_indexes: &VisibleIndexes<'_>,
271    ) -> Result<AccessPlannedQuery, QueryError> {
272        self.intent.build_plan_model_with_indexes(visible_indexes)
273    }
274
275    pub(in crate::db) fn prepare_scalar_planning_state_with_schema_info(
276        &self,
277        schema_info: SchemaInfo,
278    ) -> Result<PreparedScalarPlanningState<'_>, QueryError> {
279        self.intent
280            .prepare_scalar_planning_state_with_schema_info(schema_info)
281    }
282
283    pub(in crate::db) fn build_plan_with_visible_indexes_from_scalar_planning_state(
284        &self,
285        visible_indexes: &VisibleIndexes<'_>,
286        planning_state: PreparedScalarPlanningState<'_>,
287    ) -> Result<AccessPlannedQuery, QueryError> {
288        self.intent
289            .build_plan_model_with_indexes_from_scalar_planning_state(
290                visible_indexes,
291                planning_state,
292            )
293    }
294
295    pub(in crate::db) fn try_build_trivial_scalar_load_plan_with_schema_info(
296        &self,
297        schema_info: SchemaInfo,
298    ) -> Result<Option<AccessPlannedQuery>, QueryError> {
299        self.intent
300            .try_build_trivial_scalar_load_plan_with_schema_info(schema_info)
301    }
302
303    #[must_use]
304    pub(in crate::db) fn trivial_scalar_load_fast_path_eligible(&self) -> bool {
305        self.intent.trivial_scalar_load_fast_path_eligible()
306    }
307
308    #[must_use]
309    #[cfg(test)]
310    pub(in crate::db) fn structural_cache_key(
311        &self,
312    ) -> crate::db::query::intent::StructuralQueryCacheKey {
313        crate::db::query::intent::StructuralQueryCacheKey::from_query_model(&self.intent)
314    }
315
316    #[must_use]
317    pub(in crate::db) fn structural_cache_key_with_normalized_predicate_fingerprint(
318        &self,
319        predicate_fingerprint: Option<[u8; 32]>,
320    ) -> crate::db::query::intent::StructuralQueryCacheKey {
321        self.intent
322            .structural_cache_key_with_normalized_predicate_fingerprint(predicate_fingerprint)
323    }
324
325    // Build one access plan using either schema-owned indexes or the session
326    // visibility slice already resolved at the caller boundary.
327    fn build_plan_for_visibility(
328        &self,
329        visible_indexes: Option<&VisibleIndexes<'_>>,
330    ) -> Result<AccessPlannedQuery, QueryError> {
331        match visible_indexes {
332            Some(visible_indexes) => self.build_plan_with_visible_indexes(visible_indexes),
333            None => self.build_plan(),
334        }
335    }
336
337    #[must_use]
338    pub(in crate::db) const fn model(&self) -> &'static crate::model::entity::EntityModel {
339        self.intent.model()
340    }
341}
342
343///
344/// QueryPlanHandle
345///
346/// QueryPlanHandle stores the neutral access-planned query owned by the query
347/// layer. Executor-specific prepared-plan caching remains outside this DTO, so
348/// query values do not depend on executor runtime contracts.
349///
350
351#[derive(Clone, Debug)]
352struct QueryPlanHandle {
353    plan: Box<AccessPlannedQuery>,
354}
355
356impl QueryPlanHandle {
357    #[must_use]
358    fn from_plan(plan: AccessPlannedQuery) -> Self {
359        Self {
360            plan: Box::new(plan),
361        }
362    }
363
364    #[must_use]
365    const fn logical_plan(&self) -> &AccessPlannedQuery {
366        &self.plan
367    }
368
369    #[must_use]
370    #[cfg(test)]
371    fn into_inner(self) -> AccessPlannedQuery {
372        *self.plan
373    }
374}
375
376///
377/// PlannedQuery
378///
379/// PlannedQuery keeps the typed planning surface stable while allowing the
380/// session boundary to reuse one shared prepared-plan artifact internally.
381///
382
383#[derive(Debug)]
384pub struct PlannedQuery<E: EntityKind> {
385    plan: QueryPlanHandle,
386    _marker: PhantomData<E>,
387}
388
389impl<E: EntityKind> PlannedQuery<E> {
390    #[must_use]
391    pub(in crate::db) fn from_plan(plan: AccessPlannedQuery) -> Self {
392        Self {
393            plan: QueryPlanHandle::from_plan(plan),
394            _marker: PhantomData,
395        }
396    }
397
398    #[must_use]
399    pub fn explain(&self) -> ExplainPlan {
400        self.plan.logical_plan().explain()
401    }
402
403    /// Return the stable plan hash for this planned query.
404    #[must_use]
405    pub fn plan_hash_hex(&self) -> String {
406        self.plan.logical_plan().fingerprint().to_string()
407    }
408}
409
410///
411/// CompiledQuery
412///
413/// Typed compiled-query shell over one structural planner contract.
414/// The outer entity marker preserves executor handoff inference without
415/// carrying a second adapter object, while session-owned paths can still reuse
416/// the cached shared prepared plan directly.
417///
418
419#[derive(Clone, Debug)]
420pub struct CompiledQuery<E: EntityKind> {
421    plan: QueryPlanHandle,
422    _marker: PhantomData<E>,
423}
424
425impl<E: EntityKind> CompiledQuery<E> {
426    #[must_use]
427    pub(in crate::db) fn from_plan(plan: AccessPlannedQuery) -> Self {
428        Self {
429            plan: QueryPlanHandle::from_plan(plan),
430            _marker: PhantomData,
431        }
432    }
433
434    #[must_use]
435    pub fn explain(&self) -> ExplainPlan {
436        self.plan.logical_plan().explain()
437    }
438
439    /// Return the stable plan hash for this compiled query.
440    #[must_use]
441    pub fn plan_hash_hex(&self) -> String {
442        self.plan.logical_plan().fingerprint().to_string()
443    }
444
445    #[must_use]
446    #[cfg(test)]
447    pub(in crate::db) fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
448        self.plan.logical_plan().projection_spec(E::MODEL)
449    }
450
451    /// Convert one compiled query back into the neutral planned-query contract.
452    #[cfg(test)]
453    pub(in crate::db) fn into_plan(self) -> AccessPlannedQuery {
454        self.plan.into_inner()
455    }
456
457    #[must_use]
458    #[cfg(test)]
459    pub(in crate::db) fn into_inner(self) -> AccessPlannedQuery {
460        self.plan.into_inner()
461    }
462}
463
464///
465/// Query
466///
467/// Typed, declarative query intent for a specific entity type.
468///
469/// This intent is:
470/// - schema-agnostic at construction
471/// - normalized and validated only during planning
472/// - free of access-path decisions
473///
474
475#[derive(Debug)]
476pub struct Query<E: EntityKind> {
477    inner: StructuralQuery,
478    _marker: PhantomData<E>,
479}
480
481impl<E: EntityKind> Query<E> {
482    // Rebind one structural query core to the typed `Query<E>` surface.
483    pub(in crate::db) const fn from_inner(inner: StructuralQuery) -> Self {
484        Self {
485            inner,
486            _marker: PhantomData,
487        }
488    }
489
490    /// Create a new intent with an explicit missing-row policy.
491    /// Ignore favors idempotency and may mask index/data divergence on deletes.
492    /// Use Error to surface missing rows during scan/delete execution.
493    #[must_use]
494    pub const fn new(consistency: MissingRowPolicy) -> Self {
495        Self::from_inner(StructuralQuery::new(E::MODEL, consistency))
496    }
497
498    /// Return the intent mode (load vs delete).
499    #[must_use]
500    pub const fn mode(&self) -> QueryMode {
501        self.inner.mode()
502    }
503
504    #[cfg(test)]
505    pub(in crate::db) fn explain_with_visible_indexes(
506        &self,
507        visible_indexes: &VisibleIndexes<'_>,
508    ) -> Result<ExplainPlan, QueryError> {
509        let plan = self.build_plan_for_visibility(Some(visible_indexes))?;
510
511        Ok(plan.explain())
512    }
513
514    #[cfg(test)]
515    pub(in crate::db) fn plan_hash_hex_with_visible_indexes(
516        &self,
517        visible_indexes: &VisibleIndexes<'_>,
518    ) -> Result<String, QueryError> {
519        let plan = self.build_plan_for_visibility(Some(visible_indexes))?;
520
521        Ok(plan.fingerprint().to_string())
522    }
523
524    // Build one typed access plan using either schema-owned indexes or the
525    // visibility slice already resolved at the session boundary.
526    fn build_plan_for_visibility(
527        &self,
528        visible_indexes: Option<&VisibleIndexes<'_>>,
529    ) -> Result<AccessPlannedQuery, QueryError> {
530        self.inner.build_plan_for_visibility(visible_indexes)
531    }
532
533    // Build one structural plan for the requested visibility lane and then
534    // project it into one typed query-owned contract so planned vs compiled
535    // outputs do not each duplicate the same plan handoff shape.
536    fn map_plan_for_visibility<T>(
537        &self,
538        visible_indexes: Option<&VisibleIndexes<'_>>,
539        map: impl FnOnce(AccessPlannedQuery) -> T,
540    ) -> Result<T, QueryError> {
541        let plan = self.build_plan_for_visibility(visible_indexes)?;
542
543        Ok(map(plan))
544    }
545
546    // Wrap one built plan as the typed planned-query DTO.
547    pub(in crate::db) fn planned_query_from_plan(plan: AccessPlannedQuery) -> PlannedQuery<E> {
548        PlannedQuery::from_plan(plan)
549    }
550
551    // Wrap one built plan as the typed compiled-query DTO.
552    pub(in crate::db) fn compiled_query_from_plan(plan: AccessPlannedQuery) -> CompiledQuery<E> {
553        CompiledQuery::from_plan(plan)
554    }
555
556    #[must_use]
557    pub(in crate::db::query) fn has_explicit_order(&self) -> bool {
558        self.inner.has_explicit_order()
559    }
560
561    #[must_use]
562    pub(in crate::db) const fn structural(&self) -> &StructuralQuery {
563        &self.inner
564    }
565
566    #[must_use]
567    pub const fn has_grouping(&self) -> bool {
568        self.inner.has_grouping()
569    }
570
571    #[must_use]
572    pub(in crate::db::query) const fn load_spec(&self) -> Option<LoadSpec> {
573        self.inner.load_spec()
574    }
575
576    /// Add one typed filter expression, implicitly AND-ing with any existing filter.
577    #[must_use]
578    pub fn filter(mut self, expr: impl Into<FilterExpr>) -> Self {
579        self.inner = self.inner.filter(expr);
580        self
581    }
582
583    // Keep the internal fluent parity hook available for tests that need one
584    // exact expression-owned scalar filter shape instead of the public typed
585    // `FilterExpr` lowering path.
586    #[cfg(test)]
587    #[must_use]
588    pub(in crate::db) fn filter_expr(mut self, expr: Expr) -> Self {
589        self.inner = self.inner.filter_expr(expr);
590        self
591    }
592
593    // Keep the internal predicate-owned filter hook available for convergence
594    // tests without retaining the typed adapter in normal builds after SQL
595    // UPDATE moved to structural lowering.
596    #[cfg(test)]
597    #[must_use]
598    pub(in crate::db) fn filter_predicate(mut self, predicate: Predicate) -> Self {
599        self.inner = self.inner.filter_predicate(predicate);
600        self
601    }
602
603    /// Append one typed ORDER BY term.
604    #[must_use]
605    pub fn order_term(mut self, term: FluentOrderTerm) -> Self {
606        self.inner = self.inner.order_term(term);
607        self
608    }
609
610    /// Append multiple typed ORDER BY terms in declaration order.
611    #[must_use]
612    pub fn order_terms<I>(mut self, terms: I) -> Self
613    where
614        I: IntoIterator<Item = FluentOrderTerm>,
615    {
616        for term in terms {
617            self.inner = self.inner.order_term(term);
618        }
619
620        self
621    }
622
623    /// Enable DISTINCT semantics for this query.
624    #[must_use]
625    pub fn distinct(mut self) -> Self {
626        self.inner = self.inner.distinct();
627        self
628    }
629
630    // Keep the internal fluent SQL parity hook available for lowering tests
631    // without making generated SQL binding depend on the typed query shell.
632    #[cfg(all(test, feature = "sql"))]
633    #[must_use]
634    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
635    where
636        I: IntoIterator<Item = S>,
637        S: Into<String>,
638    {
639        self.inner = self.inner.select_fields(fields);
640        self
641    }
642
643    /// Add one GROUP BY field.
644    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
645        let Self { inner, .. } = self;
646        let inner = inner.group_by(field)?;
647
648        Ok(Self::from_inner(inner))
649    }
650
651    /// Add one aggregate terminal via composable aggregate expression.
652    #[must_use]
653    pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
654        self.inner = self.inner.aggregate(aggregate);
655        self
656    }
657
658    /// Override grouped hard limits for grouped execution budget enforcement.
659    #[must_use]
660    pub fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
661        self.inner = self.inner.grouped_limits(max_groups, max_group_bytes);
662        self
663    }
664
665    /// Add one grouped HAVING compare clause over one grouped key field.
666    pub fn having_group(
667        self,
668        field: impl AsRef<str>,
669        op: CompareOp,
670        value: InputValue,
671    ) -> Result<Self, QueryError> {
672        let Self { inner, .. } = self;
673        let inner = inner.having_group(field, op, value.into())?;
674
675        Ok(Self::from_inner(inner))
676    }
677
678    /// Add one grouped HAVING compare clause over one grouped aggregate output.
679    pub fn having_aggregate(
680        self,
681        aggregate_index: usize,
682        op: CompareOp,
683        value: InputValue,
684    ) -> Result<Self, QueryError> {
685        let Self { inner, .. } = self;
686        let inner = inner.having_aggregate(aggregate_index, op, value.into())?;
687
688        Ok(Self::from_inner(inner))
689    }
690
691    // Keep the internal fluent parity hook available for tests that need one
692    // exact grouped HAVING expression shape instead of the public grouped
693    // clause builders.
694    #[cfg(test)]
695    pub(in crate::db) fn having_expr(self, expr: Expr) -> Result<Self, QueryError> {
696        let Self { inner, .. } = self;
697        let inner = inner.having_expr(expr)?;
698
699        Ok(Self::from_inner(inner))
700    }
701
702    /// Set the access path to a single primary key lookup.
703    pub(in crate::db) fn by_id(self, id: E::Key) -> Self {
704        let Self { inner, .. } = self;
705
706        Self::from_inner(inner.by_id(id.to_key_value()))
707    }
708
709    /// Set the access path to a primary key batch lookup.
710    pub(in crate::db) fn by_ids<I>(self, ids: I) -> Self
711    where
712        I: IntoIterator<Item = E::Key>,
713    {
714        let Self { inner, .. } = self;
715
716        Self::from_inner(inner.by_ids(ids.into_iter().map(|id| id.to_key_value())))
717    }
718
719    /// Mark this intent as a delete query.
720    #[must_use]
721    pub fn delete(mut self) -> Self {
722        self.inner = self.inner.delete();
723        self
724    }
725
726    /// Apply a limit to the current mode.
727    ///
728    /// Load limits bound result size; delete limits bound mutation size.
729    /// For scalar load queries, any use of `limit` or `offset` requires an
730    /// explicit `order_term(...)` so pagination is deterministic.
731    /// GROUP BY queries use canonical grouped-key order by default.
732    #[must_use]
733    pub fn limit(mut self, limit: u32) -> Self {
734        self.inner = self.inner.limit(limit);
735        self
736    }
737
738    /// Apply an offset to the current mode.
739    ///
740    /// Scalar load pagination requires an explicit `order_term(...)`.
741    /// GROUP BY queries use canonical grouped-key order by default.
742    /// Delete mode applies this after ordering and predicate filtering.
743    #[must_use]
744    pub fn offset(mut self, offset: u32) -> Self {
745        self.inner = self.inner.offset(offset);
746        self
747    }
748
749    /// Explain this intent without executing it.
750    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
751        let plan = self.planned()?;
752
753        Ok(plan.explain())
754    }
755
756    /// Return a stable plan hash for this intent.
757    ///
758    /// The hash is derived from canonical planner contracts and is suitable
759    /// for diagnostics, explain diffing, and cache key construction.
760    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
761        let plan = self.inner.build_plan()?;
762
763        Ok(plan.fingerprint().to_string())
764    }
765
766    /// Plan this intent into a neutral planned query contract.
767    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
768        self.map_plan_for_visibility(None, Self::planned_query_from_plan)
769    }
770
771    /// Compile this intent into query-owned handoff state.
772    ///
773    /// This boundary intentionally does not expose executor runtime shape.
774    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
775        self.map_plan_for_visibility(None, Self::compiled_query_from_plan)
776    }
777
778    #[cfg(test)]
779    pub(in crate::db) fn plan_with_visible_indexes(
780        &self,
781        visible_indexes: &VisibleIndexes<'_>,
782    ) -> Result<CompiledQuery<E>, QueryError> {
783        self.map_plan_for_visibility(Some(visible_indexes), Self::compiled_query_from_plan)
784    }
785}
786
787impl<E> Query<E>
788where
789    E: EntityKind + SingletonEntity,
790    E::Key: Default,
791{
792    /// Set the access path to the singleton primary key.
793    pub(in crate::db) fn only(self) -> Self {
794        let Self { inner, .. } = self;
795
796        Self::from_inner(inner.only(E::Key::default().to_key_value()))
797    }
798}