1use super::{Tool, Result, ToolError, common_options, parse_output_format, OutputFormat};
2use clap::{Arg, ArgMatches, Command};
3use colored::*;
4use std::path::Path;
5use std::fs;
6use std::collections::HashMap;
7use regex::Regex;
8use syn::{parse_file, Item, ItemMacro, Lit};
9#[derive(Debug, Clone)]
10pub struct SqlMacroCheckTool;
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12struct SqlAnalysisReport {
13 files_analyzed: usize,
14 sql_queries_found: usize,
15 macros_analyzed: Vec<SqlMacroAnalysis>,
16 security_issues: Vec<SecurityIssue>,
17 performance_issues: Vec<PerformanceIssue>,
18 syntax_errors: Vec<SyntaxError>,
19 suggestions: Vec<String>,
20 timestamp: String,
21}
22#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23struct SqlMacroAnalysis {
24 file_path: String,
25 macro_name: String,
26 sql_query: String,
27 line_number: usize,
28 parameters: Vec<String>,
29 security_score: u8,
30 performance_score: u8,
31 issues: Vec<String>,
32}
33#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
34struct SecurityIssue {
35 file_path: String,
36 line_number: usize,
37 issue_type: String,
38 description: String,
39 severity: String,
40 sql_snippet: String,
41 suggestion: String,
42}
43#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
44struct PerformanceIssue {
45 file_path: String,
46 line_number: usize,
47 issue_type: String,
48 description: String,
49 sql_snippet: String,
50 suggestion: String,
51}
52#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
53struct SyntaxError {
54 file_path: String,
55 line_number: usize,
56 error_type: String,
57 description: String,
58 sql_snippet: String,
59}
60impl SqlMacroCheckTool {
61 pub fn new() -> Self {
62 Self
63 }
64 fn find_rust_files(&self, directory: &str) -> Result<Vec<String>> {
65 let mut files = Vec::new();
66 self.find_rust_files_recursive(directory, &mut files)?;
67 Ok(files)
68 }
69 fn find_rust_files_recursive(
70 &self,
71 dir: &str,
72 files: &mut Vec<String>,
73 ) -> Result<()> {
74 let path = Path::new(dir);
75 if !path.exists() {
76 return Ok(());
77 }
78 for entry in fs::read_dir(path)? {
79 let entry = entry?;
80 let path = entry.path();
81 if path.is_dir() {
82 let dir_name = path.file_name().unwrap_or_default().to_string_lossy();
83 if !matches!(dir_name.as_ref(), "target" | ".git" | "node_modules") {
84 self.find_rust_files_recursive(&path.to_string_lossy(), files)?;
85 }
86 } else if let Some(ext) = path.extension() {
87 if ext == "rs" {
88 files.push(path.to_string_lossy().to_string());
89 }
90 }
91 }
92 Ok(())
93 }
94 fn analyze_sql_macros(&self, file_path: &str) -> Result<Vec<SqlMacroAnalysis>> {
95 let content = fs::read_to_string(file_path)?;
96 let mut analyses = Vec::new();
97 let macro_patterns = vec![
98 r#"sql!s*\(\s*"([^"]+)""#, r#"query!s*\(\s*"([^"]+)""#,
99 r#"sqlx::query!s*\(\s*"([^"]+)""#,
100 r#"diesel::prelude::sql_query\s*\(\s*"([^"]+)""#,
101 r#"sea_orm::Statement::from_string\s*\(\s*"([^"]+)""#,
102 ];
103 for (line_num, line) in content.lines().enumerate() {
104 for pattern in ¯o_patterns {
105 if let Ok(regex) = Regex::new(pattern) {
106 if let Some(captures) = regex.captures(line) {
107 if let Some(sql_match) = captures.get(1) {
108 let sql_query = sql_match.as_str().to_string();
109 let macro_name = self.extract_macro_name(line);
110 let analysis = self
111 .analyze_sql_query(
112 file_path.to_string(),
113 macro_name,
114 sql_query,
115 line_num + 1,
116 );
117 analyses.push(analysis);
118 }
119 }
120 }
121 }
122 }
123 Ok(analyses)
124 }
125 fn extract_macro_name(&self, line: &str) -> String {
126 if line.contains("sqlx::query!") {
127 "sqlx::query!".to_string()
128 } else if line.contains("diesel::") {
129 "diesel::sql_query".to_string()
130 } else if line.contains("sea_orm::") {
131 "sea_orm::Statement".to_string()
132 } else if line.contains("sql!") {
133 "sql!".to_string()
134 } else if line.contains("query!") {
135 "query!".to_string()
136 } else {
137 "unknown_macro".to_string()
138 }
139 }
140 fn analyze_sql_query(
141 &self,
142 file_path: String,
143 macro_name: String,
144 sql_query: String,
145 line_number: usize,
146 ) -> SqlMacroAnalysis {
147 let mut issues = Vec::new();
148 let mut security_score = 100;
149 let mut performance_score = 100;
150 let parameters = self.extract_parameters(&sql_query);
151 if self.has_sql_injection_risks(&sql_query) {
152 security_score -= 50;
153 issues.push("Potential SQL injection vulnerability".to_string());
154 }
155 if self.has_unparameterized_queries(&sql_query, ¶meters) {
156 security_score -= 30;
157 issues.push("Unparameterized query detected".to_string());
158 }
159 if self.uses_deprecated_features(&sql_query) {
160 security_score -= 20;
161 issues.push("Uses deprecated SQL features".to_string());
162 }
163 if self.has_select_star(&sql_query) {
164 performance_score -= 20;
165 issues.push("SELECT * detected - specify columns explicitly".to_string());
166 }
167 if self.has_missing_indexes(&sql_query) {
168 performance_score -= 15;
169 issues.push("Query may benefit from additional indexes".to_string());
170 }
171 if self.has_cartesian_product(&sql_query) {
172 performance_score -= 25;
173 issues.push("Potential Cartesian product in JOIN".to_string());
174 }
175 if self.has_inefficient_functions(&sql_query) {
176 performance_score -= 10;
177 issues.push("Uses potentially inefficient functions".to_string());
178 }
179 SqlMacroAnalysis {
180 file_path,
181 macro_name,
182 sql_query,
183 line_number,
184 parameters,
185 security_score,
186 performance_score,
187 issues,
188 }
189 }
190 fn extract_parameters(&self, sql_query: &str) -> Vec<String> {
191 let mut parameters = Vec::new();
192 let param_patterns = vec![r"\$\d+", r"\?", r":\w+", r"#\{\w+\}", r"%s|%d|%f",];
193 for pattern in param_patterns {
194 if let Ok(regex) = Regex::new(pattern) {
195 for captures in regex.captures_iter(sql_query) {
196 if let Some(param) = captures.get(0) {
197 parameters.push(param.as_str().to_string());
198 }
199 }
200 }
201 }
202 parameters
203 }
204 fn has_sql_injection_risks(&self, sql_query: &str) -> bool {
205 sql_query.contains(" + ") || sql_query.contains(" || ")
206 || sql_query.to_lowercase().contains("concat") || sql_query.contains("' + ")
207 || sql_query.contains(" + '")
208 }
209 fn has_unparameterized_queries(
210 &self,
211 sql_query: &str,
212 parameters: &[String],
213 ) -> bool {
214 let string_literals = Regex::new(r"'[^']*'").unwrap();
215 let string_count = string_literals.captures_iter(sql_query).count();
216 string_count > parameters.len() + 2
217 }
218 fn uses_deprecated_features(&self, sql_query: &str) -> bool {
219 let deprecated_features = vec!["mysql_", "old_", "deprecated_", "type=",];
220 deprecated_features
221 .iter()
222 .any(|feature| sql_query.to_lowercase().contains(feature))
223 }
224 fn has_select_star(&self, sql_query: &str) -> bool {
225 Regex::new(r"\bSELECT\s+\*").unwrap().is_match(sql_query)
226 }
227 fn has_missing_indexes(&self, sql_query: &str) -> bool {
228 let where_clause = Regex::new(r"WHERE\s+(.+?)(?:ORDER|GROUP|LIMIT|$)").unwrap();
229 if let Some(captures) = where_clause.captures(sql_query) {
230 if let Some(where_part) = captures.get(1) {
231 let where_text = where_part.as_str();
232 where_text.contains("LIKE '%") || where_text.contains("NOT IN")
233 || where_text.contains("OR") && !where_text.contains("AND")
234 } else {
235 false
236 }
237 } else {
238 false
239 }
240 }
241 fn has_cartesian_product(&self, sql_query: &str) -> bool {
242 let join_without_on = Regex::new(r"JOIN\s+\w+\s*(?:WHERE|ORDER|GROUP|HAVING|$)")
243 .unwrap();
244 let multiple_from = Regex::new(r"FROM\s+\w+.*[,;]\s*\w+").unwrap();
245 join_without_on.is_match(sql_query) || multiple_from.is_match(sql_query)
246 }
247 fn has_inefficient_functions(&self, sql_query: &str) -> bool {
248 let inefficient_functions = vec![
249 "count(*)", "distinct(", "substring(", "concat(", "coalesce(",
250 ];
251 inefficient_functions.iter().any(|func| sql_query.to_lowercase().contains(func))
252 }
253 fn check_sql_syntax(&self, sql_query: &str) -> Vec<SyntaxError> {
254 let mut errors = Vec::new();
255 let paren_count = sql_query.chars().filter(|&c| c == '(').count()
256 - sql_query.chars().filter(|&c| c == ')').count();
257 if paren_count != 0 {
258 errors
259 .push(SyntaxError {
260 file_path: "unknown".to_string(),
261 line_number: 0,
262 error_type: "unmatched_parentheses".to_string(),
263 description: format!(
264 "Unmatched parentheses: {} open, {} close", sql_query.chars()
265 .filter(|& c | c == '(').count(), sql_query.chars().filter(|& c |
266 c == ')').count()
267 ),
268 sql_snippet: sql_query.to_string(),
269 });
270 }
271 if !sql_query.trim().ends_with(';')
272 && !sql_query.to_lowercase().contains("select")
273 {
274 errors
275 .push(SyntaxError {
276 file_path: "unknown".to_string(),
277 line_number: 0,
278 error_type: "missing_semicolon".to_string(),
279 description: "SQL statement should end with semicolon".to_string(),
280 sql_snippet: sql_query.to_string(),
281 });
282 }
283 if sql_query.to_lowercase().contains("sele ct") {
284 errors
285 .push(SyntaxError {
286 file_path: "unknown".to_string(),
287 line_number: 0,
288 error_type: "typo_in_keyword".to_string(),
289 description: "Possible typo in SELECT keyword".to_string(),
290 sql_snippet: sql_query.to_string(),
291 });
292 }
293 errors
294 }
295 fn generate_suggestions(&self, analyses: &[SqlMacroAnalysis]) -> Vec<String> {
296 let mut suggestions = Vec::new();
297 let has_security_issues = analyses.iter().any(|a| a.security_score < 100);
298 let has_performance_issues = analyses.iter().any(|a| a.performance_score < 100);
299 if has_security_issues {
300 suggestions
301 .push("Use parameterized queries to prevent SQL injection".to_string());
302 suggestions.push("Avoid string concatenation in SQL queries".to_string());
303 suggestions
304 .push(
305 "Use prepared statements with proper parameter binding".to_string(),
306 );
307 }
308 if has_performance_issues {
309 suggestions
310 .push(
311 "Specify columns explicitly instead of using SELECT *".to_string(),
312 );
313 suggestions
314 .push(
315 "Add appropriate indexes for frequently queried columns".to_string(),
316 );
317 suggestions
318 .push(
319 "Avoid Cartesian products by using proper JOIN conditions"
320 .to_string(),
321 );
322 suggestions
323 .push("Consider query optimization and EXPLAIN plans".to_string());
324 }
325 suggestions.push("Use database migrations for schema changes".to_string());
326 suggestions
327 .push("Implement proper error handling for database operations".to_string());
328 suggestions
329 .push("Add database connection pooling for better performance".to_string());
330 suggestions.push("Use transactions for multi-statement operations".to_string());
331 suggestions
332 }
333 fn display_report(
334 &self,
335 report: &SqlAnalysisReport,
336 output_format: OutputFormat,
337 verbose: bool,
338 ) {
339 match output_format {
340 OutputFormat::Human => {
341 println!(
342 "\nš {} - SQL Macro Analysis Report", "CargoMate SqlMacroCheck"
343 .bold().blue()
344 );
345 println!("{}", "ā".repeat(60).blue());
346 println!("\nš Summary:");
347 println!(" ⢠Files Analyzed: {}", report.files_analyzed);
348 println!(" ⢠SQL Queries Found: {}", report.sql_queries_found);
349 println!(" ⢠Security Issues: {}", report.security_issues.len());
350 println!(
351 " ⢠Performance Issues: {}", report.performance_issues.len()
352 );
353 println!(" ⢠Syntax Errors: {}", report.syntax_errors.len());
354 if !report.macros_analyzed.is_empty() && verbose {
355 println!("\nš§ SQL Macros Analyzed:");
356 for analysis in &report.macros_analyzed {
357 let security_icon = if analysis.security_score >= 80 {
358 "š”ļø"
359 } else {
360 "ā ļø"
361 };
362 let performance_icon = if analysis.performance_score >= 80 {
363 "š"
364 } else {
365 "š"
366 };
367 println!(
368 " {} {} - {} (Security: {}, Performance: {})", analysis
369 .macro_name, analysis.file_path.split('/').last().unwrap_or(&
370 analysis.file_path), format!("{}:{}", analysis.line_number,
371 analysis.sql_query.chars().take(50).collect::< String > ()),
372 analysis.security_score, analysis.performance_score
373 );
374 if verbose && !analysis.issues.is_empty() {
375 for issue in &analysis.issues {
376 println!(" ⢠{}", issue.yellow());
377 }
378 }
379 }
380 }
381 if !report.security_issues.is_empty() {
382 println!("\nš Security Issues:");
383 for issue in &report.security_issues {
384 let severity_icon = match issue.severity.as_str() {
385 "critical" => "šØ",
386 "high" => "ā",
387 "medium" => "ā ļø",
388 "low" => "ā¹ļø",
389 _ => "ā¢",
390 };
391 println!(
392 " {} {}:{} - {}", severity_icon, issue.file_path.split('/')
393 .last().unwrap_or(& issue.file_path), issue.line_number,
394 issue.description
395 );
396 if verbose {
397 println!(" SQL: {}", issue.sql_snippet.dimmed());
398 println!(" š” {}", issue.suggestion);
399 }
400 }
401 }
402 if !report.performance_issues.is_empty() {
403 println!("\nā” Performance Issues:");
404 for issue in &report.performance_issues {
405 println!(
406 " š {}:{} - {}", issue.file_path.split('/').last()
407 .unwrap_or(& issue.file_path), issue.line_number, issue
408 .description
409 );
410 if verbose {
411 println!(" SQL: {}", issue.sql_snippet.dimmed());
412 println!(" š” {}", issue.suggestion);
413 }
414 }
415 }
416 if !report.syntax_errors.is_empty() {
417 println!("\nā Syntax Errors:");
418 for error in &report.syntax_errors {
419 println!(
420 " š“ {}:{} - {}", error.file_path.split('/').last()
421 .unwrap_or(& error.file_path), error.line_number, error
422 .description
423 );
424 if verbose {
425 println!(" SQL: {}", error.sql_snippet.dimmed());
426 }
427 }
428 }
429 if !report.suggestions.is_empty() {
430 println!("\nš” Suggestions:");
431 for suggestion in &report.suggestions {
432 println!(" ⢠{}", suggestion.cyan());
433 }
434 }
435 println!("\nā
Analysis complete!");
436 let total_issues = report.security_issues.len()
437 + report.performance_issues.len() + report.syntax_errors.len();
438 if total_issues == 0 {
439 println!(" All SQL queries look good!");
440 } else {
441 println!(" Found {} issue(s) to address", total_issues);
442 }
443 }
444 OutputFormat::Json => {
445 let json = serde_json::to_string_pretty(report)
446 .unwrap_or_else(|_| "{}".to_string());
447 println!("{}", json);
448 }
449 OutputFormat::Table => {
450 println!(
451 "{:<30} {:<15} {:<15} {:<15} {:<10}", "File", "Queries", "Security",
452 "Performance", "Syntax"
453 );
454 println!("{}", "ā".repeat(90));
455 let mut file_stats = HashMap::new();
456 for analysis in &report.macros_analyzed {
457 let entry = file_stats
458 .entry(&analysis.file_path)
459 .or_insert((0, 0, 0, 0));
460 entry.0 += 1;
461 }
462 for issue in &report.security_issues {
463 if let Some(entry) = file_stats.get_mut(&issue.file_path) {
464 entry.1 += 1;
465 }
466 }
467 for issue in &report.performance_issues {
468 if let Some(entry) = file_stats.get_mut(&issue.file_path) {
469 entry.2 += 1;
470 }
471 }
472 for error in &report.syntax_errors {
473 if let Some(entry) = file_stats.get_mut(&error.file_path) {
474 entry.3 += 1;
475 }
476 }
477 for (file_path, (queries, security, performance, syntax)) in file_stats {
478 let file_name = file_path.split('/').last().unwrap_or(&file_path);
479 println!(
480 "{:<30} {:<15} {:<15} {:<15} {:<10}", file_name, queries
481 .to_string(), security.to_string(), performance.to_string(),
482 syntax.to_string()
483 );
484 }
485 }
486 }
487 }
488}
489impl Tool for SqlMacroCheckTool {
490 fn name(&self) -> &'static str {
491 "sql-macro-check"
492 }
493 fn description(&self) -> &'static str {
494 "Compile-time SQL query validation"
495 }
496 fn command(&self) -> Command {
497 Command::new(self.name())
498 .about(self.description())
499 .long_about(
500 "Analyze SQL queries in Rust code for security vulnerabilities, \
501 performance issues, and syntax errors. Supports multiple SQL libraries.
502
503EXAMPLES:
504 cm tool sql-macro-check --input src/
505 cm tool sql-macro-check --workspace --security-only
506 cm tool sql-macro-check --input src/db.rs --fix-suggestions",
507 )
508 .args(
509 &[
510 Arg::new("input")
511 .long("input")
512 .short('i')
513 .help("Input directory or file to analyze")
514 .default_value("src/"),
515 Arg::new("workspace")
516 .long("workspace")
517 .help("Analyze all Rust files in workspace")
518 .action(clap::ArgAction::SetTrue),
519 Arg::new("security-only")
520 .long("security-only")
521 .help("Only check for security issues")
522 .action(clap::ArgAction::SetTrue),
523 Arg::new("performance-only")
524 .long("performance-only")
525 .help("Only check for performance issues")
526 .action(clap::ArgAction::SetTrue),
527 Arg::new("syntax-only")
528 .long("syntax-only")
529 .help("Only check for syntax errors")
530 .action(clap::ArgAction::SetTrue),
531 Arg::new("fix-suggestions")
532 .long("fix-suggestions")
533 .help("Show detailed fix suggestions")
534 .action(clap::ArgAction::SetTrue),
535 Arg::new("library")
536 .long("library")
537 .short('l')
538 .help("SQL library to check")
539 .default_value("auto")
540 .value_parser(["auto", "sqlx", "diesel", "sea-orm", "rusqlite"]),
541 ],
542 )
543 .args(&common_options())
544 }
545 fn execute(&self, matches: &ArgMatches) -> Result<()> {
546 let input = matches.get_one::<String>("input").unwrap();
547 let workspace = matches.get_flag("workspace");
548 let security_only = matches.get_flag("security-only");
549 let performance_only = matches.get_flag("performance-only");
550 let syntax_only = matches.get_flag("syntax-only");
551 let fix_suggestions = matches.get_flag("fix-suggestions");
552 let library = matches.get_one::<String>("library").unwrap();
553 let output_format = parse_output_format(matches);
554 let verbose = matches.get_flag("verbose");
555 println!(
556 "š {} - Analyzing SQL Macros", "CargoMate SqlMacroCheck".bold().blue()
557 );
558 let mut all_analyses = Vec::new();
559 let mut security_issues = Vec::new();
560 let mut performance_issues = Vec::new();
561 let mut syntax_errors = Vec::new();
562 let files_to_analyze = if workspace {
563 self.find_rust_files(".")?
564 } else if Path::new(input).is_file() {
565 vec![input.clone()]
566 } else {
567 self.find_rust_files(input)?
568 };
569 if files_to_analyze.is_empty() {
570 println!("{}", "No Rust files found to analyze".yellow());
571 return Ok(());
572 }
573 for file_path in &files_to_analyze {
574 match self.analyze_sql_macros(file_path) {
575 Ok(analyses) => {
576 for analysis in analyses {
577 all_analyses.push(analysis.clone());
578 for issue in &analysis.issues {
579 if issue.contains("injection")
580 || issue.contains("parameterized")
581 || issue.contains("deprecated")
582 {
583 security_issues
584 .push(SecurityIssue {
585 file_path: analysis.file_path.clone(),
586 line_number: analysis.line_number,
587 issue_type: "security_vulnerability".to_string(),
588 description: issue.clone(),
589 severity: if issue.contains("injection") {
590 "high".to_string()
591 } else {
592 "medium".to_string()
593 },
594 sql_snippet: analysis.sql_query.clone(),
595 suggestion: self.generate_security_suggestion(issue),
596 });
597 } else {
598 performance_issues
599 .push(PerformanceIssue {
600 file_path: analysis.file_path.clone(),
601 line_number: analysis.line_number,
602 issue_type: "performance_issue".to_string(),
603 description: issue.clone(),
604 sql_snippet: analysis.sql_query.clone(),
605 suggestion: self.generate_performance_suggestion(issue),
606 });
607 }
608 }
609 let syntax_issues = self.check_sql_syntax(&analysis.sql_query);
610 for syntax_error in syntax_issues {
611 syntax_errors
612 .push(SyntaxError {
613 file_path: analysis.file_path.clone(),
614 line_number: analysis.line_number,
615 error_type: syntax_error.error_type,
616 description: syntax_error.description,
617 sql_snippet: syntax_error.sql_snippet,
618 });
619 }
620 }
621 }
622 Err(e) => {
623 println!("ā ļø Failed to analyze {}: {}", file_path, e);
624 }
625 }
626 }
627 if all_analyses.is_empty() {
628 println!("{}", "No SQL macros found in the codebase".yellow());
629 return Ok(());
630 }
631 let final_analyses = if security_only || performance_only || syntax_only {
632 all_analyses
633 .into_iter()
634 .filter(|analysis| {
635 if security_only {
636 analysis.security_score < 100
637 } else if performance_only {
638 analysis.performance_score < 100
639 } else {
640 false
641 }
642 })
643 .collect()
644 } else {
645 all_analyses
646 };
647 let final_security_issues = if security_only || !performance_only && !syntax_only
648 {
649 security_issues
650 } else {
651 Vec::new()
652 };
653 let final_performance_issues = if performance_only
654 || !security_only && !syntax_only
655 {
656 performance_issues
657 } else {
658 Vec::new()
659 };
660 let final_syntax_errors = if syntax_only || !security_only && !performance_only {
661 syntax_errors
662 } else {
663 Vec::new()
664 };
665 let suggestions = self.generate_suggestions(&final_analyses);
666 let report = SqlAnalysisReport {
667 files_analyzed: files_to_analyze.len(),
668 sql_queries_found: final_analyses.len(),
669 macros_analyzed: final_analyses,
670 security_issues: final_security_issues,
671 performance_issues: final_performance_issues,
672 syntax_errors: final_syntax_errors,
673 suggestions,
674 timestamp: chrono::Utc::now().to_rfc3339(),
675 };
676 self.display_report(&report, output_format, verbose);
677 Ok(())
678 }
679}
680impl SqlMacroCheckTool {
681 fn generate_security_suggestion(&self, issue: &str) -> String {
682 if issue.contains("injection") {
683 "Use parameterized queries with bound parameters instead of string concatenation"
684 .to_string()
685 } else if issue.contains("parameterized") {
686 "Replace string literals with parameter placeholders (e.g., $1, ?, :param)"
687 .to_string()
688 } else if issue.contains("deprecated") {
689 "Update to use current SQL features and avoid deprecated functions"
690 .to_string()
691 } else {
692 "Review query for potential security vulnerabilities".to_string()
693 }
694 }
695 fn generate_performance_suggestion(&self, issue: &str) -> String {
696 if issue.contains("SELECT *") {
697 "Specify column names explicitly to reduce data transfer and improve query performance"
698 .to_string()
699 } else if issue.contains("indexes") {
700 "Add database indexes on frequently queried columns".to_string()
701 } else if issue.contains("Cartesian") {
702 "Add proper JOIN conditions to avoid Cartesian products".to_string()
703 } else if issue.contains("inefficient") {
704 "Consider using more efficient functions or query patterns".to_string()
705 } else {
706 "Review query execution plan and consider optimization".to_string()
707 }
708 }
709}
710impl Default for SqlMacroCheckTool {
711 fn default() -> Self {
712 Self::new()
713 }
714}