sqrust_rules/lint/
insert_without_column_list.rs1use 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 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 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 advance_past_keyword(source, &source_upper, "INSERT", &mut search_from);
42 }
43 }
44 }
45
46 diags
47 }
48}
49
50fn 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
66fn 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
78fn 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
118fn 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}