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