sqrust_rules/lint/
create_temp_table.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use std::collections::HashSet;
3
4pub struct CreateTempTable;
5
6impl Rule for CreateTempTable {
7 fn name(&self) -> &'static str {
8 "Lint/CreateTempTable"
9 }
10
11 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12 let source = &ctx.source;
13 let skip = build_skip_set(source);
14 let mut diags = Vec::new();
15
16 for (line, col) in find_keyword(source, "create temporary table", &skip) {
18 diags.push(Diagnostic {
19 rule: self.name(),
20 message: "CREATE TEMPORARY TABLE is dialect-specific and bypasses dbt model \
21 management — use a CTE or a dbt ephemeral model instead"
22 .to_string(),
23 line,
24 col,
25 });
26 }
27
28 for (line, col) in find_keyword(source, "create temp table", &skip) {
30 diags.push(Diagnostic {
31 rule: self.name(),
32 message: "CREATE TEMPORARY TABLE is dialect-specific and bypasses dbt model \
33 management — use a CTE or a dbt ephemeral model instead"
34 .to_string(),
35 line,
36 col,
37 });
38 }
39
40 diags.sort_by_key(|d| (d.line, d.col));
41 diags
42 }
43}
44
45fn build_skip_set(source: &str) -> HashSet<usize> {
46 let mut skip = HashSet::new();
47 let bytes = source.as_bytes();
48 let len = bytes.len();
49 let mut i = 0;
50 while i < len {
51 if bytes[i] == b'\'' {
52 i += 1;
53 while i < len {
54 if bytes[i] == b'\'' {
55 if i + 1 < len && bytes[i + 1] == b'\'' {
56 skip.insert(i);
57 i += 2;
58 } else {
59 i += 1;
60 break;
61 }
62 } else {
63 skip.insert(i);
64 i += 1;
65 }
66 }
67 } else if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
68 while i < len && bytes[i] != b'\n' {
69 skip.insert(i);
70 i += 1;
71 }
72 } else {
73 i += 1;
74 }
75 }
76 skip
77}
78
79fn find_keyword(source: &str, keyword: &str, skip: &HashSet<usize>) -> Vec<(usize, usize)> {
80 let lower = source.to_lowercase();
81 let kw_len = keyword.len();
82 let bytes = lower.as_bytes();
83 let len = bytes.len();
84 let mut results = Vec::new();
85 let mut i = 0;
86 while i + kw_len <= len {
87 if !skip.contains(&i) && lower[i..].starts_with(keyword) {
88 let before_ok = i == 0
89 || {
90 let b = bytes[i - 1];
91 !b.is_ascii_alphanumeric() && b != b'_'
92 };
93 let after_pos = i + kw_len;
94 let after_ok = after_pos >= len
95 || {
96 let b = bytes[after_pos];
97 !b.is_ascii_alphanumeric() && b != b'_'
98 };
99 if before_ok && after_ok {
100 let (line, col) = offset_to_line_col(source, i);
101 results.push((line, col));
102 }
103 }
104 i += 1;
105 }
106 results
107}
108
109fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
110 let before = &source[..offset];
111 let line = before.chars().filter(|&c| c == '\n').count() + 1;
112 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
113 (line, col)
114}