Skip to main content

sqrust_rules/lint/
drop_schema_statement.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{ObjectType, Statement};
3
4pub struct DropSchemaStatement;
5
6impl Rule for DropSchemaStatement {
7    fn name(&self) -> &'static str {
8        "Lint/DropSchemaStatement"
9    }
10
11    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12        // Skip files that failed to parse — AST may be incomplete.
13        if !ctx.parse_errors.is_empty() {
14            return Vec::new();
15        }
16
17        let mut diags = Vec::new();
18        let source = &ctx.source;
19        let source_upper = source.to_uppercase();
20
21        // Track occurrence indices separately for SCHEMA and DATABASE drops so
22        // that each can be located independently in the source text.
23        let mut schema_occurrence: usize = 0;
24        let mut database_occurrence: usize = 0;
25
26        for stmt in &ctx.statements {
27            if let Statement::Drop { object_type, .. } = stmt {
28                match object_type {
29                    ObjectType::Schema => {
30                        let (line, col) = find_nth_keyword(
31                            source,
32                            &source_upper,
33                            "DROP",
34                            schema_occurrence,
35                        );
36                        schema_occurrence += 1;
37                        diags.push(Diagnostic {
38                            rule: self.name(),
39                            message:
40                                "DROP SCHEMA/DATABASE is irreversible; ensure you have a backup"
41                                    .to_string(),
42                            line,
43                            col,
44                        });
45                    }
46                    ObjectType::Database => {
47                        let (line, col) = find_nth_keyword(
48                            source,
49                            &source_upper,
50                            "DROP",
51                            database_occurrence,
52                        );
53                        database_occurrence += 1;
54                        diags.push(Diagnostic {
55                            rule: self.name(),
56                            message:
57                                "DROP SCHEMA/DATABASE is irreversible; ensure you have a backup"
58                                    .to_string(),
59                            line,
60                            col,
61                        });
62                    }
63                    _ => {}
64                }
65            }
66        }
67
68        diags
69    }
70}
71
72/// Finds the 1-indexed (line, col) of the `nth` (0-indexed) word-boundary
73/// occurrence of `keyword` (already uppercased) in `source_upper`.
74/// Falls back to (1, 1) if not found.
75fn find_nth_keyword(
76    source: &str,
77    source_upper: &str,
78    keyword: &str,
79    nth: usize,
80) -> (usize, usize) {
81    let kw_len = keyword.len();
82    let bytes = source_upper.as_bytes();
83    let text_len = bytes.len();
84
85    let mut count = 0usize;
86    let mut search_from = 0usize;
87
88    while search_from < text_len {
89        let Some(rel) = source_upper[search_from..].find(keyword) else {
90            break;
91        };
92        let abs = search_from + rel;
93
94        let before_ok = abs == 0
95            || {
96                let b = bytes[abs - 1];
97                !b.is_ascii_alphanumeric() && b != b'_'
98            };
99        let after = abs + kw_len;
100        let after_ok = after >= text_len
101            || {
102                let b = bytes[after];
103                !b.is_ascii_alphanumeric() && b != b'_'
104            };
105
106        if before_ok && after_ok {
107            if count == nth {
108                return offset_to_line_col(source, abs);
109            }
110            count += 1;
111        }
112        search_from = abs + 1;
113    }
114
115    (1, 1)
116}
117
118/// Converts a byte offset in `source` to a 1-indexed (line, col) pair.
119fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
120    let before = &source[..offset];
121    let line = before.chars().filter(|&c| c == '\n').count() + 1;
122    let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
123    (line, col)
124}