Skip to main content

sqrust_rules/ambiguous/
interval_expression.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct IntervalExpression;
4
5const INTERVAL_KEYWORD: &[u8] = b"INTERVAL";
6const INTERVAL_KEYWORD_LEN: usize = INTERVAL_KEYWORD.len();
7
8impl Rule for IntervalExpression {
9    fn name(&self) -> &'static str {
10        "Ambiguous/IntervalExpression"
11    }
12
13    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
14        find_violations(&ctx.source, self.name())
15    }
16}
17
18fn find_violations(source: &str, rule_name: &'static str) -> Vec<Diagnostic> {
19    let bytes = source.as_bytes();
20    let len = bytes.len();
21
22    if len == 0 {
23        return Vec::new();
24    }
25
26    let skip = build_skip_set(bytes, len);
27    let mut diags = Vec::new();
28    let mut i = 0;
29
30    while i + INTERVAL_KEYWORD_LEN <= len {
31        if skip[i] {
32            i += 1;
33            continue;
34        }
35
36        // Word boundary before INTERVAL
37        let before_ok = i == 0 || !is_word_char(bytes[i - 1]);
38        if before_ok && bytes[i..i + INTERVAL_KEYWORD_LEN].eq_ignore_ascii_case(INTERVAL_KEYWORD) {
39            let after = i + INTERVAL_KEYWORD_LEN;
40            // Word boundary after: the character immediately after must not be a word char
41            // This ensures `INTERVAL_DAYS` (identifier containing INTERVAL) is not flagged.
42            let after_ok = after >= len || !is_word_char(bytes[after]);
43            if after_ok {
44                let (line, col) = line_col(source, i);
45                diags.push(Diagnostic {
46                    rule: rule_name,
47                    message: "INTERVAL expression syntax varies across databases — \
48                               SQL Server uses DATEADD(), MySQL/BigQuery use INTERVAL N UNIT, \
49                               PostgreSQL uses INTERVAL 'N unit'; consider using a date \
50                               arithmetic function abstraction"
51                        .to_string(),
52                    line,
53                    col,
54                });
55                i += INTERVAL_KEYWORD_LEN;
56                continue;
57            }
58        }
59
60        i += 1;
61    }
62
63    diags
64}
65
66#[inline]
67fn is_word_char(ch: u8) -> bool {
68    ch.is_ascii_alphanumeric() || ch == b'_'
69}
70
71fn line_col(source: &str, offset: usize) -> (usize, usize) {
72    let before = &source[..offset.min(source.len())];
73    let line = before.chars().filter(|&c| c == '\n').count() + 1;
74    let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
75    (line, col)
76}
77
78/// Build a boolean skip-set: `skip[i] == true` means byte `i` is inside a
79/// single-quoted string, double-quoted identifier, block comment, or line comment.
80fn build_skip_set(bytes: &[u8], len: usize) -> Vec<bool> {
81    let mut skip = vec![false; len];
82    let mut i = 0;
83
84    while i < len {
85        // Single-quoted string: '...' with '' escape.
86        if bytes[i] == b'\'' {
87            skip[i] = true;
88            i += 1;
89            while i < len {
90                skip[i] = true;
91                if bytes[i] == b'\'' {
92                    if i + 1 < len && bytes[i + 1] == b'\'' {
93                        i += 1;
94                        skip[i] = true;
95                        i += 1;
96                        continue;
97                    }
98                    i += 1;
99                    break;
100                }
101                i += 1;
102            }
103            continue;
104        }
105
106        // Double-quoted identifier: "..." with "" escape.
107        if bytes[i] == b'"' {
108            skip[i] = true;
109            i += 1;
110            while i < len {
111                skip[i] = true;
112                if bytes[i] == b'"' {
113                    if i + 1 < len && bytes[i + 1] == b'"' {
114                        i += 1;
115                        skip[i] = true;
116                        i += 1;
117                        continue;
118                    }
119                    i += 1;
120                    break;
121                }
122                i += 1;
123            }
124            continue;
125        }
126
127        // Block comment: /* ... */
128        if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
129            skip[i] = true;
130            skip[i + 1] = true;
131            i += 2;
132            while i < len {
133                skip[i] = true;
134                if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
135                    skip[i + 1] = true;
136                    i += 2;
137                    break;
138                }
139                i += 1;
140            }
141            continue;
142        }
143
144        // Line comment: -- to end of line.
145        if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
146            skip[i] = true;
147            skip[i + 1] = true;
148            i += 2;
149            while i < len && bytes[i] != b'\n' {
150                skip[i] = true;
151                i += 1;
152            }
153            continue;
154        }
155
156        i += 1;
157    }
158
159    skip
160}