Skip to main content

sqrust_rules/lint/
update_without_where.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::Statement;
3
4pub struct UpdateWithoutWhere;
5
6impl Rule for UpdateWithoutWhere {
7    fn name(&self) -> &'static str {
8        "Lint/UpdateWithoutWhere"
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::Update { selection, .. } = stmt {
23                if selection.is_none() {
24                    // Find the position of the UPDATE keyword in source (case-insensitive,
25                    // word-boundary check).
26                    let (line, col) = find_keyword_position(source, &source_upper, "UPDATE");
27                    diags.push(Diagnostic {
28                        rule: self.name(),
29                        message: "UPDATE without WHERE clause will update all rows".to_string(),
30                        line,
31                        col,
32                    });
33                }
34            }
35        }
36
37        diags
38    }
39}
40
41/// Finds the 1-indexed (line, col) of the first occurrence of `keyword` (already uppercased)
42/// in `source_upper` that has word boundaries on both sides.
43/// Falls back to (1, 1) if not found.
44fn find_keyword_position(source: &str, source_upper: &str, keyword: &str) -> (usize, usize) {
45    let kw_len = keyword.len();
46    let bytes = source_upper.as_bytes();
47    let text_len = bytes.len();
48
49    let mut search_from = 0usize;
50    while search_from < text_len {
51        let Some(rel) = source_upper[search_from..].find(keyword) else {
52            break;
53        };
54        let abs = search_from + rel;
55
56        let before_ok = abs == 0
57            || {
58                let b = bytes[abs - 1];
59                !b.is_ascii_alphanumeric() && b != b'_'
60            };
61        let after = abs + kw_len;
62        let after_ok = after >= text_len
63            || {
64                let b = bytes[after];
65                !b.is_ascii_alphanumeric() && b != b'_'
66            };
67
68        if before_ok && after_ok {
69            return offset_to_line_col(source, abs);
70        }
71        search_from = abs + 1;
72    }
73
74    (1, 1)
75}
76
77/// Converts a byte offset in `source` to a 1-indexed (line, col) pair.
78fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
79    let mut line = 1usize;
80    let mut col = 1usize;
81    for (i, ch) in source.char_indices() {
82        if i == offset {
83            break;
84        }
85        if ch == '\n' {
86            line += 1;
87            col = 1;
88        } else {
89            col += 1;
90        }
91    }
92    (line, col)
93}