sqrust_rules/lint/
alter_table_add_not_null_without_default.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct AlterTableAddNotNullWithoutDefault;
4
5impl Rule for AlterTableAddNotNullWithoutDefault {
6 fn name(&self) -> &'static str {
7 "Lint/AlterTableAddNotNullWithoutDefault"
8 }
9
10 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11 let source = &ctx.source;
12 let bytes = source.as_bytes();
13 let len = bytes.len();
14 let skip = build_skip(bytes);
15
16 let mut diags = Vec::new();
17 let upper = source.to_uppercase();
18 let upper_bytes = upper.as_bytes();
19
20 let mut i = 0;
21 while i < len {
22 if skip[i] {
24 i += 1;
25 continue;
26 }
27
28 if !upper[i..].starts_with("ALTER") {
30 i += 1;
31 continue;
32 }
33
34 let alter_start = i;
35 let alter_end = i + 5; let before_ok = alter_start == 0 || {
39 let b = upper_bytes[alter_start - 1];
40 !b.is_ascii_alphanumeric() && b != b'_'
41 };
42
43 let after_ok = alter_end >= len || {
45 let b = upper_bytes[alter_end];
46 !b.is_ascii_alphanumeric() && b != b'_'
47 };
48
49 if !before_ok || !after_ok {
50 i += 1;
51 continue;
52 }
53
54 let mut j = alter_end;
56 while j < len && bytes[j].is_ascii_whitespace() && !skip[j] {
57 j += 1;
58 }
59
60 if j >= len || skip[j] || !upper[j..].starts_with("TABLE") {
61 i += 1;
62 continue;
63 }
64
65 let table_end = j + 5; let table_after_ok = table_end >= len || {
67 let b = upper_bytes[table_end];
68 !b.is_ascii_alphanumeric() && b != b'_'
69 };
70
71 if !table_after_ok {
72 i += 1;
73 continue;
74 }
75
76 let stmt_start = alter_start;
80 let mut stmt_end = len;
81 let mut k = table_end;
82 while k < len {
83 if !skip[k] && bytes[k] == b';' {
84 stmt_end = k;
85 break;
86 }
87 k += 1;
88 }
89
90 let stmt_upper = &upper[stmt_start..stmt_end];
92
93 let not_null_offset = find_word_boundary_keyword(stmt_upper, "NOT NULL");
95
96 if let Some(rel_offset) = not_null_offset {
97 if !contains_word_boundary_keyword(stmt_upper, "DEFAULT") {
100 let abs_offset = stmt_start + rel_offset;
102 let (line, col) = offset_to_line_col(source, abs_offset);
103 diags.push(Diagnostic {
104 rule: self.name(),
105 message:
106 "Adding a NOT NULL column without DEFAULT will fail on non-empty tables"
107 .to_string(),
108 line,
109 col,
110 });
111 }
112 }
113
114 i = stmt_end + 1;
116 }
117
118 diags
119 }
120}
121
122fn find_word_boundary_keyword(text: &str, keyword: &str) -> Option<usize> {
126 let kw_len = keyword.len();
127 let bytes = text.as_bytes();
128 let text_len = bytes.len();
129 let mut search_from = 0;
130
131 while search_from < text_len {
132 let Some(rel) = text[search_from..].find(keyword) else {
133 break;
134 };
135 let abs = search_from + rel;
136
137 let before_ok = abs == 0 || {
138 let b = bytes[abs - 1];
139 !b.is_ascii_alphanumeric() && b != b'_'
140 };
141 let after = abs + kw_len;
142 let after_ok = after >= text_len || {
143 let b = bytes[after];
144 !b.is_ascii_alphanumeric() && b != b'_'
145 };
146
147 if before_ok && after_ok {
148 return Some(abs);
149 }
150 search_from = abs + 1;
151 }
152
153 None
154}
155
156fn contains_word_boundary_keyword(text: &str, keyword: &str) -> bool {
158 find_word_boundary_keyword(text, keyword).is_some()
159}
160
161fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
163 let before = &source[..offset];
164 let line = before.chars().filter(|&c| c == '\n').count() + 1;
165 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
166 (line, col)
167}
168
169fn build_skip(bytes: &[u8]) -> Vec<bool> {
172 let len = bytes.len();
173 let mut skip = vec![false; len];
174 let mut i = 0;
175
176 while i < len {
177 if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
179 skip[i] = true;
180 skip[i + 1] = true;
181 i += 2;
182 while i < len && bytes[i] != b'\n' {
183 skip[i] = true;
184 i += 1;
185 }
186 continue;
187 }
188
189 if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
191 skip[i] = true;
192 skip[i + 1] = true;
193 i += 2;
194 while i < len {
195 if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
196 skip[i] = true;
197 skip[i + 1] = true;
198 i += 2;
199 break;
200 }
201 skip[i] = true;
202 i += 1;
203 }
204 continue;
205 }
206
207 if bytes[i] == b'\'' {
209 skip[i] = true;
210 i += 1;
211 while i < len {
212 if bytes[i] == b'\'' {
213 skip[i] = true;
214 i += 1;
215 if i < len && bytes[i] == b'\'' {
217 skip[i] = true;
218 i += 1;
219 continue;
220 }
221 break; }
223 skip[i] = true;
224 i += 1;
225 }
226 continue;
227 }
228
229 if bytes[i] == b'"' {
231 skip[i] = true;
232 i += 1;
233 while i < len && bytes[i] != b'"' {
234 skip[i] = true;
235 i += 1;
236 }
237 if i < len {
238 skip[i] = true;
239 i += 1;
240 }
241 continue;
242 }
243
244 if bytes[i] == b'`' {
246 skip[i] = true;
247 i += 1;
248 while i < len && bytes[i] != b'`' {
249 skip[i] = true;
250 i += 1;
251 }
252 if i < len {
253 skip[i] = true;
254 i += 1;
255 }
256 continue;
257 }
258
259 i += 1;
260 }
261
262 skip
263}