1use crate::ast::{CompareOp, FieldRef, Filter as AstFilter, JoinQuery, Projection, QueryExpr};
14use crate::sql_lowering::{
15 effective_graph_filter, effective_join_filter, effective_table_filter, effective_vector_filter,
16};
17use reddb_types::Value;
18
19#[derive(Debug, Clone, Default)]
21pub struct RewriteContext {
22 pub property_cache: Vec<CachedProperty>,
24 pub errors: Vec<String>,
26 pub warnings: Vec<String>,
28 pub stats: RewriteStats,
30}
31
32#[derive(Debug, Clone)]
34pub struct CachedProperty {
35 pub source: String,
37 pub property: String,
39 pub cached_value: Option<String>,
41}
42
43#[derive(Debug, Clone, Default)]
45pub struct RewriteStats {
46 pub filters_simplified: u32,
48 pub predicates_pushed: u32,
50 pub properties_cached: u32,
52 pub expressions_normalized: u32,
54}
55
56pub trait RewriteRule: Send + Sync {
58 fn name(&self) -> &str;
60
61 fn apply(&self, query: QueryExpr, ctx: &mut RewriteContext) -> QueryExpr;
63
64 fn is_applicable(&self, query: &QueryExpr) -> bool;
66}
67
68pub struct QueryRewriter {
70 rules: Vec<Box<dyn RewriteRule>>,
72 max_iterations: usize,
74}
75
76impl QueryRewriter {
77 pub fn new() -> Self {
79 let rules: Vec<Box<dyn RewriteRule>> = vec![
80 Box::new(NormalizeRule),
81 Box::new(SimplifyFiltersRule),
82 Box::new(PushdownPredicatesRule),
83 Box::new(EliminateDeadCodeRule),
84 Box::new(FoldConstantsRule),
85 ];
86
87 Self {
88 rules,
89 max_iterations: 10,
90 }
91 }
92
93 pub fn add_rule(&mut self, rule: Box<dyn RewriteRule>) {
95 self.rules.push(rule);
96 }
97
98 pub fn rewrite(&self, query: QueryExpr) -> QueryExpr {
100 let mut ctx = RewriteContext::default();
101 self.rewrite_with_context(query, &mut ctx)
102 }
103
104 pub fn rewrite_with_context(
106 &self,
107 mut query: QueryExpr,
108 ctx: &mut RewriteContext,
109 ) -> QueryExpr {
110 for _iteration in 0..self.max_iterations {
112 let original = format!("{:?}", query);
113
114 for rule in &self.rules {
115 if rule.is_applicable(&query) {
116 query = rule.apply(query, ctx);
117 }
118 }
119
120 if format!("{:?}", query) == original {
122 break;
123 }
124 }
125
126 query
127 }
128}
129
130impl Default for QueryRewriter {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136struct NormalizeRule;
142
143impl RewriteRule for NormalizeRule {
144 fn name(&self) -> &str {
145 "Normalize"
146 }
147
148 fn apply(&self, query: QueryExpr, ctx: &mut RewriteContext) -> QueryExpr {
149 match query {
150 QueryExpr::Table(mut tq) => {
151 tq.columns.sort_by(|a, b| {
153 let a_name = projection_name(a);
154 let b_name = projection_name(b);
155 a_name.cmp(&b_name)
156 });
157 ctx.stats.expressions_normalized += 1;
158 QueryExpr::Table(tq)
159 }
160 QueryExpr::Graph(gq) => {
161 QueryExpr::Graph(gq)
163 }
164 QueryExpr::Join(jq) => {
165 let left = self.apply(*jq.left, ctx);
167 let right = self.apply(*jq.right, ctx);
168 QueryExpr::Join(JoinQuery {
169 left: Box::new(left),
170 right: Box::new(right),
171 ..jq
172 })
173 }
174 QueryExpr::Path(pq) => QueryExpr::Path(pq),
175 QueryExpr::Vector(vq) => {
176 QueryExpr::Vector(vq)
178 }
179 QueryExpr::Hybrid(mut hq) => {
180 hq.structured = Box::new(self.apply(*hq.structured, ctx));
182 QueryExpr::Hybrid(hq)
183 }
184 other @ (QueryExpr::Insert(_)
186 | QueryExpr::Update(_)
187 | QueryExpr::Delete(_)
188 | QueryExpr::CreateTable(_)
189 | QueryExpr::CreateCollection(_)
190 | QueryExpr::CreateVector(_)
191 | QueryExpr::DropTable(_)
192 | QueryExpr::DropGraph(_)
193 | QueryExpr::DropVector(_)
194 | QueryExpr::DropDocument(_)
195 | QueryExpr::DropKv(_)
196 | QueryExpr::DropCollection(_)
197 | QueryExpr::Truncate(_)
198 | QueryExpr::AlterTable(_)
199 | QueryExpr::CreateVcsRef(_)
200 | QueryExpr::DropVcsRef(_)
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::SetKv { .. }
213 | QueryExpr::DeleteKv { .. }
214 | QueryExpr::SetTenant(_)
215 | QueryExpr::ShowTenant
216 | QueryExpr::CreateTimeSeries(_)
217 | QueryExpr::CreateMetric(_)
218 | QueryExpr::AlterMetric(_)
219 | QueryExpr::CreateSlo(_)
220 | QueryExpr::DropTimeSeries(_)
221 | QueryExpr::CreateQueue(_)
222 | QueryExpr::AlterQueue(_)
223 | QueryExpr::DropQueue(_)
224 | QueryExpr::QueueSelect(_)
225 | QueryExpr::QueueCommand(_)
226 | QueryExpr::KvCommand(_)
227 | QueryExpr::ConfigCommand(_)
228 | QueryExpr::CreateTree(_)
229 | QueryExpr::DropTree(_)
230 | QueryExpr::TreeCommand(_)
231 | QueryExpr::ExplainAlter(_)
232 | QueryExpr::TransactionControl(_)
233 | QueryExpr::MaintenanceCommand(_)
234 | QueryExpr::VcsCommand(_)
235 | QueryExpr::CreateSchema(_)
236 | QueryExpr::DropSchema(_)
237 | QueryExpr::CreateSequence(_)
238 | QueryExpr::DropSequence(_)
239 | QueryExpr::CopyFrom(_)
240 | QueryExpr::CreateView(_)
241 | QueryExpr::DropView(_)
242 | QueryExpr::RefreshMaterializedView(_)
243 | QueryExpr::CreatePolicy(_)
244 | QueryExpr::DropPolicy(_)
245 | QueryExpr::CreateServer(_)
246 | QueryExpr::DropServer(_)
247 | QueryExpr::CreateForeignTable(_)
248 | QueryExpr::DropForeignTable(_)
249 | QueryExpr::Grant(_)
250 | QueryExpr::Revoke(_)
251 | QueryExpr::AlterUser(_)
252 | QueryExpr::CreateUser(_)
253 | QueryExpr::CreateIamPolicy { .. }
254 | QueryExpr::DropIamPolicy { .. }
255 | QueryExpr::AttachPolicy { .. }
256 | QueryExpr::DetachPolicy { .. }
257 | QueryExpr::ShowPolicies { .. }
258 | QueryExpr::ShowEffectivePermissions { .. }
259 | QueryExpr::RankOf(_)
260 | QueryExpr::ApproxRankOf(_)
261 | QueryExpr::RankRange(_)
262 | QueryExpr::SimulatePolicy { .. }
263 | QueryExpr::LintPolicy { .. }
264 | QueryExpr::MigratePolicyMode { .. }
265 | QueryExpr::CreateMigration(_)
266 | QueryExpr::ApplyMigration(_)
267 | QueryExpr::RollbackMigration(_)
268 | QueryExpr::ExplainMigration(_)
269 | QueryExpr::EventsBackfill(_)
270 | QueryExpr::EventsBackfillStatus { .. }) => other,
271 }
272 }
273
274 fn is_applicable(&self, _query: &QueryExpr) -> bool {
275 true
276 }
277}
278
279struct SimplifyFiltersRule;
281
282impl RewriteRule for SimplifyFiltersRule {
283 fn name(&self) -> &str {
284 "SimplifyFilters"
285 }
286
287 fn apply(&self, query: QueryExpr, ctx: &mut RewriteContext) -> QueryExpr {
288 match query {
289 QueryExpr::Table(mut tq) => {
290 if let Some(filter) = effective_table_filter(&tq) {
291 tq.filter = Some(simplify_filter(filter, ctx));
292 }
293 QueryExpr::Table(tq)
294 }
295 QueryExpr::Graph(mut gq) => {
296 if let Some(filter) = effective_graph_filter(&gq) {
297 gq.filter = Some(simplify_filter(filter, ctx));
298 }
299 QueryExpr::Graph(gq)
300 }
301 QueryExpr::Join(mut jq) => {
302 let join_filter = effective_join_filter(&jq);
303 let left = self.apply(*jq.left, ctx);
304 let right = self.apply(*jq.right, ctx);
305 if let Some(filter) = join_filter {
306 jq.filter = Some(simplify_filter(filter, ctx));
307 }
308 jq.left = Box::new(left);
309 jq.right = Box::new(right);
310 QueryExpr::Join(jq)
311 }
312 QueryExpr::Path(pq) => QueryExpr::Path(pq),
313 QueryExpr::Vector(vq) => {
314 QueryExpr::Vector(vq)
317 }
318 QueryExpr::Hybrid(mut hq) => {
319 hq.structured = Box::new(self.apply(*hq.structured, ctx));
321 QueryExpr::Hybrid(hq)
322 }
323 other @ (QueryExpr::Insert(_)
325 | QueryExpr::Update(_)
326 | QueryExpr::Delete(_)
327 | QueryExpr::CreateTable(_)
328 | QueryExpr::CreateCollection(_)
329 | QueryExpr::CreateVector(_)
330 | QueryExpr::DropTable(_)
331 | QueryExpr::DropGraph(_)
332 | QueryExpr::DropVector(_)
333 | QueryExpr::DropDocument(_)
334 | QueryExpr::DropKv(_)
335 | QueryExpr::DropCollection(_)
336 | QueryExpr::Truncate(_)
337 | QueryExpr::AlterTable(_)
338 | QueryExpr::CreateVcsRef(_)
339 | QueryExpr::DropVcsRef(_)
340 | QueryExpr::GraphCommand(_)
341 | QueryExpr::SearchCommand(_)
342 | QueryExpr::CreateIndex(_)
343 | QueryExpr::DropIndex(_)
344 | QueryExpr::ProbabilisticCommand(_)
345 | QueryExpr::Ask(_)
346 | QueryExpr::SetConfig { .. }
347 | QueryExpr::ShowConfig { .. }
348 | QueryExpr::SetSecret { .. }
349 | QueryExpr::DeleteSecret { .. }
350 | QueryExpr::ShowSecrets { .. }
351 | QueryExpr::SetKv { .. }
352 | QueryExpr::DeleteKv { .. }
353 | QueryExpr::SetTenant(_)
354 | QueryExpr::ShowTenant
355 | QueryExpr::CreateTimeSeries(_)
356 | QueryExpr::CreateMetric(_)
357 | QueryExpr::AlterMetric(_)
358 | QueryExpr::CreateSlo(_)
359 | QueryExpr::DropTimeSeries(_)
360 | QueryExpr::CreateQueue(_)
361 | QueryExpr::AlterQueue(_)
362 | QueryExpr::DropQueue(_)
363 | QueryExpr::QueueSelect(_)
364 | QueryExpr::QueueCommand(_)
365 | QueryExpr::KvCommand(_)
366 | QueryExpr::ConfigCommand(_)
367 | QueryExpr::CreateTree(_)
368 | QueryExpr::DropTree(_)
369 | QueryExpr::TreeCommand(_)
370 | QueryExpr::ExplainAlter(_)
371 | QueryExpr::TransactionControl(_)
372 | QueryExpr::MaintenanceCommand(_)
373 | QueryExpr::VcsCommand(_)
374 | QueryExpr::CreateSchema(_)
375 | QueryExpr::DropSchema(_)
376 | QueryExpr::CreateSequence(_)
377 | QueryExpr::DropSequence(_)
378 | QueryExpr::CopyFrom(_)
379 | QueryExpr::CreateView(_)
380 | QueryExpr::DropView(_)
381 | QueryExpr::RefreshMaterializedView(_)
382 | QueryExpr::CreatePolicy(_)
383 | QueryExpr::DropPolicy(_)
384 | QueryExpr::CreateServer(_)
385 | QueryExpr::DropServer(_)
386 | QueryExpr::CreateForeignTable(_)
387 | QueryExpr::DropForeignTable(_)
388 | QueryExpr::Grant(_)
389 | QueryExpr::Revoke(_)
390 | QueryExpr::AlterUser(_)
391 | QueryExpr::CreateUser(_)
392 | QueryExpr::CreateIamPolicy { .. }
393 | QueryExpr::DropIamPolicy { .. }
394 | QueryExpr::AttachPolicy { .. }
395 | QueryExpr::DetachPolicy { .. }
396 | QueryExpr::ShowPolicies { .. }
397 | QueryExpr::ShowEffectivePermissions { .. }
398 | QueryExpr::RankOf(_)
399 | QueryExpr::ApproxRankOf(_)
400 | QueryExpr::RankRange(_)
401 | QueryExpr::SimulatePolicy { .. }
402 | QueryExpr::LintPolicy { .. }
403 | QueryExpr::MigratePolicyMode { .. }
404 | QueryExpr::CreateMigration(_)
405 | QueryExpr::ApplyMigration(_)
406 | QueryExpr::RollbackMigration(_)
407 | QueryExpr::ExplainMigration(_)
408 | QueryExpr::EventsBackfill(_)
409 | QueryExpr::EventsBackfillStatus { .. }) => other,
410 }
411 }
412
413 fn is_applicable(&self, query: &QueryExpr) -> bool {
414 match query {
415 QueryExpr::Table(tq) => effective_table_filter(tq).is_some(),
416 QueryExpr::Graph(gq) => effective_graph_filter(gq).is_some(),
417 QueryExpr::Join(_) => true,
418 QueryExpr::Path(_) => false,
419 QueryExpr::Vector(vq) => effective_vector_filter(vq).is_some(),
420 QueryExpr::Hybrid(_) => true, QueryExpr::Insert(_)
423 | QueryExpr::Update(_)
424 | QueryExpr::Delete(_)
425 | QueryExpr::CreateTable(_)
426 | QueryExpr::CreateCollection(_)
427 | QueryExpr::CreateVector(_)
428 | QueryExpr::DropTable(_)
429 | QueryExpr::DropGraph(_)
430 | QueryExpr::DropVector(_)
431 | QueryExpr::DropDocument(_)
432 | QueryExpr::DropKv(_)
433 | QueryExpr::DropCollection(_)
434 | QueryExpr::Truncate(_)
435 | QueryExpr::AlterTable(_)
436 | QueryExpr::CreateVcsRef(_)
437 | QueryExpr::DropVcsRef(_)
438 | QueryExpr::GraphCommand(_)
439 | QueryExpr::SearchCommand(_)
440 | QueryExpr::CreateIndex(_)
441 | QueryExpr::DropIndex(_)
442 | QueryExpr::ProbabilisticCommand(_)
443 | QueryExpr::Ask(_)
444 | QueryExpr::SetConfig { .. }
445 | QueryExpr::ShowConfig { .. }
446 | QueryExpr::SetSecret { .. }
447 | QueryExpr::DeleteSecret { .. }
448 | QueryExpr::ShowSecrets { .. }
449 | QueryExpr::SetKv { .. }
450 | QueryExpr::DeleteKv { .. }
451 | QueryExpr::SetTenant(_)
452 | QueryExpr::ShowTenant
453 | QueryExpr::CreateTimeSeries(_)
454 | QueryExpr::CreateMetric(_)
455 | QueryExpr::AlterMetric(_)
456 | QueryExpr::CreateSlo(_)
457 | QueryExpr::DropTimeSeries(_)
458 | QueryExpr::CreateQueue(_)
459 | QueryExpr::AlterQueue(_)
460 | QueryExpr::DropQueue(_)
461 | QueryExpr::QueueSelect(_)
462 | QueryExpr::QueueCommand(_)
463 | QueryExpr::KvCommand(_)
464 | QueryExpr::ConfigCommand(_)
465 | QueryExpr::CreateTree(_)
466 | QueryExpr::DropTree(_)
467 | QueryExpr::TreeCommand(_)
468 | QueryExpr::ExplainAlter(_)
469 | QueryExpr::TransactionControl(_)
470 | QueryExpr::MaintenanceCommand(_)
471 | QueryExpr::VcsCommand(_)
472 | QueryExpr::CreateSchema(_)
473 | QueryExpr::DropSchema(_)
474 | QueryExpr::CreateSequence(_)
475 | QueryExpr::DropSequence(_)
476 | QueryExpr::CopyFrom(_)
477 | QueryExpr::CreateView(_)
478 | QueryExpr::DropView(_)
479 | QueryExpr::RefreshMaterializedView(_)
480 | QueryExpr::CreatePolicy(_)
481 | QueryExpr::DropPolicy(_)
482 | QueryExpr::CreateServer(_)
483 | QueryExpr::DropServer(_)
484 | QueryExpr::CreateForeignTable(_)
485 | QueryExpr::DropForeignTable(_)
486 | QueryExpr::Grant(_)
487 | QueryExpr::Revoke(_)
488 | QueryExpr::AlterUser(_)
489 | QueryExpr::CreateUser(_)
490 | QueryExpr::CreateIamPolicy { .. }
491 | QueryExpr::DropIamPolicy { .. }
492 | QueryExpr::AttachPolicy { .. }
493 | QueryExpr::DetachPolicy { .. }
494 | QueryExpr::ShowPolicies { .. }
495 | QueryExpr::ShowEffectivePermissions { .. }
496 | QueryExpr::RankOf(_)
497 | QueryExpr::ApproxRankOf(_)
498 | QueryExpr::RankRange(_)
499 | QueryExpr::SimulatePolicy { .. }
500 | QueryExpr::LintPolicy { .. }
501 | QueryExpr::MigratePolicyMode { .. }
502 | QueryExpr::CreateMigration(_)
503 | QueryExpr::ApplyMigration(_)
504 | QueryExpr::RollbackMigration(_)
505 | QueryExpr::ExplainMigration(_)
506 | QueryExpr::EventsBackfill(_)
507 | QueryExpr::EventsBackfillStatus { .. } => false,
508 }
509 }
510}
511
512struct PushdownPredicatesRule;
514
515impl RewriteRule for PushdownPredicatesRule {
516 fn name(&self) -> &str {
517 "PushdownPredicates"
518 }
519
520 fn apply(&self, query: QueryExpr, ctx: &mut RewriteContext) -> QueryExpr {
521 match query {
522 QueryExpr::Join(mut jq) => {
523 jq.left = Box::new(self.apply(*jq.left, ctx));
529 jq.right = Box::new(self.apply(*jq.right, ctx));
530 ctx.stats.predicates_pushed += 1;
531 QueryExpr::Join(jq)
532 }
533 other => other,
534 }
535 }
536
537 fn is_applicable(&self, query: &QueryExpr) -> bool {
538 matches!(query, QueryExpr::Join(_))
539 }
540}
541
542struct EliminateDeadCodeRule;
544
545impl RewriteRule for EliminateDeadCodeRule {
546 fn name(&self) -> &str {
547 "EliminateDeadCode"
548 }
549
550 fn apply(&self, query: QueryExpr, _ctx: &mut RewriteContext) -> QueryExpr {
551 match query {
552 QueryExpr::Table(mut tq) => {
553 if let Some(filter) = effective_table_filter(&tq).as_ref() {
555 if is_always_true(filter) {
556 tq.filter = None;
557 }
558 }
559 QueryExpr::Table(tq)
560 }
561 other => other,
562 }
563 }
564
565 fn is_applicable(&self, query: &QueryExpr) -> bool {
566 matches!(query, QueryExpr::Table(_))
567 }
568}
569
570struct FoldConstantsRule;
572
573impl RewriteRule for FoldConstantsRule {
574 fn name(&self) -> &str {
575 "FoldConstants"
576 }
577
578 fn apply(&self, query: QueryExpr, _ctx: &mut RewriteContext) -> QueryExpr {
579 query
582 }
583
584 fn is_applicable(&self, _query: &QueryExpr) -> bool {
585 true
586 }
587}
588
589fn projection_name(proj: &Projection) -> String {
594 match proj {
595 Projection::All => "*".to_string(),
596 Projection::Column(name) => name.clone(),
597 Projection::Alias(_, alias) => alias.clone(),
598 Projection::Function(name, _) => name
599 .split_once(':')
600 .map(|(_, alias)| alias.to_string())
601 .unwrap_or_else(|| name.clone()),
602 Projection::Expression(expr, alias) => {
603 alias.clone().unwrap_or_else(|| format!("{:?}", expr))
604 }
605 Projection::Field(field, alias) => alias.clone().unwrap_or_else(|| format!("{:?}", field)),
606 Projection::Window { name, alias, .. } => alias.clone().unwrap_or_else(|| name.clone()),
607 }
608}
609
610fn simplify_filter(filter: AstFilter, ctx: &mut RewriteContext) -> AstFilter {
611 match filter {
612 AstFilter::And(left, right) => {
613 let left = simplify_filter(*left, ctx);
614 let right = simplify_filter(*right, ctx);
615
616 if is_always_true(&left) {
618 ctx.stats.filters_simplified += 1;
619 return right;
620 }
621 if is_always_true(&right) {
622 ctx.stats.filters_simplified += 1;
623 return left;
624 }
625
626 if is_always_false(&left) || is_always_false(&right) {
628 ctx.stats.filters_simplified += 1;
629 return AstFilter::Compare {
630 field: FieldRef::TableColumn {
631 table: String::new(),
632 column: "1".to_string(),
633 },
634 op: CompareOp::Eq,
635 value: Value::Integer(0),
636 };
637 }
638
639 AstFilter::And(Box::new(left), Box::new(right))
640 }
641 AstFilter::Or(left, right) => {
642 let left = simplify_filter(*left, ctx);
643 let right = simplify_filter(*right, ctx);
644
645 if is_always_false(&left) {
647 ctx.stats.filters_simplified += 1;
648 return right;
649 }
650 if is_always_false(&right) {
651 ctx.stats.filters_simplified += 1;
652 return left;
653 }
654
655 if is_always_true(&left) || is_always_true(&right) {
657 ctx.stats.filters_simplified += 1;
658 return AstFilter::Compare {
659 field: FieldRef::TableColumn {
660 table: String::new(),
661 column: "1".to_string(),
662 },
663 op: CompareOp::Eq,
664 value: Value::Integer(1),
665 };
666 }
667
668 AstFilter::Or(Box::new(left), Box::new(right))
669 }
670 AstFilter::Not(inner) => {
671 let inner = simplify_filter(*inner, ctx);
672
673 if let AstFilter::Not(double_inner) = inner {
675 ctx.stats.filters_simplified += 1;
676 return *double_inner;
677 }
678
679 AstFilter::Not(Box::new(inner))
680 }
681 other => other,
682 }
683}
684
685fn is_always_true(filter: &AstFilter) -> bool {
686 match filter {
687 AstFilter::Compare { field, op, value } => {
688 matches!(field, FieldRef::TableColumn { column, .. } if column == "1")
690 && matches!(op, CompareOp::Eq)
691 && matches!(value, Value::Integer(1))
692 }
693 _ => false,
694 }
695}
696
697fn is_always_false(filter: &AstFilter) -> bool {
698 match filter {
699 AstFilter::Compare { field, op, value } => {
700 matches!(field, FieldRef::TableColumn { column, .. } if column == "1")
702 && matches!(op, CompareOp::Eq)
703 && matches!(value, Value::Integer(0))
704 }
705 _ => false,
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use crate::ast::{JoinCondition, TableQuery, WindowSpec};
713
714 fn make_field(name: &str) -> FieldRef {
715 FieldRef::TableColumn {
716 table: String::new(),
717 column: name.to_string(),
718 }
719 }
720
721 #[test]
722 fn test_simplify_and_with_true() {
723 let mut ctx = RewriteContext::default();
724
725 let filter = AstFilter::And(
726 Box::new(AstFilter::Compare {
727 field: make_field("1"),
728 op: CompareOp::Eq,
729 value: Value::Integer(1),
730 }),
731 Box::new(AstFilter::Compare {
732 field: make_field("x"),
733 op: CompareOp::Eq,
734 value: Value::Integer(5),
735 }),
736 );
737
738 let simplified = simplify_filter(filter, &mut ctx);
739
740 match simplified {
741 AstFilter::Compare { field, .. } => {
742 assert!(matches!(field, FieldRef::TableColumn { column, .. } if column == "x"));
743 }
744 _ => panic!("Expected Compare filter"),
745 }
746 }
747
748 #[test]
749 fn test_simplify_double_not() {
750 let mut ctx = RewriteContext::default();
751
752 let filter = AstFilter::Not(Box::new(AstFilter::Not(Box::new(AstFilter::Compare {
753 field: make_field("x"),
754 op: CompareOp::Eq,
755 value: Value::Integer(5),
756 }))));
757
758 let simplified = simplify_filter(filter, &mut ctx);
759
760 match simplified {
761 AstFilter::Compare { field, .. } => {
762 assert!(matches!(field, FieldRef::TableColumn { column, .. } if column == "x"));
763 }
764 _ => panic!("Expected Compare filter"),
765 }
766 }
767
768 #[test]
769 fn projection_name_uses_visible_output_name_for_all_projection_shapes() {
770 assert_eq!(projection_name(&Projection::All), "*");
771 assert_eq!(
772 projection_name(&Projection::Column("raw".to_string())),
773 "raw"
774 );
775 assert_eq!(
776 projection_name(&Projection::Alias("raw".to_string(), "alias".to_string())),
777 "alias"
778 );
779 assert_eq!(
780 projection_name(&Projection::Function(
781 "LOWER:display".to_string(),
782 Vec::new()
783 )),
784 "display"
785 );
786 assert_eq!(
787 projection_name(&Projection::Expression(
788 Box::new(AstFilter::Compare {
789 field: make_field("x"),
790 op: CompareOp::Eq,
791 value: Value::Integer(1),
792 }),
793 Some("expr_alias".to_string()),
794 )),
795 "expr_alias"
796 );
797 assert_eq!(
798 projection_name(&Projection::Field(
799 FieldRef::node_prop("n", "name"),
800 Some("node_name".to_string()),
801 )),
802 "node_name"
803 );
804 assert_eq!(
805 projection_name(&Projection::Window {
806 name: "ROW_NUMBER".to_string(),
807 args: Vec::new(),
808 window: Box::new(WindowSpec::default()),
809 alias: Some("rn".to_string()),
810 }),
811 "rn"
812 );
813 }
814
815 #[test]
816 fn normalize_rule_sorts_table_columns_by_output_name() {
817 let mut table = TableQuery::new("users");
818 table.columns = vec![
819 Projection::Column("z".to_string()),
820 Projection::Function("LOWER:a_alias".to_string(), Vec::new()),
821 Projection::Alias("name".to_string(), "m".to_string()),
822 ];
823
824 let mut ctx = RewriteContext::default();
825 let normalized = NormalizeRule.apply(QueryExpr::Table(table), &mut ctx);
826 let QueryExpr::Table(table) = normalized else {
827 panic!("expected table query");
828 };
829
830 assert_eq!(ctx.stats.expressions_normalized, 1);
831 assert_eq!(
832 table
833 .columns
834 .iter()
835 .map(projection_name)
836 .collect::<Vec<_>>(),
837 vec!["a_alias", "m", "z"]
838 );
839 }
840
841 #[test]
842 fn simplify_filter_covers_or_true_false_and_not_paths() {
843 let mut ctx = RewriteContext::default();
844 let truth = AstFilter::Compare {
845 field: make_field("1"),
846 op: CompareOp::Eq,
847 value: Value::Integer(1),
848 };
849 let falsehood = AstFilter::Compare {
850 field: make_field("1"),
851 op: CompareOp::Eq,
852 value: Value::Integer(0),
853 };
854 let predicate = AstFilter::Compare {
855 field: make_field("x"),
856 op: CompareOp::Eq,
857 value: Value::Integer(5),
858 };
859
860 assert_eq!(
861 simplify_filter(
862 AstFilter::Or(Box::new(falsehood.clone()), Box::new(predicate.clone())),
863 &mut ctx,
864 ),
865 predicate
866 );
867 assert!(is_always_true(&simplify_filter(
868 AstFilter::Or(Box::new(truth), Box::new(falsehood.clone())),
869 &mut ctx,
870 )));
871 assert!(is_always_false(&simplify_filter(
872 AstFilter::And(
873 Box::new(falsehood),
874 Box::new(AstFilter::IsNotNull(make_field("x")))
875 ),
876 &mut ctx,
877 )));
878 assert!(ctx.stats.filters_simplified >= 3);
879 }
880
881 #[test]
882 fn query_rewriter_runs_rules_until_fixed_point_and_exposes_context() {
883 let mut table = TableQuery::new("users");
884 table.filter = Some(AstFilter::And(
885 Box::new(AstFilter::Compare {
886 field: make_field("1"),
887 op: CompareOp::Eq,
888 value: Value::Integer(1),
889 }),
890 Box::new(AstFilter::Compare {
891 field: make_field("age"),
892 op: CompareOp::Ge,
893 value: Value::Integer(18),
894 }),
895 ));
896
897 let mut ctx = RewriteContext::default();
898 let rewritten =
899 QueryRewriter::default().rewrite_with_context(QueryExpr::Table(table), &mut ctx);
900 let QueryExpr::Table(table) = rewritten else {
901 panic!("expected table query");
902 };
903
904 assert!(matches!(
905 table.filter,
906 Some(AstFilter::Compare {
907 field: FieldRef::TableColumn { column, .. },
908 op: CompareOp::Ge,
909 value: Value::Integer(18),
910 }) if column == "age"
911 ));
912 assert!(ctx.stats.filters_simplified >= 1);
913 }
914
915 #[test]
916 fn query_rewriter_recurses_into_join_children_and_tracks_pushdown() {
917 let mut left = TableQuery::new("users");
918 left.columns = vec![
919 Projection::Column("z".to_string()),
920 Projection::Column("a".to_string()),
921 ];
922 left.filter = Some(AstFilter::And(
923 Box::new(AstFilter::Compare {
924 field: make_field("1"),
925 op: CompareOp::Eq,
926 value: Value::Integer(1),
927 }),
928 Box::new(AstFilter::Compare {
929 field: make_field("age"),
930 op: CompareOp::Ge,
931 value: Value::Integer(18),
932 }),
933 ));
934
935 let join = JoinQuery::new(
936 QueryExpr::Table(left),
937 QueryExpr::Table(TableQuery::new("orders")),
938 JoinCondition::new(make_field("id"), make_field("user_id")),
939 );
940
941 let mut ctx = RewriteContext::default();
942 let rewritten =
943 QueryRewriter::default().rewrite_with_context(QueryExpr::Join(join), &mut ctx);
944 let QueryExpr::Join(join) = rewritten else {
945 panic!("expected join query");
946 };
947 let QueryExpr::Table(left) = join.left.as_ref() else {
948 panic!("expected table on left side");
949 };
950
951 assert_eq!(
952 left.columns.iter().map(projection_name).collect::<Vec<_>>(),
953 vec!["a", "z"]
954 );
955 assert!(matches!(
956 &left.filter,
957 Some(AstFilter::Compare {
958 field: FieldRef::TableColumn { ref column, .. },
959 op: CompareOp::Ge,
960 value: Value::Integer(18),
961 }) if column == "age"
962 ));
963 assert!(ctx.stats.predicates_pushed >= 1);
964 }
965
966 #[test]
967 fn query_rewriter_eliminates_always_true_table_filters() {
968 let mut table = TableQuery::new("users");
969 table.filter = Some(AstFilter::Compare {
970 field: make_field("1"),
971 op: CompareOp::Eq,
972 value: Value::Integer(1),
973 });
974
975 let rewritten = QueryRewriter::default().rewrite(QueryExpr::Table(table));
976 let QueryExpr::Table(table) = rewritten else {
977 panic!("expected table query");
978 };
979
980 assert!(table.filter.is_none());
981 }
982
983 struct CountingRule;
984
985 impl RewriteRule for CountingRule {
986 fn name(&self) -> &str {
987 "CountingRule"
988 }
989
990 fn apply(&self, query: QueryExpr, ctx: &mut RewriteContext) -> QueryExpr {
991 ctx.warnings.push(self.name().to_string());
992 query
993 }
994
995 fn is_applicable(&self, query: &QueryExpr) -> bool {
996 matches!(query, QueryExpr::Table(_))
997 }
998 }
999
1000 #[test]
1001 fn custom_rules_can_be_added_after_defaults() {
1002 let mut rewriter = QueryRewriter::new();
1003 rewriter.add_rule(Box::new(CountingRule));
1004
1005 let mut ctx = RewriteContext::default();
1006 let rewritten =
1007 rewriter.rewrite_with_context(QueryExpr::Table(TableQuery::new("users")), &mut ctx);
1008
1009 assert!(matches!(rewritten, QueryExpr::Table(_)));
1010 assert!(ctx.warnings.iter().any(|warning| warning == "CountingRule"));
1011 }
1012}