Skip to main content

cargo_mate/tools/
sql_macro_check.rs

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 &macro_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, &parameters) {
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}