1use 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, SingletonEntity},
21 value::Value,
22};
23use core::marker::PhantomData;
24
25#[derive(Debug)]
37pub struct Query<E: EntityKind> {
38 intent: QueryModel<'static, E::Key>,
39}
40
41impl<E: EntityKind> Query<E> {
42 #[must_use]
46 pub const fn new(consistency: MissingRowPolicy) -> Self {
47 Self {
48 intent: QueryModel::new(E::MODEL, consistency),
49 }
50 }
51
52 #[must_use]
54 pub const fn mode(&self) -> QueryMode {
55 self.intent.mode()
56 }
57
58 #[must_use]
59 pub(crate) fn has_explicit_order(&self) -> bool {
60 self.intent.has_explicit_order()
61 }
62
63 #[must_use]
64 pub(crate) const fn has_grouping(&self) -> bool {
65 self.intent.has_grouping()
66 }
67
68 #[must_use]
69 pub(crate) const fn load_spec(&self) -> Option<LoadSpec> {
70 match self.intent.mode() {
71 QueryMode::Load(spec) => Some(spec),
72 QueryMode::Delete(_) => None,
73 }
74 }
75
76 #[must_use]
78 pub fn filter(mut self, predicate: Predicate) -> Self {
79 self.intent = self.intent.filter(predicate);
80 self
81 }
82
83 pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
85 let Self { intent } = self;
86 let intent = intent.filter_expr(expr)?;
87
88 Ok(Self { intent })
89 }
90
91 pub fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
93 let Self { intent } = self;
94 let intent = intent.sort_expr(expr)?;
95
96 Ok(Self { intent })
97 }
98
99 #[must_use]
101 pub fn order_by(mut self, field: impl AsRef<str>) -> Self {
102 self.intent = self.intent.order_by(field);
103 self
104 }
105
106 #[must_use]
108 pub fn order_by_desc(mut self, field: impl AsRef<str>) -> Self {
109 self.intent = self.intent.order_by_desc(field);
110 self
111 }
112
113 #[must_use]
115 pub fn distinct(mut self) -> Self {
116 self.intent = self.intent.distinct();
117 self
118 }
119
120 #[must_use]
122 pub(in crate::db) fn select_fields<I, S>(mut self, fields: I) -> Self
123 where
124 I: IntoIterator<Item = S>,
125 S: Into<String>,
126 {
127 self.intent = self.intent.select_fields(fields);
128 self
129 }
130
131 pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
133 let Self { intent } = self;
134 let intent = intent.push_group_field(field.as_ref())?;
135
136 Ok(Self { intent })
137 }
138
139 #[must_use]
141 pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
142 self.intent = self.intent.push_group_aggregate(aggregate);
143 self
144 }
145
146 #[must_use]
148 pub fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
149 self.intent = self.intent.grouped_limits(max_groups, max_group_bytes);
150 self
151 }
152
153 pub fn having_group(
155 self,
156 field: impl AsRef<str>,
157 op: CompareOp,
158 value: Value,
159 ) -> Result<Self, QueryError> {
160 let field = field.as_ref().to_owned();
161 let Self { intent } = self;
162 let intent = intent.push_having_group_clause(&field, op, value)?;
163
164 Ok(Self { intent })
165 }
166
167 pub fn having_aggregate(
169 self,
170 aggregate_index: usize,
171 op: CompareOp,
172 value: Value,
173 ) -> Result<Self, QueryError> {
174 let Self { intent } = self;
175 let intent = intent.push_having_aggregate_clause(aggregate_index, op, value)?;
176
177 Ok(Self { intent })
178 }
179
180 pub(crate) fn by_id(self, id: E::Key) -> Self {
182 let Self { intent } = self;
183 Self {
184 intent: intent.by_id(id),
185 }
186 }
187
188 pub(crate) fn by_ids<I>(self, ids: I) -> Self
190 where
191 I: IntoIterator<Item = E::Key>,
192 {
193 let Self { intent } = self;
194 Self {
195 intent: intent.by_ids(ids),
196 }
197 }
198
199 #[must_use]
201 pub fn delete(mut self) -> Self {
202 self.intent = self.intent.delete();
203 self
204 }
205
206 #[must_use]
213 pub fn limit(mut self, limit: u32) -> Self {
214 self.intent = self.intent.limit(limit);
215 self
216 }
217
218 #[must_use]
224 pub fn offset(mut self, offset: u32) -> Self {
225 self.intent = self.intent.offset(offset);
226 self
227 }
228
229 pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
231 let plan = self.planned()?;
232
233 Ok(plan.explain())
234 }
235
236 pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
241 let plan = self.build_plan()?;
242
243 Ok(plan.fingerprint().to_string())
244 }
245
246 pub fn explain_execution(&self) -> Result<ExplainExecutionNodeDescriptor, QueryError>
248 where
249 E: EntityValue,
250 {
251 let executable = self.plan()?.into_executable();
252
253 executable
254 .explain_load_execution_node_descriptor()
255 .map_err(QueryError::execute)
256 }
257
258 pub fn explain_execution_text(&self) -> Result<String, QueryError>
260 where
261 E: EntityValue,
262 {
263 Ok(self.explain_execution()?.render_text_tree())
264 }
265
266 pub fn explain_execution_json(&self) -> Result<String, QueryError>
268 where
269 E: EntityValue,
270 {
271 Ok(self.explain_execution()?.render_json_canonical())
272 }
273
274 pub fn explain_execution_verbose(&self) -> Result<String, QueryError>
276 where
277 E: EntityValue,
278 {
279 let executable = self.plan()?.into_executable();
280 let descriptor = executable
281 .explain_load_execution_node_descriptor()
282 .map_err(QueryError::execute)?;
283 let route_diagnostics = executable
284 .explain_load_execution_verbose_diagnostics()
285 .map_err(QueryError::execute)?;
286 let explain = self.explain()?;
287
288 let mut lines = vec![descriptor.render_text_tree_verbose()];
290 lines.extend(route_diagnostics);
291
292 lines.push(format!(
294 "diagnostic.descriptor.has_top_n_seek={}",
295 contains_execution_node_type(&descriptor, ExplainExecutionNodeType::TopNSeek)
296 ));
297 lines.push(format!(
298 "diagnostic.descriptor.has_index_range_limit_pushdown={}",
299 contains_execution_node_type(
300 &descriptor,
301 ExplainExecutionNodeType::IndexRangeLimitPushdown,
302 )
303 ));
304 lines.push(format!(
305 "diagnostic.descriptor.has_index_predicate_prefilter={}",
306 contains_execution_node_type(
307 &descriptor,
308 ExplainExecutionNodeType::IndexPredicatePrefilter,
309 )
310 ));
311 lines.push(format!(
312 "diagnostic.descriptor.has_residual_predicate_filter={}",
313 contains_execution_node_type(
314 &descriptor,
315 ExplainExecutionNodeType::ResidualPredicateFilter,
316 )
317 ));
318
319 lines.push(format!("diagnostic.plan.mode={:?}", explain.mode()));
321 lines.push(format!(
322 "diagnostic.plan.order_pushdown={}",
323 plan_order_pushdown_label(explain.order_pushdown())
324 ));
325 lines.push(format!(
326 "diagnostic.plan.predicate_pushdown={}",
327 plan_predicate_pushdown_label(explain.predicate(), explain.access())
328 ));
329 lines.push(format!("diagnostic.plan.distinct={}", explain.distinct()));
330 lines.push(format!("diagnostic.plan.page={:?}", explain.page()));
331 lines.push(format!(
332 "diagnostic.plan.consistency={:?}",
333 explain.consistency()
334 ));
335
336 Ok(lines.join("\n"))
337 }
338
339 pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
341 let plan = self.build_plan()?;
342 let _projection = plan.projection_spec(E::MODEL);
343
344 Ok(PlannedQuery::<E>::new(plan))
345 }
346
347 pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
351 let plan = self.build_plan()?;
352 let _projection = plan.projection_spec(E::MODEL);
353
354 Ok(CompiledQuery::<E>::new(plan))
355 }
356
357 fn build_plan(&self) -> Result<AccessPlannedQuery, QueryError> {
359 self.intent.build_plan_model()
360 }
361}
362
363fn contains_execution_node_type(
364 descriptor: &ExplainExecutionNodeDescriptor,
365 target: ExplainExecutionNodeType,
366) -> bool {
367 descriptor.node_type() == target
368 || descriptor
369 .children()
370 .iter()
371 .any(|child| contains_execution_node_type(child, target))
372}
373
374fn plan_order_pushdown_label(order_pushdown: &ExplainOrderPushdown) -> String {
375 match order_pushdown {
376 ExplainOrderPushdown::MissingModelContext => "missing_model_context".to_string(),
377 ExplainOrderPushdown::EligibleSecondaryIndex { index, prefix_len } => {
378 format!("eligible(index={index},prefix_len={prefix_len})",)
379 }
380 ExplainOrderPushdown::Rejected(reason) => format!("rejected({reason:?})"),
381 }
382}
383
384fn plan_predicate_pushdown_label(
385 predicate: &ExplainPredicate,
386 access: &ExplainAccessPath,
387) -> String {
388 let access_label = match access {
389 ExplainAccessPath::ByKey { .. } => "by_key",
390 ExplainAccessPath::ByKeys { keys } if keys.is_empty() => "empty_access_contract",
391 ExplainAccessPath::ByKeys { .. } => "by_keys",
392 ExplainAccessPath::KeyRange { .. } => "key_range",
393 ExplainAccessPath::IndexPrefix { .. } => "index_prefix",
394 ExplainAccessPath::IndexMultiLookup { .. } => "index_multi_lookup",
395 ExplainAccessPath::IndexRange { .. } => "index_range",
396 ExplainAccessPath::FullScan => "full_scan",
397 ExplainAccessPath::Union(_) => "union",
398 ExplainAccessPath::Intersection(_) => "intersection",
399 };
400 if matches!(predicate, ExplainPredicate::None) {
401 return "none".to_string();
402 }
403 if matches!(access, ExplainAccessPath::FullScan) {
404 if explain_predicate_contains_non_strict_compare(predicate) {
405 return "fallback(non_strict_compare_coercion)".to_string();
406 }
407 if explain_predicate_contains_empty_prefix_starts_with(predicate) {
408 return "fallback(starts_with_empty_prefix)".to_string();
409 }
410 if explain_predicate_contains_is_null(predicate) {
411 return "fallback(is_null_full_scan)".to_string();
412 }
413 if explain_predicate_contains_text_scan_operator(predicate) {
414 return "fallback(text_operator_full_scan)".to_string();
415 }
416
417 return format!("fallback({access_label})");
418 }
419
420 format!("applied({access_label})")
421}
422
423fn explain_predicate_contains_non_strict_compare(predicate: &ExplainPredicate) -> bool {
424 match predicate {
425 ExplainPredicate::Compare { coercion, .. } => coercion.id != CoercionId::Strict,
426 ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
427 .iter()
428 .any(explain_predicate_contains_non_strict_compare),
429 ExplainPredicate::Not(inner) => explain_predicate_contains_non_strict_compare(inner),
430 ExplainPredicate::None
431 | ExplainPredicate::True
432 | ExplainPredicate::False
433 | ExplainPredicate::IsNull { .. }
434 | ExplainPredicate::IsNotNull { .. }
435 | ExplainPredicate::IsMissing { .. }
436 | ExplainPredicate::IsEmpty { .. }
437 | ExplainPredicate::IsNotEmpty { .. }
438 | ExplainPredicate::TextContains { .. }
439 | ExplainPredicate::TextContainsCi { .. } => false,
440 }
441}
442
443fn explain_predicate_contains_is_null(predicate: &ExplainPredicate) -> bool {
444 match predicate {
445 ExplainPredicate::IsNull { .. } => true,
446 ExplainPredicate::And(children) | ExplainPredicate::Or(children) => {
447 children.iter().any(explain_predicate_contains_is_null)
448 }
449 ExplainPredicate::Not(inner) => explain_predicate_contains_is_null(inner),
450 ExplainPredicate::None
451 | ExplainPredicate::True
452 | ExplainPredicate::False
453 | ExplainPredicate::Compare { .. }
454 | ExplainPredicate::IsNotNull { .. }
455 | ExplainPredicate::IsMissing { .. }
456 | ExplainPredicate::IsEmpty { .. }
457 | ExplainPredicate::IsNotEmpty { .. }
458 | ExplainPredicate::TextContains { .. }
459 | ExplainPredicate::TextContainsCi { .. } => false,
460 }
461}
462
463fn explain_predicate_contains_empty_prefix_starts_with(predicate: &ExplainPredicate) -> bool {
464 match predicate {
465 ExplainPredicate::Compare {
466 op: CompareOp::StartsWith,
467 value: Value::Text(prefix),
468 ..
469 } => prefix.is_empty(),
470 ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
471 .iter()
472 .any(explain_predicate_contains_empty_prefix_starts_with),
473 ExplainPredicate::Not(inner) => explain_predicate_contains_empty_prefix_starts_with(inner),
474 ExplainPredicate::None
475 | ExplainPredicate::True
476 | ExplainPredicate::False
477 | ExplainPredicate::Compare { .. }
478 | ExplainPredicate::IsNull { .. }
479 | ExplainPredicate::IsNotNull { .. }
480 | ExplainPredicate::IsMissing { .. }
481 | ExplainPredicate::IsEmpty { .. }
482 | ExplainPredicate::IsNotEmpty { .. }
483 | ExplainPredicate::TextContains { .. }
484 | ExplainPredicate::TextContainsCi { .. } => false,
485 }
486}
487
488fn explain_predicate_contains_text_scan_operator(predicate: &ExplainPredicate) -> bool {
489 match predicate {
490 ExplainPredicate::Compare {
491 op: CompareOp::EndsWith,
492 ..
493 }
494 | ExplainPredicate::TextContains { .. }
495 | ExplainPredicate::TextContainsCi { .. } => true,
496 ExplainPredicate::And(children) | ExplainPredicate::Or(children) => children
497 .iter()
498 .any(explain_predicate_contains_text_scan_operator),
499 ExplainPredicate::Not(inner) => explain_predicate_contains_text_scan_operator(inner),
500 ExplainPredicate::Compare { .. }
501 | ExplainPredicate::None
502 | ExplainPredicate::True
503 | ExplainPredicate::False
504 | ExplainPredicate::IsNull { .. }
505 | ExplainPredicate::IsNotNull { .. }
506 | ExplainPredicate::IsMissing { .. }
507 | ExplainPredicate::IsEmpty { .. }
508 | ExplainPredicate::IsNotEmpty { .. } => false,
509 }
510}
511
512impl<E> Query<E>
513where
514 E: EntityKind + SingletonEntity,
515 E::Key: Default,
516{
517 pub(crate) fn only(self) -> Self {
519 let Self { intent } = self;
520
521 Self {
522 intent: intent.only(E::Key::default()),
523 }
524 }
525}
526
527#[derive(Debug)]
535pub struct PlannedQuery<E: EntityKind> {
536 plan: AccessPlannedQuery,
537 _marker: PhantomData<E>,
538}
539
540impl<E: EntityKind> PlannedQuery<E> {
541 #[must_use]
542 pub(in crate::db) const fn new(plan: AccessPlannedQuery) -> Self {
543 Self {
544 plan,
545 _marker: PhantomData,
546 }
547 }
548
549 #[must_use]
550 pub fn explain(&self) -> ExplainPlan {
551 self.plan.explain_with_model(E::MODEL)
552 }
553
554 #[must_use]
556 pub fn plan_hash_hex(&self) -> String {
557 self.plan.fingerprint().to_string()
558 }
559}
560
561#[derive(Clone, Debug)]
570pub struct CompiledQuery<E: EntityKind> {
571 plan: AccessPlannedQuery,
572 _marker: PhantomData<E>,
573}
574
575impl<E: EntityKind> CompiledQuery<E> {
576 #[must_use]
577 pub(in crate::db) const fn new(plan: AccessPlannedQuery) -> Self {
578 Self {
579 plan,
580 _marker: PhantomData,
581 }
582 }
583
584 #[must_use]
585 pub fn explain(&self) -> ExplainPlan {
586 self.plan.explain_with_model(E::MODEL)
587 }
588
589 #[must_use]
591 pub fn plan_hash_hex(&self) -> String {
592 self.plan.fingerprint().to_string()
593 }
594
595 #[must_use]
597 pub(in crate::db) fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
598 self.plan.projection_spec(E::MODEL)
599 }
600
601 #[must_use]
602 pub(in crate::db) fn into_inner(self) -> AccessPlannedQuery {
603 self.plan
604 }
605}
606
607#[cfg(test)]
612mod tests {
613 use super::*;
614 use crate::{db::predicate::CoercionSpec, types::Ulid};
615
616 fn strict_compare(field: &str, op: CompareOp, value: Value) -> ExplainPredicate {
617 ExplainPredicate::Compare {
618 field: field.to_string(),
619 op,
620 value,
621 coercion: CoercionSpec::new(CoercionId::Strict),
622 }
623 }
624
625 #[test]
626 fn predicate_pushdown_label_prefix_like_and_equivalent_range_share_label() {
627 let starts_with_predicate = strict_compare(
628 "name",
629 CompareOp::StartsWith,
630 Value::Text("foo".to_string()),
631 );
632 let equivalent_range_predicate = ExplainPredicate::And(vec![
633 strict_compare("name", CompareOp::Gte, Value::Text("foo".to_string())),
634 strict_compare("name", CompareOp::Lt, Value::Text("fop".to_string())),
635 ]);
636 let access = ExplainAccessPath::IndexRange {
637 name: "idx_name",
638 fields: vec!["name"],
639 prefix_len: 0,
640 prefix: Vec::new(),
641 lower: std::ops::Bound::Included(Value::Text("foo".to_string())),
642 upper: std::ops::Bound::Excluded(Value::Text("fop".to_string())),
643 };
644
645 assert_eq!(
646 plan_predicate_pushdown_label(&starts_with_predicate, &access),
647 plan_predicate_pushdown_label(&equivalent_range_predicate, &access),
648 "equivalent prefix-like and bounded-range shapes should report identical pushdown reason labels",
649 );
650 assert_eq!(
651 plan_predicate_pushdown_label(&starts_with_predicate, &access),
652 "applied(index_range)"
653 );
654 }
655
656 #[test]
657 fn predicate_pushdown_label_distinguishes_is_null_and_non_strict_full_scan_fallbacks() {
658 let is_null_predicate = ExplainPredicate::IsNull {
659 field: "group".to_string(),
660 };
661 let non_strict_predicate = ExplainPredicate::Compare {
662 field: "group".to_string(),
663 op: CompareOp::Eq,
664 value: Value::Uint(7),
665 coercion: CoercionSpec::new(CoercionId::NumericWiden),
666 };
667 let access = ExplainAccessPath::FullScan;
668
669 assert_eq!(
670 plan_predicate_pushdown_label(&is_null_predicate, &access),
671 "fallback(is_null_full_scan)"
672 );
673 assert_eq!(
674 plan_predicate_pushdown_label(&non_strict_predicate, &access),
675 "fallback(non_strict_compare_coercion)"
676 );
677 }
678
679 #[test]
680 fn predicate_pushdown_label_reports_none_when_no_predicate_is_present() {
681 let predicate = ExplainPredicate::None;
682 let access = ExplainAccessPath::ByKey {
683 key: Value::Ulid(Ulid::from_u128(7)),
684 };
685
686 assert_eq!(plan_predicate_pushdown_label(&predicate, &access), "none");
687 }
688
689 #[test]
690 fn predicate_pushdown_label_reports_empty_access_contract_for_impossible_shapes() {
691 let predicate = ExplainPredicate::Or(vec![
692 ExplainPredicate::IsNull {
693 field: "id".to_string(),
694 },
695 ExplainPredicate::And(vec![
696 ExplainPredicate::Compare {
697 field: "id".to_string(),
698 op: CompareOp::In,
699 value: Value::List(Vec::new()),
700 coercion: CoercionSpec::new(CoercionId::Strict),
701 },
702 ExplainPredicate::True,
703 ]),
704 ]);
705 let access = ExplainAccessPath::ByKeys { keys: Vec::new() };
706
707 assert_eq!(
708 plan_predicate_pushdown_label(&predicate, &access),
709 "applied(empty_access_contract)"
710 );
711 }
712
713 #[test]
714 fn predicate_pushdown_label_distinguishes_empty_prefix_starts_with_full_scan_fallback() {
715 let empty_prefix_predicate = ExplainPredicate::Compare {
716 field: "label".to_string(),
717 op: CompareOp::StartsWith,
718 value: Value::Text(String::new()),
719 coercion: CoercionSpec::new(CoercionId::Strict),
720 };
721 let non_empty_prefix_predicate = ExplainPredicate::Compare {
722 field: "label".to_string(),
723 op: CompareOp::StartsWith,
724 value: Value::Text("l".to_string()),
725 coercion: CoercionSpec::new(CoercionId::Strict),
726 };
727 let access = ExplainAccessPath::FullScan;
728
729 assert_eq!(
730 plan_predicate_pushdown_label(&empty_prefix_predicate, &access),
731 "fallback(starts_with_empty_prefix)"
732 );
733 assert_eq!(
734 plan_predicate_pushdown_label(&non_empty_prefix_predicate, &access),
735 "fallback(full_scan)"
736 );
737 }
738
739 #[test]
740 fn predicate_pushdown_label_reports_text_operator_full_scan_fallback() {
741 let text_contains = ExplainPredicate::TextContainsCi {
742 field: "label".to_string(),
743 value: Value::Text("needle".to_string()),
744 };
745 let ends_with = ExplainPredicate::Compare {
746 field: "label".to_string(),
747 op: CompareOp::EndsWith,
748 value: Value::Text("fix".to_string()),
749 coercion: CoercionSpec::new(CoercionId::Strict),
750 };
751 let access = ExplainAccessPath::FullScan;
752
753 assert_eq!(
754 plan_predicate_pushdown_label(&text_contains, &access),
755 "fallback(text_operator_full_scan)"
756 );
757 assert_eq!(
758 plan_predicate_pushdown_label(&ends_with, &access),
759 "fallback(text_operator_full_scan)"
760 );
761 }
762
763 #[test]
764 fn predicate_pushdown_label_keeps_collection_contains_on_generic_full_scan_fallback() {
765 let collection_contains = ExplainPredicate::Compare {
766 field: "tags".to_string(),
767 op: CompareOp::Contains,
768 value: Value::Uint(7),
769 coercion: CoercionSpec::new(CoercionId::CollectionElement),
770 };
771 let access = ExplainAccessPath::FullScan;
772
773 assert_eq!(
774 plan_predicate_pushdown_label(&collection_contains, &access),
775 "fallback(non_strict_compare_coercion)"
776 );
777 assert_ne!(
778 plan_predicate_pushdown_label(&collection_contains, &access),
779 "fallback(text_operator_full_scan)"
780 );
781 }
782
783 #[test]
784 fn predicate_pushdown_label_non_strict_ends_with_uses_non_strict_fallback_precedence() {
785 let non_strict_ends_with = ExplainPredicate::Compare {
786 field: "label".to_string(),
787 op: CompareOp::EndsWith,
788 value: Value::Text("fix".to_string()),
789 coercion: CoercionSpec::new(CoercionId::TextCasefold),
790 };
791 let access = ExplainAccessPath::FullScan;
792
793 assert_eq!(
794 plan_predicate_pushdown_label(&non_strict_ends_with, &access),
795 "fallback(non_strict_compare_coercion)"
796 );
797 assert_ne!(
798 plan_predicate_pushdown_label(&non_strict_ends_with, &access),
799 "fallback(text_operator_full_scan)"
800 );
801 }
802}