sqrust_rules/lint/
drop_schema_statement.rs1use 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 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 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
72fn 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
118fn 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}