Skip to main content

sqrust_rules/lint/
create_temp_table.rs

1use 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        // Check CREATE TEMPORARY TABLE (longer pattern first to avoid double-matching)
17        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        // Check CREATE TEMP TABLE
29        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}