Skip to main content

normalize_languages/
sql.rs

1//! SQL language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// SQL language support.
9pub struct Sql;
10
11impl Language for Sql {
12    fn name(&self) -> &'static str {
13        "SQL"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["sql"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "sql"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &["create_table", "create_view", "create_schema"]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &["create_function"]
32    }
33
34    fn type_kinds(&self) -> &'static [&'static str] {
35        &["create_type"]
36    }
37
38    fn import_kinds(&self) -> &'static [&'static str] {
39        &[]
40    }
41
42    fn public_symbol_kinds(&self) -> &'static [&'static str] {
43        &["create_table", "create_view", "create_function"]
44    }
45
46    fn visibility_mechanism(&self) -> VisibilityMechanism {
47        VisibilityMechanism::NotApplicable
48    }
49
50    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51        let name = match self.extract_sql_name(node, content) {
52            Some(n) => n,
53            None => return Vec::new(),
54        };
55
56        let kind = match node.kind() {
57            "create_table" => SymbolKind::Struct,
58            "create_view" | "create_materialized_view" => SymbolKind::Struct,
59            "create_function" => SymbolKind::Function,
60            "create_type" => SymbolKind::Type,
61            "create_index" => SymbolKind::Variable,
62            _ => return Vec::new(),
63        };
64
65        vec![Export {
66            name,
67            kind,
68            line: node.start_position().row + 1,
69        }]
70    }
71
72    fn scope_creating_kinds(&self) -> &'static [&'static str] {
73        &["subquery", "cte"]
74    }
75
76    fn control_flow_kinds(&self) -> &'static [&'static str] {
77        &["case"]
78    }
79
80    fn complexity_nodes(&self) -> &'static [&'static str] {
81        &["case", "join", "where", "having"]
82    }
83
84    fn nesting_nodes(&self) -> &'static [&'static str] {
85        &["subquery", "case"]
86    }
87
88    fn signature_suffix(&self) -> &'static str {
89        ""
90    }
91
92    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
93        let name = self.extract_sql_name(node, content)?;
94
95        // Extract first line as signature
96        let text = &content[node.byte_range()];
97        let first_line = text.lines().next().unwrap_or(text);
98
99        Some(Symbol {
100            name,
101            kind: SymbolKind::Function,
102            signature: first_line.trim().to_string(),
103            docstring: self.extract_docstring(node, content),
104            attributes: Vec::new(),
105            start_line: node.start_position().row + 1,
106            end_line: node.end_position().row + 1,
107            visibility: Visibility::Public,
108            children: Vec::new(),
109            is_interface_impl: false,
110            implements: Vec::new(),
111        })
112    }
113
114    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
115        let name = self.extract_sql_name(node, content)?;
116        let (kind, keyword) = match node.kind() {
117            "create_view" | "create_materialized_view" => (SymbolKind::Struct, "VIEW"),
118            "create_schema" => (SymbolKind::Module, "SCHEMA"),
119            _ => (SymbolKind::Struct, "TABLE"),
120        };
121
122        Some(Symbol {
123            name: name.clone(),
124            kind,
125            signature: format!("CREATE {} {}", keyword, name),
126            docstring: self.extract_docstring(node, content),
127            attributes: Vec::new(),
128            start_line: node.start_position().row + 1,
129            end_line: node.end_position().row + 1,
130            visibility: Visibility::Public,
131            children: Vec::new(),
132            is_interface_impl: false,
133            implements: Vec::new(),
134        })
135    }
136
137    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
138        let name = self.extract_sql_name(node, content)?;
139
140        Some(Symbol {
141            name: name.clone(),
142            kind: SymbolKind::Type,
143            signature: format!("CREATE TYPE {}", name),
144            docstring: None,
145            attributes: Vec::new(),
146            start_line: node.start_position().row + 1,
147            end_line: node.end_position().row + 1,
148            visibility: Visibility::Public,
149            children: Vec::new(),
150            is_interface_impl: false,
151            implements: Vec::new(),
152        })
153    }
154
155    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
156        // SQL uses -- for comments
157        let mut prev = node.prev_sibling();
158        let mut doc_lines = Vec::new();
159
160        while let Some(sibling) = prev {
161            let text = &content[sibling.byte_range()];
162            if sibling.kind() == "comment" && text.starts_with("--") {
163                let line = text.strip_prefix("--").unwrap_or(text).trim();
164                doc_lines.push(line.to_string());
165                prev = sibling.prev_sibling();
166            } else {
167                break;
168            }
169        }
170
171        if doc_lines.is_empty() {
172            return None;
173        }
174
175        doc_lines.reverse();
176        Some(doc_lines.join(" "))
177    }
178
179    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
180        Vec::new()
181    }
182
183    fn extract_imports(&self, _node: &Node, _content: &str) -> Vec<Import> {
184        Vec::new()
185    }
186
187    fn format_import(&self, _import: &Import, _names: Option<&[&str]>) -> String {
188        // SQL has no imports
189        String::new()
190    }
191
192    fn is_public(&self, _node: &Node, _content: &str) -> bool {
193        true
194    }
195    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
196        Visibility::Public
197    }
198
199    fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
200        false
201    }
202
203    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
204        None
205    }
206
207    fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
208        None
209    }
210    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
211        false
212    }
213    fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
214        None
215    }
216
217    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
218        let ext = path.extension()?.to_str()?;
219        if ext != "sql" {
220            return None;
221        }
222        let stem = path.file_stem()?.to_str()?;
223        Some(stem.to_string())
224    }
225
226    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
227        vec![format!("{}.sql", module)]
228    }
229
230    fn lang_key(&self) -> &'static str {
231        "sql"
232    }
233
234    fn is_stdlib_import(&self, _import_name: &str, _project_root: &Path) -> bool {
235        false
236    }
237    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
238        None
239    }
240    fn resolve_local_import(
241        &self,
242        _import: &str,
243        _current_file: &Path,
244        _project_root: &Path,
245    ) -> Option<PathBuf> {
246        None
247    }
248    fn resolve_external_import(
249        &self,
250        _import_name: &str,
251        _project_root: &Path,
252    ) -> Option<ResolvedPackage> {
253        None
254    }
255    fn get_version(&self, _project_root: &Path) -> Option<String> {
256        None
257    }
258    fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
259        None
260    }
261    fn indexable_extensions(&self) -> &'static [&'static str] {
262        &["sql"]
263    }
264    fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
265        Vec::new()
266    }
267
268    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
269        use crate::traits::{has_extension, skip_dotfiles};
270        if skip_dotfiles(name) {
271            return true;
272        }
273        !is_dir && !has_extension(name, self.indexable_extensions())
274    }
275
276    fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
277        Vec::new()
278    }
279
280    fn package_module_name(&self, entry_name: &str) -> String {
281        entry_name
282            .strip_suffix(".sql")
283            .unwrap_or(entry_name)
284            .to_string()
285    }
286
287    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
288        if path.is_file() {
289            Some(path.to_path_buf())
290        } else {
291            None
292        }
293    }
294}
295
296impl Sql {
297    fn extract_sql_name(&self, node: &Node, content: &str) -> Option<String> {
298        // Look for identifier after CREATE TABLE/VIEW/FUNCTION etc.
299        let mut cursor = node.walk();
300        let mut found_create = false;
301        for child in node.children(&mut cursor) {
302            if child.kind() == "keyword" {
303                let text = &content[child.byte_range()].to_uppercase();
304                if text == "CREATE" {
305                    found_create = true;
306                }
307            }
308            if found_create && (child.kind() == "identifier" || child.kind() == "object_reference")
309            {
310                return Some(content[child.byte_range()].to_string());
311            }
312        }
313        None
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::validate_unused_kinds_audit;
321
322    #[test]
323    fn unused_node_kinds_audit() {
324        #[rustfmt::skip]
325        let documented_unused: &[&str] = &[
326            "alter_type", "array_size_definition", "between_expression", "binary_expression",
327            "block", "column_definition", "column_definitions", "comment_statement",
328            "drop_function", "drop_type", "enum", "enum_elements", "filter_expression",
329            "frame_definition", "function_argument", "function_arguments",
330            "function_body", "function_cost", "function_declaration", "function_language",
331            "function_leakproof", "function_rows", "function_safety", "function_security",
332            "function_strictness", "function_support", "function_volatility", "identifier",
333            "keyword_before", "keyword_case", "keyword_else", "keyword_enum",
334            "keyword_except", "keyword_for", "keyword_force", "keyword_force_not_null",
335            "keyword_force_null", "keyword_force_quote", "keyword_foreign",
336            "keyword_format", "keyword_function", "keyword_geometry", "keyword_if",
337            "keyword_match", "keyword_matched", "keyword_modify", "keyword_regclass",
338            "keyword_regtype", "keyword_return", "keyword_returning", "keyword_returns",
339            "keyword_statement", "keyword_type", "keyword_while", "keyword_with",
340            "keyword_without", "modify_column", "parenthesized_expression",
341            "reset_statement", "returning", "row_format", "select_expression",
342            "set_statement", "statement", "unary_expression", "var_declaration",
343            "var_declarations", "when_clause", "while_statement", "window_clause",
344            "window_function", "window_specification",
345        ];
346        validate_unused_kinds_audit(&Sql, documented_unused)
347            .expect("SQL unused node kinds audit failed");
348    }
349}