Skip to main content

sqrust_rules/structure/
lateral_join.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3/// Flag `LATERAL` keyword in FROM clauses. LATERAL joins/subqueries are not
4/// universally supported — they work in PostgreSQL and MySQL 8.0+ but are not
5/// supported in SQL Server or older databases.
6pub struct LateralJoin;
7
8impl Rule for LateralJoin {
9    fn name(&self) -> &'static str {
10        "Structure/LateralJoin"
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 keyword = b"LATERAL";
28    let kw_len = keyword.len();
29    let mut diags = Vec::new();
30    let mut i = 0;
31
32    while i + kw_len <= len {
33        if skip[i] {
34            i += 1;
35            continue;
36        }
37
38        // Word boundary before.
39        let before_ok = i == 0 || !is_word_char(bytes[i - 1]);
40        if !before_ok {
41            i += 1;
42            continue;
43        }
44
45        // Match LATERAL (case-insensitive).
46        let matches = bytes[i..i + kw_len]
47            .iter()
48            .zip(keyword.iter())
49            .all(|(a, b)| a.eq_ignore_ascii_case(b));
50
51        if !matches {
52            i += 1;
53            continue;
54        }
55
56        // Ensure none of those bytes are skipped.
57        if (i..i + kw_len).any(|k| skip[k]) {
58            i += 1;
59            continue;
60        }
61
62        // Word boundary after.
63        let after = i + kw_len;
64        let after_ok = after >= len || !is_word_char(bytes[after]);
65
66        if after_ok {
67            let (line, col) = offset_to_line_col(source, i);
68            diags.push(Diagnostic {
69                rule: rule_name,
70                message: "LATERAL join is not supported in all databases (unsupported in SQL Server); verify dialect compatibility".to_string(),
71                line,
72                col,
73            });
74            i = after;
75        } else {
76            i += 1;
77        }
78    }
79
80    diags
81}
82
83#[inline]
84fn is_word_char(ch: u8) -> bool {
85    ch.is_ascii_alphanumeric() || ch == b'_'
86}
87
88/// Converts a byte offset to a 1-indexed (line, col) pair.
89fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
90    let before = &source[..offset];
91    let line = before.chars().filter(|&c| c == '\n').count() + 1;
92    let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
93    (line, col)
94}
95
96/// Build a boolean skip-set: `skip[i] == true` means byte `i` is inside a
97/// single-quoted string, double-quoted identifier, block comment, or line
98/// comment.
99fn build_skip_set(bytes: &[u8], len: usize) -> Vec<bool> {
100    let mut skip = vec![false; len];
101    let mut i = 0;
102
103    while i < len {
104        // Single-quoted string: '...' with '' escape.
105        if bytes[i] == b'\'' {
106            skip[i] = true;
107            i += 1;
108            while i < len {
109                skip[i] = true;
110                if bytes[i] == b'\'' {
111                    if i + 1 < len && bytes[i + 1] == b'\'' {
112                        i += 1;
113                        skip[i] = true;
114                        i += 1;
115                        continue;
116                    }
117                    i += 1;
118                    break;
119                }
120                i += 1;
121            }
122            continue;
123        }
124
125        // Double-quoted identifier: "..." with "" escape.
126        if bytes[i] == b'"' {
127            skip[i] = true;
128            i += 1;
129            while i < len {
130                skip[i] = true;
131                if bytes[i] == b'"' {
132                    if i + 1 < len && bytes[i + 1] == b'"' {
133                        i += 1;
134                        skip[i] = true;
135                        i += 1;
136                        continue;
137                    }
138                    i += 1;
139                    break;
140                }
141                i += 1;
142            }
143            continue;
144        }
145
146        // Block comment: /* ... */
147        if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
148            skip[i] = true;
149            skip[i + 1] = true;
150            i += 2;
151            while i < len {
152                skip[i] = true;
153                if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
154                    skip[i + 1] = true;
155                    i += 2;
156                    break;
157                }
158                i += 1;
159            }
160            continue;
161        }
162
163        // Line comment: -- to end of line.
164        if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
165            skip[i] = true;
166            skip[i + 1] = true;
167            i += 2;
168            while i < len && bytes[i] != b'\n' {
169                skip[i] = true;
170                i += 1;
171            }
172            continue;
173        }
174
175        i += 1;
176    }
177
178    skip
179}