sqrust_rules/lint/
insert_or_replace.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{SqliteOnConflict, Statement};
3
4pub struct InsertOrReplace;
5
6impl Rule for InsertOrReplace {
7 fn name(&self) -> &'static str {
8 "Lint/InsertOrReplace"
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;
23
24 for stmt in &ctx.statements {
25 if let Statement::Insert(insert) = stmt {
26 let is_replace_into = insert.replace_into;
28
29 let is_insert_or_replace = matches!(insert.or, Some(SqliteOnConflict::Replace));
32
33 if is_replace_into || is_insert_or_replace {
34 let (line, col) =
35 find_keyword_position(source, &source_upper, "REPLACE", &mut search_from);
36 diags.push(Diagnostic {
37 rule: self.name(),
38 message:
39 "INSERT OR REPLACE/REPLACE INTO silently deletes and re-inserts rows; prefer INSERT ... ON CONFLICT"
40 .to_string(),
41 line,
42 col,
43 });
44 } else {
45 advance_past_keyword(source, &source_upper, "INSERT", &mut search_from);
48 }
49 }
50 }
51
52 diags
53 }
54}
55
56fn find_keyword_position(
60 source: &str,
61 source_upper: &str,
62 keyword: &str,
63 search_from: &mut usize,
64) -> (usize, usize) {
65 let (line, col, new_from) = find_keyword_inner(source, source_upper, keyword, *search_from);
66 *search_from = new_from;
67 (line, col)
68}
69
70fn advance_past_keyword(
73 source: &str,
74 source_upper: &str,
75 keyword: &str,
76 search_from: &mut usize,
77) {
78 let (_, _, new_from) = find_keyword_inner(source, source_upper, keyword, *search_from);
79 *search_from = new_from;
80}
81
82fn find_keyword_inner(
85 source: &str,
86 source_upper: &str,
87 keyword: &str,
88 start: usize,
89) -> (usize, usize, usize) {
90 let kw_len = keyword.len();
91 let bytes = source_upper.as_bytes();
92 let text_len = bytes.len();
93
94 let mut pos = start;
95 while pos < text_len {
96 let Some(rel) = source_upper[pos..].find(keyword) else {
97 break;
98 };
99 let abs = pos + rel;
100
101 let before_ok = abs == 0
103 || {
104 let b = bytes[abs - 1];
105 !b.is_ascii_alphanumeric() && b != b'_'
106 };
107 let after = abs + kw_len;
109 let after_ok = after >= text_len
110 || {
111 let b = bytes[after];
112 !b.is_ascii_alphanumeric() && b != b'_'
113 };
114
115 if before_ok && after_ok {
116 let (line, col) = offset_to_line_col(source, abs);
117 return (line, col, after);
118 }
119 pos = abs + 1;
120 }
121
122 (1, 1, start)
123}
124
125fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
127 let before = &source[..offset];
128 let line = before.chars().filter(|&c| c == '\n').count() + 1;
129 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
130 (line, col)
131}