Skip to main content

sqrust_rules/lint/
insert_without_column_list.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::Statement;
3
4pub struct InsertWithoutColumnList;
5
6impl Rule for InsertWithoutColumnList {
7    fn name(&self) -> &'static str {
8        "Lint/InsertWithoutColumnList"
9    }
10
11    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12        // Skip files that failed to parse — AST is 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 byte offset so we can report the position of each INSERT.
22        // When there are multiple INSERT statements, we search for the
23        // next INSERT keyword from after the previous one.
24        let mut search_from = 0usize;
25
26        for stmt in &ctx.statements {
27            if let Statement::Insert(insert) = stmt {
28                if insert.columns.is_empty() {
29                    let (line, col) =
30                        find_keyword_position(source, &source_upper, "INSERT", &mut search_from);
31                    diags.push(Diagnostic {
32                        rule: self.name(),
33                        message: "INSERT statement missing explicit column list; specify columns for safety"
34                            .to_string(),
35                        line,
36                        col,
37                    });
38                } else {
39                    // Has columns — still advance past this INSERT keyword so
40                    // the next search starts after it.
41                    advance_past_keyword(source, &source_upper, "INSERT", &mut search_from);
42                }
43            }
44        }
45
46        diags
47    }
48}
49
50/// Finds the 1-indexed (line, col) of the next occurrence of `keyword`
51/// (already uppercased) in `source_upper` starting from `search_from`,
52/// with word boundaries on both sides.
53/// Updates `search_from` to point past the matched keyword.
54/// Falls back to (1, 1) if not found.
55fn find_keyword_position(
56    source: &str,
57    source_upper: &str,
58    keyword: &str,
59    search_from: &mut usize,
60) -> (usize, usize) {
61    let (line, col, new_from) = find_keyword_inner(source, source_upper, keyword, *search_from);
62    *search_from = new_from;
63    (line, col)
64}
65
66/// Advance `search_from` past the next occurrence of `keyword` without
67/// recording a diagnostic.
68fn advance_past_keyword(
69    source: &str,
70    source_upper: &str,
71    keyword: &str,
72    search_from: &mut usize,
73) {
74    let (_, _, new_from) = find_keyword_inner(source, source_upper, keyword, *search_from);
75    *search_from = new_from;
76}
77
78/// Core search: returns (line, col, next_search_from).
79fn find_keyword_inner(
80    source: &str,
81    source_upper: &str,
82    keyword: &str,
83    start: usize,
84) -> (usize, usize, usize) {
85    let kw_len = keyword.len();
86    let bytes = source_upper.as_bytes();
87    let text_len = bytes.len();
88
89    let mut search_from = start;
90    while search_from < text_len {
91        let Some(rel) = source_upper[search_from..].find(keyword) else {
92            break;
93        };
94        let abs = search_from + rel;
95
96        let before_ok = abs == 0
97            || {
98                let b = bytes[abs - 1];
99                !b.is_ascii_alphanumeric() && b != b'_'
100            };
101        let after = abs + kw_len;
102        let after_ok = after >= text_len
103            || {
104                let b = bytes[after];
105                !b.is_ascii_alphanumeric() && b != b'_'
106            };
107
108        if before_ok && after_ok {
109            let (line, col) = offset_to_line_col(source, abs);
110            return (line, col, after);
111        }
112        search_from = abs + 1;
113    }
114
115    (1, 1, start)
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 mut line = 1usize;
121    let mut col = 1usize;
122    for (i, ch) in source.char_indices() {
123        if i == offset {
124            break;
125        }
126        if ch == '\n' {
127            line += 1;
128            col = 1;
129        } else {
130            col += 1;
131        }
132    }
133    (line, col)
134}