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
6use crate::{
7    db::{
8        predicate::{CoercionId, CompareOp, MissingRowPolicy, Predicate},
9        query::{
10            builder::aggregate::AggregateExpr,
11            explain::{
12                ExplainAccessPath, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
13                ExplainOrderPushdown, ExplainPlan, ExplainPredicate,
14            },
15            expr::{FilterExpr, SortExpr},
16            intent::{QueryError, model::QueryModel},
17            plan::{AccessPlannedQuery, LoadSpec, QueryMode},
18        },
19    },
20    traits::{EntityKind, EntityValue, FieldValue, SingletonEntity},
21    value::Value,
22};
23use core::marker::PhantomData;
24
25///
26/// StructuralQuery
27///
28/// Generic-free query-intent core shared by typed `Query<E>` wrappers.
29/// Stores model-level key access as `Value` so only typed key-entry helpers
30/// remain entity-specific at the outer API boundary.
31///
32
33#[derive(Debug)]
34struct StructuralQuery {
35    intent: QueryModel<'static, Value>,
36}
37
38impl StructuralQuery {
39    #[must_use]
40    const fn new(
41        model: &'static crate::model::entity::EntityModel,
42        consistency: MissingRowPolicy,
43    ) -> Self {
44        Self {
45            intent: QueryModel::new(model, consistency),
46        }
47    }
48
49    #[must_use]
50    const fn mode(&self) -> QueryMode {
51        self.intent.mode()
52    }
53
54    #[must_use]
55    fn has_explicit_order(&self) -> bool {
56        self.intent.has_explicit_order()
57    }
58
59    #[must_use]
60    const fn has_grouping(&self) -> bool {
61        self.intent.has_grouping()
62    }
63
64    #[must_use]
65    const fn load_spec(&self) -> Option<LoadSpec> {
66        match self.intent.mode() {
67            QueryMode::Load(spec) => Some(spec),
68            QueryMode::Delete(_) => None,
69        }
70    }
71
72    #[must_use]
73    fn filter(mut self, predicate: Predicate) -> Self {
74        self.intent = self.intent.filter(predicate);
75        self
76    }
77
78    fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
79        let Self { intent } = self;
80        let intent = intent.filter_expr(expr)?;
81
82        Ok(Self { intent })
83    }
84
85    fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
86        let Self { intent } = self;
87        let intent = intent.sort_expr(expr)?;
88
89        Ok(Self { intent })
90    }
91
92    #[must_use]
93    fn order_by(mut self, field: impl AsRef<str>) -> Self {
94        self.intent = self.intent.order_by(field);
95        self
96    }
97
98    #[must_use]
99    fn order_by_desc(mut self, field: impl AsRef<str>) -> Self {
100        self.intent = self.intent.order_by_desc(field);
101        self
102    }
103
104    #[must_use]
105    fn distinct(mut self) -> Self {
106        self.intent = self.intent.distinct();
107        self
108    }
109
110    #[cfg(feature = "sql")]
111    #[must_use]
112    fn select_fields<I, S>(mut self, fields: I) -> Self
113    where
114        I: IntoIterator<Item = S>,
115        S: Into<String>,
116    {
117        self.intent = self.intent.select_fields(fields);
118        self
119    }
120
121    fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
122        let Self { intent } = self;
123        let intent = intent.push_group_field(field.as_ref())?;
124
125        Ok(Self { intent })
126    }
127
128    #[must_use]
129    fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
130        self.intent = self.intent.push_group_aggregate(aggregate);
131        self
132    }
133
134    #[must_use]
135    fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
136        self.intent = self.intent.grouped_limits(max_groups, max_group_bytes);
137        self
138    }
139
140    fn having_group(
141        self,
142        field: impl AsRef<str>,
143        op: CompareOp,
144        value: Value,
145    ) -> Result<Self, QueryError> {
146        let field = field.as_ref().to_owned();
147        let Self { intent } = self;
148        let intent = intent.push_having_group_clause(&field, op, value)?;
149
150        Ok(Self { intent })
151    }
152
153    fn having_aggregate(
154        self,
155        aggregate_index: usize,
156        op: CompareOp,
157        value: Value,
158    ) -> Result<Self, QueryError> {
159        let Self { intent } = self;
160        let intent = intent.push_having_aggregate_clause(aggregate_index, op, value)?;
161
162        Ok(Self { intent })
163    }
164
165    #[must_use]
166    fn by_id(self, id: Value) -> Self {
167        let Self { intent } = self;
168        Self {
169            intent: intent.by_id(id),
170        }
171    }
172
173    #[must_use]
174    fn by_ids<I>(self, ids: I) -> Self
175    where
176        I: IntoIterator<Item = Value>,
177    {
178        let Self { intent } = self;
179        Self {
180            intent: intent.by_ids(ids),
181        }
182    }
183
184    #[must_use]
185    fn only(self, id: Value) -> Self {
186        let Self { intent } = self;
187
188        Self {
189            intent: intent.only(id),
190        }
191    }
192
193    #[must_use]
194    fn delete(mut self) -> Self {
195        self.intent = self.intent.delete();
196        self
197    }
198
199    #[must_use]
200    fn limit(mut self, limit: u32) -> Self {
201        self.intent = self.intent.limit(limit);
202        self
203    }
204
205    #[must_use]
206    fn offset(mut self, offset: u32) -> Self {
207        self.intent = self.intent.offset(offset);
208        self
209    }
210
211    fn build_plan(&self) -> Result<AccessPlannedQuery, QueryError> {
212        self.intent.build_plan_model()
213    }
214}
215
216///
217/// PlannedQueryCore
218///
219/// Generic-free planned-query payload shared by typed planned-query wrappers
220/// so explain and plan-hash logic stay structural while public callers retain
221/// entity-specific type inference.
222///
223
224#[derive(Debug)]
225struct PlannedQueryCore {
226    model: &'static crate::model::entity::EntityModel,
227    plan: AccessPlannedQuery,
228}
229
230impl PlannedQueryCore {
231    #[must_use]
232    const fn new(
233        model: &'static crate::model::entity::EntityModel,
234        plan: AccessPlannedQuery,
235    ) -> Self {
236        Self { model, plan }
237    }
238
239    #[must_use]
240    fn explain(&self) -> ExplainPlan {
241        self.plan.explain_with_model(self.model)
242    }
243
244    /// Return the stable plan hash for this planned query.
245    #[must_use]
246    fn plan_hash_hex(&self) -> String {
247        self.plan.fingerprint().to_string()
248    }
249}
250
251///
252/// PlannedQuery
253///
254/// Typed planned-query shell over one generic-free planner contract.
255/// This preserves caller-side entity inference while keeping the stored plan
256/// payload and explain/hash logic structural.
257///
258
259#[derive(Debug)]
260pub struct PlannedQuery<E: EntityKind> {
261    inner: PlannedQueryCore,
262    _marker: PhantomData<E>,
263}
264
265impl<E: EntityKind> PlannedQuery<E> {
266    #[must_use]
267    const fn from_inner(inner: PlannedQueryCore) -> Self {
268        Self {
269            inner,
270            _marker: PhantomData,
271        }
272    }
273
274    #[must_use]
275    pub fn explain(&self) -> ExplainPlan {
276        self.inner.explain()
277    }
278
279    /// Return the stable plan hash for this planned query.
280    #[must_use]
281    pub fn plan_hash_hex(&self) -> String {
282        self.inner.plan_hash_hex()
283    }
284}
285
286///
287/// CompiledQueryCore
288///
289/// Generic-free compiled-query payload shared by typed compiled-query wrappers
290/// so executor handoff state remains structural until the final typed adapter
291/// boundary.
292///
293
294#[derive(Clone, Debug)]
295struct CompiledQueryCore {
296    model: &'static crate::model::entity::EntityModel,
297    entity_path: &'static str,
298    plan: AccessPlannedQuery,
299}
300
301impl CompiledQueryCore {
302    #[must_use]
303    const fn new(
304        model: &'static crate::model::entity::EntityModel,
305        entity_path: &'static str,
306        plan: AccessPlannedQuery,
307    ) -> Self {
308        Self {
309            model,
310            entity_path,
311            plan,
312        }
313    }
314
315    #[must_use]
316    fn explain(&self) -> ExplainPlan {
317        self.plan.explain_with_model(self.model)
318    }
319
320    /// Return the stable plan hash for this compiled query.
321    #[must_use]
322    fn plan_hash_hex(&self) -> String {
323        self.plan.fingerprint().to_string()
324    }
325
326    #[must_use]
327    #[cfg(feature = "sql")]
328    fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
329        self.plan.projection_spec(self.model)
330    }
331
332    #[must_use]
333    fn into_inner(self) -> AccessPlannedQuery {
334        self.plan
335    }
336}
337
338///
339/// CompiledQuery
340///
341/// Typed compiled-query shell over one generic-free planner contract.
342/// The outer entity marker restores inference for executor handoff sites
343/// while the stored execution payload remains structural.
344///
345
346#[derive(Clone, Debug)]
347pub struct CompiledQuery<E: EntityKind> {
348    inner: CompiledQueryCore,
349    _marker: PhantomData<E>,
350}
351
352impl<E: EntityKind> CompiledQuery<E> {
353    #[must_use]
354    const fn from_inner(inner: CompiledQueryCore) -> Self {
355        Self {
356            inner,
357            _marker: PhantomData,
358        }
359    }
360
361    #[must_use]
362    pub fn explain(&self) -> ExplainPlan {
363        self.inner.explain()
364    }
365
366    /// Return the stable plan hash for this compiled query.
367    #[must_use]
368    pub fn plan_hash_hex(&self) -> String {
369        self.inner.plan_hash_hex()
370    }
371
372    #[must_use]
373    #[cfg(feature = "sql")]
374    pub(in crate::db) fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
375        self.inner.projection_spec()
376    }
377
378    /// Convert one structural compiled query into an executor-ready typed plan.
379    pub(in crate::db) fn into_executable(self) -> crate::db::executor::ExecutablePlan<E> {
380        assert!(
381            self.inner.entity_path == E::PATH,
382            "compiled query entity mismatch: compiled for '{}', requested '{}'",
383            self.inner.entity_path,
384            E::PATH,
385        );
386
387        crate::db::executor::ExecutablePlan::new(self.into_inner())
388    }
389
390    #[must_use]
391    pub(in crate::db) fn into_inner(self) -> AccessPlannedQuery {
392        self.inner.into_inner()
393    }
394}
395
396///
397/// Query
398///
399/// Typed, declarative query intent for a specific entity type.
400///
401/// This intent is:
402/// - schema-agnostic at construction
403/// - normalized and validated only during planning
404/// - free of access-path decisions
405///
406
407#[derive(Debug)]
408pub struct Query<E: EntityKind> {
409    inner: StructuralQuery,
410    _marker: PhantomData<E>,
411}
412
413impl<E: EntityKind> Query<E> {
414    // Rebind one structural query core to the typed `Query<E>` surface.
415    const fn from_inner(inner: StructuralQuery) -> Self {
416        Self {
417            inner,
418            _marker: PhantomData,
419        }
420    }
421
422    /// Create a new intent with an explicit missing-row policy.
423    /// Ignore favors idempotency and may mask index/data divergence on deletes.
424    /// Use Error to surface missing rows during scan/delete execution.
425    #[must_use]
426    pub const fn new(consistency: MissingRowPolicy) -> Self {
427        Self::from_inner(StructuralQuery::new(E::MODEL, consistency))
428    }
429
430    /// Return the intent mode (load vs delete).
431    #[must_use]
432    pub const fn mode(&self) -> QueryMode {
433        self.inner.mode()
434    }
435
436    #[must_use]
437    pub(crate) fn has_explicit_order(&self) -> bool {
438        self.inner.has_explicit_order()
439    }
440
441    #[must_use]
442    pub(crate) const fn has_grouping(&self) -> bool {
443        self.inner.has_grouping()
444    }
445
446    #[must_use]
447    pub(crate) const fn load_spec(&self) -> Option<LoadSpec> {
448        self.inner.load_spec()
449    }
450
451    /// Add a predicate, implicitly AND-ing with any existing predicate.
452    #[must_use]
453    pub fn filter(mut self, predicate: Predicate) -> Self {
454        self.inner = self.inner.filter(predicate);
455        self
456    }
457
458    /// Apply a dynamic filter expression.
459    pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
460        let Self { inner, .. } = self;
461        let inner = inner.filter_expr(expr)?;
462
463        Ok(Self::from_inner(inner))
464    }
465
466    /// Apply a dynamic sort expression.
467    pub fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
468        let Self { inner, .. } = self;
469        let inner = inner.sort_expr(expr)?;
470
471        Ok(Self::from_inner(inner))
472    }
473
474    /// Append an ascending sort key.
475    #[must_use]
476    pub fn order_by(mut self, field: impl AsRef<str>) -> Self {
477        self.inner = self.inner.order_by(field);
478        self
479    }
480
481    /// Append a descending sort key.
482    #[must_use]
483    pub fn order_by_desc(mut self, field: impl AsRef<str>) -> Self {
484        self.inner = self.inner.order_by_desc(field);
485        self
486    }
487
488    /// Enable DISTINCT semantics for this query.
489    #[must_use]
490    pub fn distinct(mut self) -> Self {
491        self.inner = self.inner.distinct();
492        self
493    }
494
495    /// Override scalar projection selection with one explicit field list.
496    #[cfg(feature = "sql")]
497    #[must_use]
498    pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
499    where
500        I: IntoIterator<Item = S>,
501        S: Into<String>,
502    {
503        self.inner = self.inner.select_fields(fields);
504        self
505    }
506
507    /// Add one GROUP BY field.
508    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
509        let Self { inner, .. } = self;
510        let inner = inner.group_by(field)?;
511
512        Ok(Self::from_inner(inner))
513    }
514
515    /// Add one aggregate terminal via composable aggregate expression.
516    #[must_use]
517    pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
518        self.inner = self.inner.aggregate(aggregate);
519        self
520    }
521
522    /// Override grouped hard limits for grouped execution budget enforcement.
523    #[must_use]
524    pub fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
525        self.inner = self.inner.grouped_limits(max_groups, max_group_bytes);
526        self
527    }
528
529    /// Add one grouped HAVING compare clause over one grouped key field.
530    pub fn having_group(
531        self,
532        field: impl AsRef<str>,
533        op: CompareOp,
534        value: Value,
535    ) -> Result<Self, QueryError> {
536        let Self { inner, .. } = self;
537        let inner = inner.having_group(field, op, value)?;
538
539        Ok(Self::from_inner(inner))
540    }
541
542    /// Add one grouped HAVING compare clause over one grouped aggregate output.
543    pub fn having_aggregate(
544        self,
545        aggregate_index: usize,
546        op: CompareOp,
547        value: Value,
548    ) -> Result<Self, QueryError> {
549        let Self { inner, .. } = self;
550        let inner = inner.having_aggregate(aggregate_index, op, value)?;
551
552        Ok(Self::from_inner(inner))
553    }
554
555    /// Set the access path to a single primary key lookup.
556    pub(crate) fn by_id(self, id: E::Key) -> Self {
557        let Self { inner, .. } = self;
558
559        Self::from_inner(inner.by_id(id.to_value()))
560    }
561
562    /// Set the access path to a primary key batch lookup.
563    pub(crate) fn by_ids<I>(self, ids: I) -> Self
564    where
565        I: IntoIterator<Item = E::Key>,
566    {
567        let Self { inner, .. } = self;
568
569        Self::from_inner(inner.by_ids(ids.into_iter().map(|id| id.to_value())))
570    }
571
572    /// Mark this intent as a delete query.
573    #[must_use]
574    pub fn delete(mut self) -> Self {
575        self.inner = self.inner.delete();
576        self
577    }
578
579    /// Apply a limit to the current mode.
580    ///
581    /// Load limits bound result size; delete limits bound mutation size.
582    /// For scalar load queries, any use of `limit` or `offset` requires an
583    /// explicit `order_by(...)` so pagination is deterministic.
584    /// GROUP BY queries use canonical grouped-key order by default.
585    #[must_use]
586    pub fn limit(mut self, limit: u32) -> Self {
587        self.inner = self.inner.limit(limit);
588        self
589    }
590
591    /// Apply an offset to a load intent.
592    ///
593    /// Scalar pagination requires an explicit `order_by(...)`.
594    /// GROUP BY queries use canonical grouped-key order by default.
595    /// Delete intents reject `offset(...)` during planning.
596    #[must_use]
597    pub fn offset(mut self, offset: u32) -> Self {
598        self.inner = self.inner.offset(offset);
599        self
600    }
601
602    /// Explain this intent without executing it.
603    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
604        let plan = self.planned()?;
605
606        Ok(plan.explain())
607    }
608
609    /// Return a stable plan hash for this intent.
610    ///
611    /// The hash is derived from canonical planner contracts and is suitable
612    /// for diagnostics, explain diffing, and cache key construction.
613    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
614        let plan = self.inner.build_plan()?;
615
616        Ok(plan.fingerprint().to_string())
617    }
618
619    /// Explain executor-selected scalar load execution shape without running it.
620    pub fn explain_execution(&self) -> Result<ExplainExecutionNodeDescriptor, QueryError>
621    where
622        E: EntityValue,
623    {
624        let executable = self.plan()?.into_executable();
625
626        executable
627            .explain_load_execution_node_descriptor()
628            .map_err(QueryError::execute)
629    }
630
631    /// Explain executor-selected scalar load execution shape as deterministic text.
632    pub fn explain_execution_text(&self) -> Result<String, QueryError>
633    where
634        E: EntityValue,
635    {
636        Ok(self.explain_execution()?.render_text_tree())
637    }
638
639    /// Explain executor-selected scalar load execution shape as canonical JSON.
640    pub fn explain_execution_json(&self) -> Result<String, QueryError>
641    where
642        E: EntityValue,
643    {
644        Ok(self.explain_execution()?.render_json_canonical())
645    }
646
647    /// Explain executor-selected scalar load execution shape with route diagnostics.
648    pub fn explain_execution_verbose(&self) -> Result<String, QueryError>
649    where
650        E: EntityValue,
651    {
652        let executable = self.plan()?.into_executable();
653        let descriptor = executable
654            .explain_load_execution_node_descriptor()
655            .map_err(QueryError::execute)?;
656        let route_diagnostics = executable
657            .explain_load_execution_verbose_diagnostics()
658            .map_err(QueryError::execute)?;
659        let explain = self.explain()?;
660
661        // Phase 1: render descriptor tree with node-local metadata.
662        let mut lines = vec![descriptor.render_text_tree_verbose()];
663        lines.extend(route_diagnostics);
664
665        // Phase 2: add descriptor-stage summaries for key execution operators.
666        lines.push(format!(
667            "diagnostic.descriptor.has_top_n_seek={}",
668            contains_execution_node_type(&descriptor, ExplainExecutionNodeType::TopNSeek)
669        ));
670        lines.push(format!(
671            "diagnostic.descriptor.has_index_range_limit_pushdown={}",
672            contains_execution_node_type(
673                &descriptor,
674                ExplainExecutionNodeType::IndexRangeLimitPushdown,
675            )
676        ));
677        lines.push(format!(
678            "diagnostic.descriptor.has_index_predicate_prefilter={}",
679            contains_execution_node_type(
680                &descriptor,
681                ExplainExecutionNodeType::IndexPredicatePrefilter,
682            )
683        ));
684        lines.push(format!(
685            "diagnostic.descriptor.has_residual_predicate_filter={}",
686            contains_execution_node_type(
687                &descriptor,
688                ExplainExecutionNodeType::ResidualPredicateFilter,
689            )
690        ));
691
692        // Phase 3: append logical-plan diagnostics relevant to verbose explain.
693        lines.push(format!("diagnostic.plan.mode={:?}", explain.mode()));
694        lines.push(format!(
695            "diagnostic.plan.order_pushdown={}",
696            plan_order_pushdown_label(explain.order_pushdown())
697        ));
698        lines.push(format!(
699            "diagnostic.plan.predicate_pushdown={}",
700            plan_predicate_pushdown_label(explain.predicate(), explain.access())
701        ));
702        lines.push(format!("diagnostic.plan.distinct={}", explain.distinct()));
703        lines.push(format!("diagnostic.plan.page={:?}", explain.page()));
704        lines.push(format!(
705            "diagnostic.plan.consistency={:?}",
706            explain.consistency()
707        ));
708
709        Ok(lines.join("\n"))
710    }
711
712    /// Plan this intent into a neutral planned query contract.
713    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
714        let plan = self.inner.build_plan()?;
715        let _projection = plan.projection_spec(E::MODEL);
716
717        Ok(PlannedQuery::from_inner(PlannedQueryCore::new(
718            E::MODEL,
719            plan,
720        )))
721    }
722
723    /// Compile this intent into query-owned handoff state.
724    ///
725    /// This boundary intentionally does not expose executor runtime shape.
726    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
727        let plan = self.inner.build_plan()?;
728        let _projection = plan.projection_spec(E::MODEL);
729
730        Ok(CompiledQuery::from_inner(CompiledQueryCore::new(
731            E::MODEL,
732            E::PATH,
733            plan,
734        )))
735    }
736}
737
738fn contains_execution_node_type(
739    descriptor: &ExplainExecutionNodeDescriptor,
740    target: ExplainExecutionNodeType,
741) -> bool {
742    descriptor.node_type() == target
743        || descriptor
744            .children()
745            .iter()
746            .any(|child| contains_execution_node_type(child, target))
747}
748
749fn plan_order_pushdown_label(order_pushdown: &ExplainOrderPushdown) -> String {
750    match order_pushdown {
751        ExplainOrderPushdown::MissingModelContext => "missing_model_context".to_string(),
752        ExplainOrderPushdown::EligibleSecondaryIndex { index, prefix_len } => {
753            format!("eligible(index={index},prefix_len={prefix_len})",)
754        }
755        ExplainOrderPushdown::Rejected(reason) => format!("rejected({reason:?})"),
756    }
757}
758
759fn plan_predicate_pushdown_label(
760    predicate: &ExplainPredicate,
761    access: &ExplainAccessPath,
762) -> String {
763    let access_label = match access {
764        ExplainAccessPath::ByKey { .. } => "by_key",
765        ExplainAccessPath::ByKeys { keys } if keys.is_empty() => "empty_access_contract",
766        ExplainAccessPath::ByKeys { .. } => "by_keys",
767        ExplainAccessPath::KeyRange { .. } => "key_range",
768        ExplainAccessPath::IndexPrefix { .. } => "index_prefix",
769        ExplainAccessPath::IndexMultiLookup { .. } => "index_multi_lookup",
770        ExplainAccessPath::IndexRange { .. } => "index_range",
771        ExplainAccessPath::FullScan => "full_scan",
772        ExplainAccessPath::Union(_) => "union",
773        ExplainAccessPath::Intersection(_) => "intersection",
774    };
775    if matches!(predicate, ExplainPredicate::None) {
776        return "none".to_string();
777    }
778    if matches!(access, ExplainAccessPath::FullScan) {
779        if explain_predicate_contains_non_strict_compare(predicate) {
780            return "fallback(non_strict_compare_coercion)".to_string();
781        }
782        if explain_predicate_contains_empty_prefix_starts_with(predicate) {
783            return "fallback(starts_with_empty_prefix)".to_string();
784        }
785        if explain_predicate_contains_is_null(predicate) {
786            return "fallback(is_null_full_scan)".to_string();
787        }
788        if explain_predicate_contains_text_scan_operator(predicate) {
789            return "fallback(text_operator_full_scan)".to_string();
790        }
791
792        return format!("fallback({access_label})");
793    }
794
795    format!("applied({access_label})")
796}
797
798fn explain_predicate_contains_non_strict_compare(predicate: &ExplainPredicate) -> bool {
799    match predicate {
800        ExplainPredicate::Compare { coercion, .. } => coercion.id != CoercionId::Strict,
801        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
802            .iter()
803            .any(explain_predicate_contains_non_strict_compare),
804        ExplainPredicate::Not(inner) => explain_predicate_contains_non_strict_compare(inner),
805        ExplainPredicate::None
806        | ExplainPredicate::True
807        | ExplainPredicate::False
808        | ExplainPredicate::IsNull { .. }
809        | ExplainPredicate::IsNotNull { .. }
810        | ExplainPredicate::IsMissing { .. }
811        | ExplainPredicate::IsEmpty { .. }
812        | ExplainPredicate::IsNotEmpty { .. }
813        | ExplainPredicate::TextContains { .. }
814        | ExplainPredicate::TextContainsCi { .. } => false,
815    }
816}
817
818fn explain_predicate_contains_is_null(predicate: &ExplainPredicate) -> bool {
819    match predicate {
820        ExplainPredicate::IsNull { .. } => true,
821        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => {
822            children.iter().any(explain_predicate_contains_is_null)
823        }
824        ExplainPredicate::Not(inner) => explain_predicate_contains_is_null(inner),
825        ExplainPredicate::None
826        | ExplainPredicate::True
827        | ExplainPredicate::False
828        | ExplainPredicate::Compare { .. }
829        | ExplainPredicate::IsNotNull { .. }
830        | ExplainPredicate::IsMissing { .. }
831        | ExplainPredicate::IsEmpty { .. }
832        | ExplainPredicate::IsNotEmpty { .. }
833        | ExplainPredicate::TextContains { .. }
834        | ExplainPredicate::TextContainsCi { .. } => false,
835    }
836}
837
838fn explain_predicate_contains_empty_prefix_starts_with(predicate: &ExplainPredicate) -> bool {
839    match predicate {
840        ExplainPredicate::Compare {
841            op: CompareOp::StartsWith,
842            value: Value::Text(prefix),
843            ..
844        } => prefix.is_empty(),
845        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
846            .iter()
847            .any(explain_predicate_contains_empty_prefix_starts_with),
848        ExplainPredicate::Not(inner) => explain_predicate_contains_empty_prefix_starts_with(inner),
849        ExplainPredicate::None
850        | ExplainPredicate::True
851        | ExplainPredicate::False
852        | ExplainPredicate::Compare { .. }
853        | ExplainPredicate::IsNull { .. }
854        | ExplainPredicate::IsNotNull { .. }
855        | ExplainPredicate::IsMissing { .. }
856        | ExplainPredicate::IsEmpty { .. }
857        | ExplainPredicate::IsNotEmpty { .. }
858        | ExplainPredicate::TextContains { .. }
859        | ExplainPredicate::TextContainsCi { .. } => false,
860    }
861}
862
863fn explain_predicate_contains_text_scan_operator(predicate: &ExplainPredicate) -> bool {
864    match predicate {
865        ExplainPredicate::Compare {
866            op: CompareOp::EndsWith,
867            ..
868        }
869        | ExplainPredicate::TextContains { .. }
870        | ExplainPredicate::TextContainsCi { .. } => true,
871        ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
872            .iter()
873            .any(explain_predicate_contains_text_scan_operator),
874        ExplainPredicate::Not(inner) => explain_predicate_contains_text_scan_operator(inner),
875        ExplainPredicate::Compare { .. }
876        | ExplainPredicate::None
877        | ExplainPredicate::True
878        | ExplainPredicate::False
879        | ExplainPredicate::IsNull { .. }
880        | ExplainPredicate::IsNotNull { .. }
881        | ExplainPredicate::IsMissing { .. }
882        | ExplainPredicate::IsEmpty { .. }
883        | ExplainPredicate::IsNotEmpty { .. } => false,
884    }
885}
886
887impl<E> Query<E>
888where
889    E: EntityKind + SingletonEntity,
890    E::Key: Default,
891{
892    /// Set the access path to the singleton primary key.
893    pub(crate) fn only(self) -> Self {
894        let Self { inner, .. } = self;
895
896        Self::from_inner(inner.only(E::Key::default().to_value()))
897    }
898}