1use crate::storage::query::ast::{
14 CompareOp, FieldRef, Filter as AstFilter, JoinQuery, Projection, QueryExpr,
15};
16use crate::storage::query::sql_lowering::{
17 effective_graph_filter, effective_join_filter, effective_table_filter, effective_vector_filter,
18};
19use crate::storage::schema::Value;
20
21#[derive(Debug, Clone, Default)]
23pub struct RewriteContext {
24 pub property_cache: Vec<CachedProperty>,
26 pub errors: Vec<String>,
28 pub warnings: Vec<String>,
30 pub stats: RewriteStats,
32}
33
34#[derive(Debug, Clone)]
36pub struct CachedProperty {
37 pub source: String,
39 pub property: String,
41 pub cached_value: Option<String>,
43}
44
45#[derive(Debug, Clone, Default)]
47pub struct RewriteStats {
48 pub filters_simplified: u32,
50 pub predicates_pushed: u32,
52 pub properties_cached: u32,
54 pub expressions_normalized: u32,
56}
57
58pub trait RewriteRule: Send + Sync {
60 fn name(&self) -> &str;
62
63 fn apply(&self, query: QueryExpr, ctx: &mut RewriteContext) -> QueryExpr;
65
66 fn is_applicable(&self, query: &QueryExpr) -> bool;
68}
69
70pub struct QueryRewriter {
72 rules: Vec<Box<dyn RewriteRule>>,
74 max_iterations: usize,
76}
77
78impl QueryRewriter {
79 pub fn new() -> Self {
81 let rules: Vec<Box<dyn RewriteRule>> = vec![
82 Box::new(NormalizeRule),
83 Box::new(SimplifyFiltersRule),
84 Box::new(PushdownPredicatesRule),
85 Box::new(EliminateDeadCodeRule),
86 Box::new(FoldConstantsRule),
87 ];
88
89 Self {
90 rules,
91 max_iterations: 10,
92 }
93 }
94
95 pub fn add_rule(&mut self, rule: Box<dyn RewriteRule>) {
97 self.rules.push(rule);
98 }
99
100 pub fn rewrite(&self, query: QueryExpr) -> QueryExpr {
102 let mut ctx = RewriteContext::default();
103 self.rewrite_with_context(query, &mut ctx)
104 }
105
106 pub fn rewrite_with_context(
108 &self,
109 mut query: QueryExpr,
110 ctx: &mut RewriteContext,
111 ) -> QueryExpr {
112 for _iteration in 0..self.max_iterations {
114 let original = format!("{:?}", query);
115
116 for rule in &self.rules {
117 if rule.is_applicable(&query) {
118 query = rule.apply(query, ctx);
119 }
120 }
121
122 if format!("{:?}", query) == original {
124 break;
125 }
126 }
127
128 query
129 }
130}
131
132impl Default for QueryRewriter {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138struct NormalizeRule;
144
145impl RewriteRule for NormalizeRule {
146 fn name(&self) -> &str {
147 "Normalize"
148 }
149
150 fn apply(&self, query: QueryExpr, ctx: &mut RewriteContext) -> QueryExpr {
151 match query {
152 QueryExpr::Table(mut tq) => {
153 tq.columns.sort_by(|a, b| {
155 let a_name = projection_name(a);
156 let b_name = projection_name(b);
157 a_name.cmp(&b_name)
158 });
159 ctx.stats.expressions_normalized += 1;
160 QueryExpr::Table(tq)
161 }
162 QueryExpr::Graph(gq) => {
163 QueryExpr::Graph(gq)
165 }
166 QueryExpr::Join(jq) => {
167 let left = self.apply(*jq.left, ctx);
169 let right = self.apply(*jq.right, ctx);
170 QueryExpr::Join(JoinQuery {
171 left: Box::new(left),
172 right: Box::new(right),
173 ..jq
174 })
175 }
176 QueryExpr::Path(pq) => QueryExpr::Path(pq),
177 QueryExpr::Vector(vq) => {
178 QueryExpr::Vector(vq)
180 }
181 QueryExpr::Hybrid(mut hq) => {
182 hq.structured = Box::new(self.apply(*hq.structured, ctx));
184 QueryExpr::Hybrid(hq)
185 }
186 other @ (QueryExpr::Insert(_)
188 | QueryExpr::Update(_)
189 | QueryExpr::Delete(_)
190 | QueryExpr::CreateTable(_)
191 | QueryExpr::CreateCollection(_)
192 | QueryExpr::CreateVector(_)
193 | QueryExpr::DropTable(_)
194 | QueryExpr::DropGraph(_)
195 | QueryExpr::DropVector(_)
196 | QueryExpr::DropDocument(_)
197 | QueryExpr::DropKv(_)
198 | QueryExpr::DropCollection(_)
199 | QueryExpr::Truncate(_)
200 | QueryExpr::AlterTable(_)
201 | QueryExpr::GraphCommand(_)
202 | QueryExpr::SearchCommand(_)
203 | QueryExpr::CreateIndex(_)
204 | QueryExpr::DropIndex(_)
205 | QueryExpr::ProbabilisticCommand(_)
206 | QueryExpr::Ask(_)
207 | QueryExpr::SetConfig { .. }
208 | QueryExpr::ShowConfig { .. }
209 | QueryExpr::SetSecret { .. }
210 | QueryExpr::DeleteSecret { .. }
211 | QueryExpr::ShowSecrets { .. }
212 | QueryExpr::SetTenant(_)
213 | QueryExpr::ShowTenant
214 | QueryExpr::CreateTimeSeries(_)
215 | QueryExpr::CreateMetric(_)
216 | QueryExpr::AlterMetric(_)
217 | QueryExpr::CreateSlo(_)
218 | QueryExpr::DropTimeSeries(_)
219 | QueryExpr::CreateQueue(_)
220 | QueryExpr::AlterQueue(_)
221 | QueryExpr::DropQueue(_)
222 | QueryExpr::QueueSelect(_)
223 | QueryExpr::QueueCommand(_)
224 | QueryExpr::KvCommand(_)
225 | QueryExpr::ConfigCommand(_)
226 | QueryExpr::CreateTree(_)
227 | QueryExpr::DropTree(_)
228 | QueryExpr::TreeCommand(_)
229 | QueryExpr::ExplainAlter(_)
230 | QueryExpr::TransactionControl(_)
231 | QueryExpr::MaintenanceCommand(_)
232 | QueryExpr::CreateSchema(_)
233 | QueryExpr::DropSchema(_)
234 | QueryExpr::CreateSequence(_)
235 | QueryExpr::DropSequence(_)
236 | QueryExpr::CopyFrom(_)
237 | QueryExpr::CreateView(_)
238 | QueryExpr::DropView(_)
239 | QueryExpr::RefreshMaterializedView(_)
240 | QueryExpr::CreatePolicy(_)
241 | QueryExpr::DropPolicy(_)
242 | QueryExpr::CreateServer(_)
243 | QueryExpr::DropServer(_)
244 | QueryExpr::CreateForeignTable(_)
245 | QueryExpr::DropForeignTable(_)
246 | QueryExpr::Grant(_)
247 | QueryExpr::Revoke(_)
248 | QueryExpr::AlterUser(_)
249 | QueryExpr::CreateIamPolicy { .. }
250 | QueryExpr::DropIamPolicy { .. }
251 | QueryExpr::AttachPolicy { .. }
252 | QueryExpr::DetachPolicy { .. }
253 | QueryExpr::ShowPolicies { .. }
254 | QueryExpr::ShowEffectivePermissions { .. }
255 | QueryExpr::RankOf(_)
256 | QueryExpr::ApproxRankOf(_)
257 | QueryExpr::RankRange(_)
258 | QueryExpr::SimulatePolicy { .. }
259 | QueryExpr::LintPolicy { .. }
260 | QueryExpr::MigratePolicyMode { .. }
261 | QueryExpr::CreateMigration(_)
262 | QueryExpr::ApplyMigration(_)
263 | QueryExpr::RollbackMigration(_)
264 | QueryExpr::ExplainMigration(_)
265 | QueryExpr::EventsBackfill(_)
266 | QueryExpr::EventsBackfillStatus { .. }) => other,
267 }
268 }
269
270 fn is_applicable(&self, _query: &QueryExpr) -> bool {
271 true
272 }
273}
274
275struct SimplifyFiltersRule;
277
278impl RewriteRule for SimplifyFiltersRule {
279 fn name(&self) -> &str {
280 "SimplifyFilters"
281 }
282
283 fn apply(&self, query: QueryExpr, ctx: &mut RewriteContext) -> QueryExpr {
284 match query {
285 QueryExpr::Table(mut tq) => {
286 if let Some(filter) = effective_table_filter(&tq) {
287 tq.filter = Some(simplify_filter(filter, ctx));
288 }
289 QueryExpr::Table(tq)
290 }
291 QueryExpr::Graph(mut gq) => {
292 if let Some(filter) = effective_graph_filter(&gq) {
293 gq.filter = Some(simplify_filter(filter, ctx));
294 }
295 QueryExpr::Graph(gq)
296 }
297 QueryExpr::Join(mut jq) => {
298 let join_filter = effective_join_filter(&jq);
299 let left = self.apply(*jq.left, ctx);
300 let right = self.apply(*jq.right, ctx);
301 if let Some(filter) = join_filter {
302 jq.filter = Some(simplify_filter(filter, ctx));
303 }
304 jq.left = Box::new(left);
305 jq.right = Box::new(right);
306 QueryExpr::Join(jq)
307 }
308 QueryExpr::Path(pq) => QueryExpr::Path(pq),
309 QueryExpr::Vector(vq) => {
310 QueryExpr::Vector(vq)
313 }
314 QueryExpr::Hybrid(mut hq) => {
315 hq.structured = Box::new(self.apply(*hq.structured, ctx));
317 QueryExpr::Hybrid(hq)
318 }
319 other @ (QueryExpr::Insert(_)
321 | QueryExpr::Update(_)
322 | QueryExpr::Delete(_)
323 | QueryExpr::CreateTable(_)
324 | QueryExpr::CreateCollection(_)
325 | QueryExpr::CreateVector(_)
326 | QueryExpr::DropTable(_)
327 | QueryExpr::DropGraph(_)
328 | QueryExpr::DropVector(_)
329 | QueryExpr::DropDocument(_)
330 | QueryExpr::DropKv(_)
331 | QueryExpr::DropCollection(_)
332 | QueryExpr::Truncate(_)
333 | QueryExpr::AlterTable(_)
334 | QueryExpr::GraphCommand(_)
335 | QueryExpr::SearchCommand(_)
336 | QueryExpr::CreateIndex(_)
337 | QueryExpr::DropIndex(_)
338 | QueryExpr::ProbabilisticCommand(_)
339 | QueryExpr::Ask(_)
340 | QueryExpr::SetConfig { .. }
341 | QueryExpr::ShowConfig { .. }
342 | QueryExpr::SetSecret { .. }
343 | QueryExpr::DeleteSecret { .. }
344 | QueryExpr::ShowSecrets { .. }
345 | QueryExpr::SetTenant(_)
346 | QueryExpr::ShowTenant
347 | QueryExpr::CreateTimeSeries(_)
348 | QueryExpr::CreateMetric(_)
349 | QueryExpr::AlterMetric(_)
350 | QueryExpr::CreateSlo(_)
351 | QueryExpr::DropTimeSeries(_)
352 | QueryExpr::CreateQueue(_)
353 | QueryExpr::AlterQueue(_)
354 | QueryExpr::DropQueue(_)
355 | QueryExpr::QueueSelect(_)
356 | QueryExpr::QueueCommand(_)
357 | QueryExpr::KvCommand(_)
358 | QueryExpr::ConfigCommand(_)
359 | QueryExpr::CreateTree(_)
360 | QueryExpr::DropTree(_)
361 | QueryExpr::TreeCommand(_)
362 | QueryExpr::ExplainAlter(_)
363 | QueryExpr::TransactionControl(_)
364 | QueryExpr::MaintenanceCommand(_)
365 | QueryExpr::CreateSchema(_)
366 | QueryExpr::DropSchema(_)
367 | QueryExpr::CreateSequence(_)
368 | QueryExpr::DropSequence(_)
369 | QueryExpr::CopyFrom(_)
370 | QueryExpr::CreateView(_)
371 | QueryExpr::DropView(_)
372 | QueryExpr::RefreshMaterializedView(_)
373 | QueryExpr::CreatePolicy(_)
374 | QueryExpr::DropPolicy(_)
375 | QueryExpr::CreateServer(_)
376 | QueryExpr::DropServer(_)
377 | QueryExpr::CreateForeignTable(_)
378 | QueryExpr::DropForeignTable(_)
379 | QueryExpr::Grant(_)
380 | QueryExpr::Revoke(_)
381 | QueryExpr::AlterUser(_)
382 | QueryExpr::CreateIamPolicy { .. }
383 | QueryExpr::DropIamPolicy { .. }
384 | QueryExpr::AttachPolicy { .. }
385 | QueryExpr::DetachPolicy { .. }
386 | QueryExpr::ShowPolicies { .. }
387 | QueryExpr::ShowEffectivePermissions { .. }
388 | QueryExpr::RankOf(_)
389 | QueryExpr::ApproxRankOf(_)
390 | QueryExpr::RankRange(_)
391 | QueryExpr::SimulatePolicy { .. }
392 | QueryExpr::LintPolicy { .. }
393 | QueryExpr::MigratePolicyMode { .. }
394 | QueryExpr::CreateMigration(_)
395 | QueryExpr::ApplyMigration(_)
396 | QueryExpr::RollbackMigration(_)
397 | QueryExpr::ExplainMigration(_)
398 | QueryExpr::EventsBackfill(_)
399 | QueryExpr::EventsBackfillStatus { .. }) => other,
400 }
401 }
402
403 fn is_applicable(&self, query: &QueryExpr) -> bool {
404 match query {
405 QueryExpr::Table(tq) => effective_table_filter(tq).is_some(),
406 QueryExpr::Graph(gq) => effective_graph_filter(gq).is_some(),
407 QueryExpr::Join(_) => true,
408 QueryExpr::Path(_) => false,
409 QueryExpr::Vector(vq) => effective_vector_filter(vq).is_some(),
410 QueryExpr::Hybrid(_) => true, QueryExpr::Insert(_)
413 | QueryExpr::Update(_)
414 | QueryExpr::Delete(_)
415 | QueryExpr::CreateTable(_)
416 | QueryExpr::CreateCollection(_)
417 | QueryExpr::CreateVector(_)
418 | QueryExpr::DropTable(_)
419 | QueryExpr::DropGraph(_)
420 | QueryExpr::DropVector(_)
421 | QueryExpr::DropDocument(_)
422 | QueryExpr::DropKv(_)
423 | QueryExpr::DropCollection(_)
424 | QueryExpr::Truncate(_)
425 | QueryExpr::AlterTable(_)
426 | QueryExpr::GraphCommand(_)
427 | QueryExpr::SearchCommand(_)
428 | QueryExpr::CreateIndex(_)
429 | QueryExpr::DropIndex(_)
430 | QueryExpr::ProbabilisticCommand(_)
431 | QueryExpr::Ask(_)
432 | QueryExpr::SetConfig { .. }
433 | QueryExpr::ShowConfig { .. }
434 | QueryExpr::SetSecret { .. }
435 | QueryExpr::DeleteSecret { .. }
436 | QueryExpr::ShowSecrets { .. }
437 | QueryExpr::SetTenant(_)
438 | QueryExpr::ShowTenant
439 | QueryExpr::CreateTimeSeries(_)
440 | QueryExpr::CreateMetric(_)
441 | QueryExpr::AlterMetric(_)
442 | QueryExpr::CreateSlo(_)
443 | QueryExpr::DropTimeSeries(_)
444 | QueryExpr::CreateQueue(_)
445 | QueryExpr::AlterQueue(_)
446 | QueryExpr::DropQueue(_)
447 | QueryExpr::QueueSelect(_)
448 | QueryExpr::QueueCommand(_)
449 | QueryExpr::KvCommand(_)
450 | QueryExpr::ConfigCommand(_)
451 | QueryExpr::CreateTree(_)
452 | QueryExpr::DropTree(_)
453 | QueryExpr::TreeCommand(_)
454 | QueryExpr::ExplainAlter(_)
455 | QueryExpr::TransactionControl(_)
456 | QueryExpr::MaintenanceCommand(_)
457 | QueryExpr::CreateSchema(_)
458 | QueryExpr::DropSchema(_)
459 | QueryExpr::CreateSequence(_)
460 | QueryExpr::DropSequence(_)
461 | QueryExpr::CopyFrom(_)
462 | QueryExpr::CreateView(_)
463 | QueryExpr::DropView(_)
464 | QueryExpr::RefreshMaterializedView(_)
465 | QueryExpr::CreatePolicy(_)
466 | QueryExpr::DropPolicy(_)
467 | QueryExpr::CreateServer(_)
468 | QueryExpr::DropServer(_)
469 | QueryExpr::CreateForeignTable(_)
470 | QueryExpr::DropForeignTable(_)
471 | QueryExpr::Grant(_)
472 | QueryExpr::Revoke(_)
473 | QueryExpr::AlterUser(_)
474 | QueryExpr::CreateIamPolicy { .. }
475 | QueryExpr::DropIamPolicy { .. }
476 | QueryExpr::AttachPolicy { .. }
477 | QueryExpr::DetachPolicy { .. }
478 | QueryExpr::ShowPolicies { .. }
479 | QueryExpr::ShowEffectivePermissions { .. }
480 | QueryExpr::RankOf(_)
481 | QueryExpr::ApproxRankOf(_)
482 | QueryExpr::RankRange(_)
483 | QueryExpr::SimulatePolicy { .. }
484 | QueryExpr::LintPolicy { .. }
485 | QueryExpr::MigratePolicyMode { .. }
486 | QueryExpr::CreateMigration(_)
487 | QueryExpr::ApplyMigration(_)
488 | QueryExpr::RollbackMigration(_)
489 | QueryExpr::ExplainMigration(_)
490 | QueryExpr::EventsBackfill(_)
491 | QueryExpr::EventsBackfillStatus { .. } => false,
492 }
493 }
494}
495
496struct PushdownPredicatesRule;
498
499impl RewriteRule for PushdownPredicatesRule {
500 fn name(&self) -> &str {
501 "PushdownPredicates"
502 }
503
504 fn apply(&self, query: QueryExpr, ctx: &mut RewriteContext) -> QueryExpr {
505 match query {
506 QueryExpr::Join(mut jq) => {
507 jq.left = Box::new(self.apply(*jq.left, ctx));
513 jq.right = Box::new(self.apply(*jq.right, ctx));
514 ctx.stats.predicates_pushed += 1;
515 QueryExpr::Join(jq)
516 }
517 other => other,
518 }
519 }
520
521 fn is_applicable(&self, query: &QueryExpr) -> bool {
522 matches!(query, QueryExpr::Join(_))
523 }
524}
525
526struct EliminateDeadCodeRule;
528
529impl RewriteRule for EliminateDeadCodeRule {
530 fn name(&self) -> &str {
531 "EliminateDeadCode"
532 }
533
534 fn apply(&self, query: QueryExpr, _ctx: &mut RewriteContext) -> QueryExpr {
535 match query {
536 QueryExpr::Table(mut tq) => {
537 if let Some(filter) = effective_table_filter(&tq).as_ref() {
539 if is_always_true(filter) {
540 tq.filter = None;
541 }
542 }
543 QueryExpr::Table(tq)
544 }
545 other => other,
546 }
547 }
548
549 fn is_applicable(&self, query: &QueryExpr) -> bool {
550 matches!(query, QueryExpr::Table(_))
551 }
552}
553
554struct FoldConstantsRule;
556
557impl RewriteRule for FoldConstantsRule {
558 fn name(&self) -> &str {
559 "FoldConstants"
560 }
561
562 fn apply(&self, query: QueryExpr, _ctx: &mut RewriteContext) -> QueryExpr {
563 query
566 }
567
568 fn is_applicable(&self, _query: &QueryExpr) -> bool {
569 true
570 }
571}
572
573fn projection_name(proj: &Projection) -> String {
578 match proj {
579 Projection::All => "*".to_string(),
580 Projection::Column(name) => name.clone(),
581 Projection::Alias(_, alias) => alias.clone(),
582 Projection::Function(name, _) => name
583 .split_once(':')
584 .map(|(_, alias)| alias.to_string())
585 .unwrap_or_else(|| name.clone()),
586 Projection::Expression(expr, alias) => {
587 alias.clone().unwrap_or_else(|| format!("{:?}", expr))
588 }
589 Projection::Field(field, alias) => alias.clone().unwrap_or_else(|| format!("{:?}", field)),
590 Projection::Window { name, alias, .. } => alias.clone().unwrap_or_else(|| name.clone()),
591 }
592}
593
594fn simplify_filter(filter: AstFilter, ctx: &mut RewriteContext) -> AstFilter {
595 match filter {
596 AstFilter::And(left, right) => {
597 let left = simplify_filter(*left, ctx);
598 let right = simplify_filter(*right, ctx);
599
600 if is_always_true(&left) {
602 ctx.stats.filters_simplified += 1;
603 return right;
604 }
605 if is_always_true(&right) {
606 ctx.stats.filters_simplified += 1;
607 return left;
608 }
609
610 if is_always_false(&left) || is_always_false(&right) {
612 ctx.stats.filters_simplified += 1;
613 return AstFilter::Compare {
614 field: FieldRef::TableColumn {
615 table: String::new(),
616 column: "1".to_string(),
617 },
618 op: CompareOp::Eq,
619 value: Value::Integer(0),
620 };
621 }
622
623 AstFilter::And(Box::new(left), Box::new(right))
624 }
625 AstFilter::Or(left, right) => {
626 let left = simplify_filter(*left, ctx);
627 let right = simplify_filter(*right, ctx);
628
629 if is_always_false(&left) {
631 ctx.stats.filters_simplified += 1;
632 return right;
633 }
634 if is_always_false(&right) {
635 ctx.stats.filters_simplified += 1;
636 return left;
637 }
638
639 if is_always_true(&left) || is_always_true(&right) {
641 ctx.stats.filters_simplified += 1;
642 return AstFilter::Compare {
643 field: FieldRef::TableColumn {
644 table: String::new(),
645 column: "1".to_string(),
646 },
647 op: CompareOp::Eq,
648 value: Value::Integer(1),
649 };
650 }
651
652 AstFilter::Or(Box::new(left), Box::new(right))
653 }
654 AstFilter::Not(inner) => {
655 let inner = simplify_filter(*inner, ctx);
656
657 if let AstFilter::Not(double_inner) = inner {
659 ctx.stats.filters_simplified += 1;
660 return *double_inner;
661 }
662
663 AstFilter::Not(Box::new(inner))
664 }
665 other => other,
666 }
667}
668
669fn is_always_true(filter: &AstFilter) -> bool {
670 match filter {
671 AstFilter::Compare { field, op, value } => {
672 matches!(field, FieldRef::TableColumn { column, .. } if column == "1")
674 && matches!(op, CompareOp::Eq)
675 && matches!(value, Value::Integer(1))
676 }
677 _ => false,
678 }
679}
680
681fn is_always_false(filter: &AstFilter) -> bool {
682 match filter {
683 AstFilter::Compare { field, op, value } => {
684 matches!(field, FieldRef::TableColumn { column, .. } if column == "1")
686 && matches!(op, CompareOp::Eq)
687 && matches!(value, Value::Integer(0))
688 }
689 _ => false,
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696
697 fn make_field(name: &str) -> FieldRef {
698 FieldRef::TableColumn {
699 table: String::new(),
700 column: name.to_string(),
701 }
702 }
703
704 #[test]
705 fn test_simplify_and_with_true() {
706 let mut ctx = RewriteContext::default();
707
708 let filter = AstFilter::And(
709 Box::new(AstFilter::Compare {
710 field: make_field("1"),
711 op: CompareOp::Eq,
712 value: Value::Integer(1),
713 }),
714 Box::new(AstFilter::Compare {
715 field: make_field("x"),
716 op: CompareOp::Eq,
717 value: Value::Integer(5),
718 }),
719 );
720
721 let simplified = simplify_filter(filter, &mut ctx);
722
723 match simplified {
724 AstFilter::Compare { field, .. } => {
725 assert!(matches!(field, FieldRef::TableColumn { column, .. } if column == "x"));
726 }
727 _ => panic!("Expected Compare filter"),
728 }
729 }
730
731 #[test]
732 fn test_simplify_double_not() {
733 let mut ctx = RewriteContext::default();
734
735 let filter = AstFilter::Not(Box::new(AstFilter::Not(Box::new(AstFilter::Compare {
736 field: make_field("x"),
737 op: CompareOp::Eq,
738 value: Value::Integer(5),
739 }))));
740
741 let simplified = simplify_filter(filter, &mut ctx);
742
743 match simplified {
744 AstFilter::Compare { field, .. } => {
745 assert!(matches!(field, FieldRef::TableColumn { column, .. } if column == "x"));
746 }
747 _ => panic!("Expected Compare filter"),
748 }
749 }
750}