Skip to main content

sqrust_rules/lint/
alter_table_drop_column.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{AlterTableOperation, Statement};
3
4pub struct AlterTableDropColumn;
5
6impl Rule for AlterTableDropColumn {
7    fn name(&self) -> &'static str {
8        "Lint/AlterTableDropColumn"
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        for stmt in &ctx.statements {
22            if let Statement::AlterTable { operations, .. } = stmt {
23                for op in operations {
24                    if matches!(op, AlterTableOperation::DropColumn { .. }) {
25                        let (line, col) =
26                            find_keyword_position(source, &source_upper, "ALTER");
27                        diags.push(Diagnostic {
28                            rule: self.name(),
29                            message:
30                                "ALTER TABLE DROP COLUMN is irreversible; verify all dependent \
31                                 code has been updated"
32                                    .to_string(),
33                            line,
34                            col,
35                        });
36                    }
37                }
38            }
39        }
40
41        diags
42    }
43}
44
45/// Finds the 1-indexed (line, col) of the first occurrence of `keyword` (already uppercased)
46/// in `source_upper` that has word boundaries on both sides.
47/// Falls back to (1, 1) if not found.
48fn find_keyword_position(source: &str, source_upper: &str, keyword: &str) -> (usize, usize) {
49    let kw_len = keyword.len();
50    let bytes = source_upper.as_bytes();
51    let text_len = bytes.len();
52
53    let mut search_from = 0usize;
54    while search_from < text_len {
55        let Some(rel) = source_upper[search_from..].find(keyword) else {
56            break;
57        };
58        let abs = search_from + rel;
59
60        let before_ok = abs == 0
61            || {
62                let b = bytes[abs - 1];
63                !b.is_ascii_alphanumeric() && b != b'_'
64            };
65        let after = abs + kw_len;
66        let after_ok = after >= text_len
67            || {
68                let b = bytes[after];
69                !b.is_ascii_alphanumeric() && b != b'_'
70            };
71
72        if before_ok && after_ok {
73            return offset_to_line_col(source, abs);
74        }
75        search_from = abs + 1;
76    }
77
78    (1, 1)
79}
80
81/// Converts a byte offset in `source` to a 1-indexed (line, col) pair.
82fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
83    let before = &source[..offset];
84    let line = before.chars().filter(|&c| c == '\n').count() + 1;
85    let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
86    (line, col)
87}