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