1use std::cmp::Reverse;
21use std::collections::HashMap;
22
23use risingwave_sqlparser::ast::*;
24
25use crate::sqlreduce::path::{AstField, AstNode, AstPath, PathComponent};
26
27#[derive(Debug, Clone, Default)]
29pub struct ReductionRule {
30 pub try_null: bool,
32 pub descend: Vec<AstField>,
34 pub remove: Vec<AstField>,
36 pub pullup: Vec<AstField>,
38 pub replace: Vec<AstField>,
40}
41
42pub struct ReductionRules {
45 rules: HashMap<String, ReductionRule>,
46}
47
48impl Default for ReductionRules {
49 fn default() -> Self {
50 let mut rules = HashMap::new();
51
52 rules.insert(
60 "Select".to_owned(),
61 ReductionRule {
62 try_null: false,
63 descend: vec![
64 AstField::Projection,
65 AstField::From,
66 AstField::Selection,
67 AstField::GroupBy,
68 AstField::Having,
69 ],
70 remove: vec![
71 AstField::Selection, AstField::Having, AstField::Projection, AstField::From, AstField::GroupBy, ],
77 pullup: vec![],
78 replace: vec![],
79 },
80 );
81
82 rules.insert(
84 "Query".to_owned(),
85 ReductionRule {
86 try_null: false,
87 descend: vec![AstField::Body, AstField::With, AstField::OrderBy],
88 remove: vec![AstField::With, AstField::OrderBy],
89 pullup: vec![],
90 replace: vec![AstField::Body],
91 },
92 );
93
94 rules.insert(
96 "With".to_owned(),
97 ReductionRule {
98 try_null: true,
99 descend: vec![AstField::CteTable],
100 remove: vec![],
101 pullup: vec![],
102 replace: vec![],
103 },
104 );
105
106 rules.insert(
108 "CteList".to_owned(),
109 ReductionRule {
110 try_null: false,
111 descend: vec![], remove: vec![],
113 pullup: vec![],
114 replace: vec![],
115 },
116 );
117
118 rules.insert(
120 "Cte".to_owned(),
121 ReductionRule {
122 try_null: false,
123 descend: vec![AstField::CteInner],
124 remove: vec![],
125 pullup: vec![AstField::CteInner],
126 replace: vec![],
127 },
128 );
129
130 rules.insert(
132 "BinaryOp".to_owned(),
133 ReductionRule {
134 try_null: true,
135 descend: vec![],
136 remove: vec![],
137 pullup: vec![AstField::Left, AstField::Right],
138 replace: vec![],
139 },
140 );
141
142 rules.insert(
143 "Case".to_owned(),
144 ReductionRule {
145 try_null: true,
146 descend: vec![],
147 remove: vec![],
148 pullup: vec![AstField::Operand, AstField::ElseResult],
149 replace: vec![],
150 },
151 );
152
153 rules.insert(
155 "Function".to_owned(),
156 ReductionRule {
157 try_null: true,
158 descend: vec![], remove: vec![],
160 pullup: vec![],
161 replace: vec![],
162 },
163 );
164
165 rules.insert(
167 "Subquery".to_owned(),
168 ReductionRule {
169 try_null: true,
170 descend: vec![],
171 remove: vec![],
172 pullup: vec![],
173 replace: vec![AstField::Subquery],
174 },
175 );
176
177 rules.insert(
179 "Value".to_owned(),
180 ReductionRule {
181 try_null: true,
182 descend: vec![],
183 remove: vec![],
184 pullup: vec![],
185 replace: vec![],
186 },
187 );
188
189 rules.insert(
191 "SelectItemList".to_owned(),
192 ReductionRule {
193 try_null: false,
194 descend: vec![],
195 remove: vec![],
196 pullup: vec![],
197 replace: vec![],
198 },
199 );
200
201 rules.insert(
202 "ExprList".to_owned(),
203 ReductionRule {
204 try_null: true,
205 descend: vec![],
206 remove: vec![],
207 pullup: vec![],
208 replace: vec![],
209 },
210 );
211
212 rules.insert(
213 "TableList".to_owned(),
214 ReductionRule {
215 try_null: false,
216 descend: vec![],
217 remove: vec![],
218 pullup: vec![],
219 replace: vec![],
220 },
221 );
222
223 rules.insert(
224 "OrderByList".to_owned(),
225 ReductionRule {
226 try_null: false,
227 descend: vec![],
228 remove: vec![],
229 pullup: vec![],
230 replace: vec![],
231 },
232 );
233
234 rules.insert(
236 "TableWithJoins".to_owned(),
237 ReductionRule {
238 try_null: false,
239 descend: vec![AstField::Relation, AstField::Joins],
240 remove: vec![AstField::Joins],
241 pullup: vec![AstField::Relation],
242 replace: vec![AstField::Relation],
243 },
244 );
245
246 rules.insert(
247 "Join".to_owned(),
248 ReductionRule {
249 try_null: true,
250 descend: vec![AstField::Relation],
251 remove: vec![],
252 pullup: vec![AstField::Relation],
253 replace: vec![AstField::Relation],
254 },
255 );
256
257 rules.insert(
258 "TableFactor".to_owned(),
259 ReductionRule {
260 try_null: true, descend: vec![AstField::Subquery],
262 remove: vec![AstField::Subquery], pullup: vec![AstField::Subquery],
264 replace: vec![AstField::Subquery],
265 },
266 );
267
268 rules.insert(
269 "JoinList".to_owned(),
270 ReductionRule {
271 try_null: true, descend: vec![],
273 remove: vec![], pullup: vec![],
275 replace: vec![],
276 },
277 );
278
279 Self { rules }
280 }
281}
282
283impl ReductionRules {
284 pub fn get_rule(&self, node_type: &str) -> Option<&ReductionRule> {
286 self.rules.get(node_type)
287 }
288
289 pub fn get_node_type(node: &AstNode) -> String {
291 match node {
292 AstNode::Statement(_) => "Statement".to_owned(),
293 AstNode::Query(_) => "Query".to_owned(),
294 AstNode::Select(_) => "Select".to_owned(),
295 AstNode::Expr(expr) => match expr {
296 Expr::BinaryOp { .. } => "BinaryOp".to_owned(),
297 Expr::Case { .. } => "Case".to_owned(),
298 Expr::Function { .. } => "Function".to_owned(),
299 Expr::Subquery(_) => "Subquery".to_owned(),
300 Expr::Value(_) => "Value".to_owned(),
301 _ => "Expr".to_owned(),
302 },
303 AstNode::SelectItem(_) => "SelectItem".to_owned(),
304 AstNode::TableWithJoins(_) => "TableWithJoins".to_owned(),
305 AstNode::Join(_) => "Join".to_owned(),
306 AstNode::TableFactor(_) => "TableFactor".to_owned(),
307 AstNode::OrderByExpr(_) => "OrderByExpr".to_owned(),
308 AstNode::With(_) => "With".to_owned(),
309 AstNode::Cte(_) => "Cte".to_owned(),
310 AstNode::ExprList(_) => "ExprList".to_owned(),
311 AstNode::SelectItemList(_) => "SelectItemList".to_owned(),
312 AstNode::TableList(_) => "TableList".to_owned(),
313 AstNode::JoinList(_) => "JoinList".to_owned(),
314 AstNode::OrderByList(_) => "OrderByList".to_owned(),
315 AstNode::CteList(_) => "CteList".to_owned(),
316 AstNode::Option(_) => "Option".to_owned(),
317 }
318 }
319}
320
321#[derive(Debug, Clone)]
323pub enum ReductionOperation {
324 TryNull,
326 Remove(AstField),
328 Pullup(AstField),
330 Replace(AstField),
332 RemoveListElement(usize),
334}
335
336impl ReductionOperation {
337 pub fn priority(&self) -> i32 {
341 match self {
342 ReductionOperation::RemoveListElement(_) => 100,
345
346 ReductionOperation::Remove(field) => {
349 use AstField::*;
350 match field {
351 Selection => 95,
354 OrderBy => 94,
356 Limit | Offset => 93,
358 With => 92,
360
361 Having => 85,
364
365 GroupBy => 70,
369 Projection => 65,
370
371 From => 60,
373
374 _ => 80,
376 }
377 }
378
379 ReductionOperation::Replace(_) => 55,
382
383 ReductionOperation::Pullup(_) => 40,
386
387 ReductionOperation::TryNull => 20,
390 }
391 }
392}
393
394#[derive(Debug, Clone)]
396pub struct ReductionCandidate {
397 pub path: AstPath,
398 pub operation: ReductionOperation,
399}
400
401pub fn generate_reduction_candidates(
404 root: &AstNode,
405 rules: &ReductionRules,
406 paths: &[AstPath],
407) -> Vec<ReductionCandidate> {
408 let mut candidates = Vec::new();
409
410 tracing::debug!("Generating reduction candidates for {} paths", paths.len());
411
412 for (path_idx, path) in paths.iter().enumerate() {
413 if let Some(node) = crate::sqlreduce::path::get_node_at_path(root, path) {
414 let node_type = ReductionRules::get_node_type(&node);
415 let path_str = crate::sqlreduce::path::display_ast_path(path);
416
417 tracing::debug!("Path {}: {} ({})", path_idx, path_str, node_type);
418
419 match &node {
423 AstNode::SelectItemList(items) if items.len() > 1 => {
424 tracing::debug!(
425 "Adding {} RemoveListElement candidates for SelectItemList (reverse order)",
426 items.len()
427 );
428 for i in (0..items.len()).rev() {
429 candidates.push(ReductionCandidate {
430 path: path.clone(),
431 operation: ReductionOperation::RemoveListElement(i),
432 });
433 }
434 }
435 AstNode::ExprList(exprs) if exprs.len() > 1 => {
436 tracing::debug!(
437 "Adding {} RemoveListElement candidates for ExprList (reverse order)",
438 exprs.len()
439 );
440 for i in (0..exprs.len()).rev() {
441 candidates.push(ReductionCandidate {
442 path: path.clone(),
443 operation: ReductionOperation::RemoveListElement(i),
444 });
445 }
446 }
447 AstNode::TableList(tables) if tables.len() > 1 => {
448 tracing::debug!(
449 "Adding {} RemoveListElement candidates for TableList (reverse order)",
450 tables.len()
451 );
452 for i in (0..tables.len()).rev() {
453 candidates.push(ReductionCandidate {
454 path: path.clone(),
455 operation: ReductionOperation::RemoveListElement(i),
456 });
457 }
458 }
459 AstNode::OrderByList(orders) if orders.len() > 1 => {
460 tracing::debug!(
461 "Adding {} RemoveListElement candidates for OrderByList (reverse order)",
462 orders.len()
463 );
464 for i in (0..orders.len()).rev() {
465 candidates.push(ReductionCandidate {
466 path: path.clone(),
467 operation: ReductionOperation::RemoveListElement(i),
468 });
469 }
470 }
471 _ => {}
472 }
473
474 if let Some(rule) = rules.get_rule(&node_type) {
476 let mut rule_candidates = 0;
477
478 if rule.try_null {
480 candidates.push(ReductionCandidate {
481 path: path.clone(),
482 operation: ReductionOperation::TryNull,
483 });
484 rule_candidates += 1;
485 }
486
487 for attr in &rule.remove {
489 candidates.push(ReductionCandidate {
490 path: path.clone(),
491 operation: ReductionOperation::Remove(attr.clone()),
492 });
493 rule_candidates += 1;
494 }
495
496 for attr in &rule.pullup {
498 candidates.push(ReductionCandidate {
499 path: path.clone(),
500 operation: ReductionOperation::Pullup(attr.clone()),
501 });
502 rule_candidates += 1;
503 }
504
505 for attr in &rule.replace {
507 candidates.push(ReductionCandidate {
508 path: path.clone(),
509 operation: ReductionOperation::Replace(attr.clone()),
510 });
511 rule_candidates += 1;
512 }
513
514 if rule_candidates > 0 {
515 tracing::debug!(
516 "Added {} rule-based candidates for {}",
517 rule_candidates,
518 node_type
519 );
520 }
521 } else {
522 tracing::debug!("No rules found for node type: {}", node_type);
523 }
524 }
525 }
526
527 tracing::debug!(
528 "Generated {} total candidates from {} paths (before sorting)",
529 candidates.len(),
530 paths.len()
531 );
532
533 candidates.sort_by_key(|c| Reverse(c.operation.priority()));
536
537 tracing::debug!(
538 "Sorted candidates by dependency-aware priority - first 10: {:?}",
539 candidates
540 .iter()
541 .take(10)
542 .map(|c| {
543 let op_desc = match &c.operation {
544 ReductionOperation::Remove(field) => format!("Remove({:?})", field),
545 op => format!("{:?}", op),
546 };
547 (c.operation.priority(), op_desc)
548 })
549 .collect::<Vec<_>>()
550 );
551
552 candidates
553}
554
555pub fn apply_reduction_operation(
558 root: &AstNode,
559 candidate: &ReductionCandidate,
560) -> Option<AstNode> {
561 use crate::sqlreduce::path::{display_ast_path, get_node_at_path, set_node_at_path};
562
563 tracing::debug!(
564 "apply_reduction_operation: Trying to apply {:?} at path {}",
565 candidate.operation,
566 display_ast_path(&candidate.path)
567 );
568
569 let target_node = get_node_at_path(root, &candidate.path);
570 if target_node.is_none() {
571 tracing::debug!(
572 "apply_reduction_operation: Failed to get node at path {}",
573 display_ast_path(&candidate.path)
574 );
575 return None;
576 }
577 let target_node = target_node?;
578
579 match &candidate.operation {
580 ReductionOperation::TryNull => {
581 let null_expr = AstNode::Expr(Expr::Value(Value::Null));
583 set_node_at_path(root, &candidate.path, Some(null_expr))
584 }
585
586 ReductionOperation::Remove(field) => {
587 let attr_path = [
589 candidate.path.clone(),
590 vec![PathComponent::field(field.clone())],
591 ]
592 .concat();
593 tracing::debug!(
594 "apply_reduction_operation: Removing attribute '{}' at path {}",
595 field.to_string(),
596 display_ast_path(&attr_path)
597 );
598
599 let result = set_node_at_path(root, &attr_path, None);
600 if result.is_none() {
601 tracing::debug!(
602 "apply_reduction_operation: Failed to remove attribute '{}'",
603 field.to_string()
604 );
605 } else {
606 tracing::debug!(
607 "apply_reduction_operation: Successfully removed attribute '{}'",
608 field.to_string()
609 );
610 }
611 result
612 }
613
614 ReductionOperation::Pullup(field) => {
615 let attr_path = [
617 candidate.path.clone(),
618 vec![PathComponent::field(field.clone())],
619 ]
620 .concat();
621 if let Some(subnode) = get_node_at_path(root, &attr_path) {
622 set_node_at_path(root, &candidate.path, Some(subnode))
623 } else {
624 None
625 }
626 }
627
628 ReductionOperation::Replace(field) => {
629 let attr_path = [
631 candidate.path.clone(),
632 vec![PathComponent::field(field.clone())],
633 ]
634 .concat();
635 tracing::debug!(
636 "apply_reduction_operation: Replacing with attribute '{}' from path {}",
637 field.to_string(),
638 display_ast_path(&attr_path)
639 );
640
641 if let Some(subtree) = get_node_at_path(root, &attr_path) {
642 tracing::debug!("apply_reduction_operation: Found subtree for replacement");
643 let result = set_node_at_path(root, &candidate.path, Some(subtree));
644 if result.is_none() {
645 tracing::debug!("apply_reduction_operation: Failed to set replacement subtree");
646 } else {
647 tracing::debug!(
648 "apply_reduction_operation: Successfully replaced with subtree"
649 );
650 }
651 result
652 } else {
653 tracing::debug!(
654 "apply_reduction_operation: No subtree found at path {}",
655 display_ast_path(&attr_path)
656 );
657 None
658 }
659 }
660
661 ReductionOperation::RemoveListElement(index) => {
662 match target_node {
664 AstNode::SelectItemList(mut items) => {
665 if *index < items.len() && items.len() > 1 {
666 items.remove(*index);
667 set_node_at_path(
668 root,
669 &candidate.path,
670 Some(AstNode::SelectItemList(items)),
671 )
672 } else {
673 None
674 }
675 }
676 AstNode::ExprList(mut exprs) => {
677 if *index < exprs.len() && exprs.len() > 1 {
678 exprs.remove(*index);
679 set_node_at_path(root, &candidate.path, Some(AstNode::ExprList(exprs)))
680 } else {
681 None
682 }
683 }
684 AstNode::TableList(mut tables) => {
685 if *index < tables.len() && tables.len() > 1 {
686 tables.remove(*index);
687 set_node_at_path(root, &candidate.path, Some(AstNode::TableList(tables)))
688 } else {
689 None
690 }
691 }
692 AstNode::OrderByList(mut orders) => {
693 if *index < orders.len() && orders.len() > 1 {
694 orders.remove(*index);
695 set_node_at_path(root, &candidate.path, Some(AstNode::OrderByList(orders)))
696 } else {
697 None
698 }
699 }
700 _ => None,
701 }
702 }
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use risingwave_sqlparser::parser::Parser;
709
710 use super::*;
711 use crate::sqlreduce::path::{enumerate_reduction_paths, statement_to_ast_node};
712
713 #[test]
714 fn test_reduction_candidates() {
715 let sql = "SELECT a, b, c FROM t1, t2;";
716 let parsed = Parser::parse_sql(sql).expect("Failed to parse SQL");
717 let stmt = &parsed[0];
718 let ast_node = statement_to_ast_node(stmt);
719
720 let paths = enumerate_reduction_paths(&ast_node, vec![]);
721 let rules = ReductionRules::default();
722 let candidates = generate_reduction_candidates(&ast_node, &rules, &paths);
723
724 assert!(!candidates.is_empty());
726 println!(
727 "Generated {} candidates for complex query",
728 candidates.len()
729 );
730 }
731
732 #[test]
733 fn test_list_element_removal() {
734 let sql = "SELECT a, b, c FROM t;";
735 let parsed = Parser::parse_sql(sql).expect("Failed to parse SQL");
736 let stmt = &parsed[0];
737 let ast_node = statement_to_ast_node(stmt);
738
739 let paths = enumerate_reduction_paths(&ast_node, vec![]);
740 let rules = ReductionRules::default();
741 let candidates = generate_reduction_candidates(&ast_node, &rules, &paths);
742
743 let list_removals: Vec<_> = candidates
745 .iter()
746 .filter(|c| matches!(c.operation, ReductionOperation::RemoveListElement(_)))
747 .collect();
748
749 assert!(list_removals.len() == 3);
751 println!(
752 "✓ Found {} list element removal candidates as expected",
753 list_removals.len()
754 );
755 }
756}