sql_lsp/dialects/
clickhouse.rs

1use crate::dialect::Dialect;
2use crate::parser::SqlParser;
3use crate::schema::Schema;
4use async_trait::async_trait;
5use tower_lsp::lsp_types::{
6    CompletionItem, CompletionItemKind, Diagnostic, Hover, Location, MarkedString, Position,
7};
8
9pub struct ClickHouseDialect {
10    parser: std::sync::Mutex<SqlParser>,
11}
12
13impl Default for ClickHouseDialect {
14    fn default() -> Self {
15        Self::new()
16    }
17}
18
19impl ClickHouseDialect {
20    pub fn new() -> Self {
21        Self {
22            parser: std::sync::Mutex::new(SqlParser::new()),
23        }
24    }
25
26    fn create_keyword_item(&self, keyword: &str) -> CompletionItem {
27        CompletionItem {
28            label: keyword.to_string(),
29            kind: Some(CompletionItemKind::KEYWORD),
30            detail: Some(format!("ClickHouse keyword: {}", keyword)),
31            documentation: None,
32            deprecated: None,
33            preselect: None,
34            sort_text: Some(format!("0{}", keyword)),
35            filter_text: None,
36            insert_text: Some(keyword.to_string()),
37            insert_text_format: None,
38            insert_text_mode: None,
39            text_edit: None,
40            additional_text_edits: None,
41            commit_characters: None,
42            command: None,
43            data: None,
44            tags: None,
45            label_details: None,
46        }
47    }
48
49    fn create_table_item(&self, table: &crate::schema::Table) -> CompletionItem {
50        CompletionItem {
51            label: table.name.clone(),
52            kind: Some(CompletionItemKind::CLASS),
53            detail: Some(format!("Table: {}", table.name)),
54            documentation: table
55                .comment
56                .clone()
57                .map(tower_lsp::lsp_types::Documentation::String),
58            deprecated: None,
59            preselect: None,
60            sort_text: Some(format!("1{}", table.name)),
61            filter_text: None,
62            insert_text: Some(table.name.clone()),
63            insert_text_format: None,
64            insert_text_mode: None,
65            text_edit: None,
66            additional_text_edits: None,
67            commit_characters: None,
68            command: None,
69            data: None,
70            tags: None,
71            label_details: None,
72        }
73    }
74
75    fn create_column_item(
76        &self,
77        column: &crate::schema::Column,
78        table_name: Option<&str>,
79    ) -> CompletionItem {
80        let label = if let Some(table) = table_name {
81            format!("{}.{}", table, column.name)
82        } else {
83            column.name.clone()
84        };
85
86        CompletionItem {
87            label,
88            kind: Some(CompletionItemKind::FIELD),
89            detail: Some(format!("Column: {} ({})", column.name, column.data_type)),
90            documentation: column
91                .comment
92                .clone()
93                .map(tower_lsp::lsp_types::Documentation::String),
94            deprecated: None,
95            preselect: None,
96            sort_text: Some(format!("2{}", column.name)),
97            filter_text: None,
98            insert_text: Some(column.name.clone()),
99            insert_text_format: None,
100            insert_text_mode: None,
101            text_edit: None,
102            additional_text_edits: None,
103            commit_characters: None,
104            command: None,
105            data: None,
106            tags: None,
107            label_details: None,
108        }
109    }
110}
111
112#[async_trait]
113impl Dialect for ClickHouseDialect {
114    fn name(&self) -> &str {
115        "clickhouse"
116    }
117
118    async fn parse(&self, sql: &str, _schema: Option<&Schema>) -> Vec<Diagnostic> {
119        let mut parser = self.parser.lock().unwrap();
120        let parse_result = parser.parse(sql);
121        parse_result.diagnostics
122    }
123
124    async fn completion(
125        &self,
126        sql: &str,
127        position: Position,
128        schema: Option<&Schema>,
129    ) -> Vec<CompletionItem> {
130        let mut parser = self.parser.lock().unwrap();
131        let parse_result = parser.parse(sql);
132
133        let context = if let Some(tree) = &parse_result.tree {
134            if let Some(node) = parser.get_node_at_position(tree, position) {
135                parser.analyze_completion_context(node, sql, position)
136            } else {
137                crate::parser::CompletionContext::Default
138            }
139        } else {
140            crate::parser::CompletionContext::Default
141        };
142
143        let mut items = Vec::new();
144        let keywords = &[
145            "SELECT",
146            "FROM",
147            "WHERE",
148            "INSERT",
149            "INTO",
150            "VALUES",
151            "CREATE",
152            "DROP",
153            "ALTER",
154            "TABLE",
155            "DATABASE",
156            "ENGINE",
157            "MergeTree",
158            "ReplacingMergeTree",
159            "SummingMergeTree",
160            "AggregatingMergeTree",
161            "CollapsingMergeTree",
162            "VersionedCollapsingMergeTree",
163            "JOIN",
164            "INNER",
165            "LEFT",
166            "RIGHT",
167            "FULL",
168            "OUTER",
169            "ON",
170            "GROUP",
171            "BY",
172            "ORDER",
173            "HAVING",
174            "LIMIT",
175            "OFFSET",
176            "UNION",
177            "ALL",
178            "DISTINCT",
179            "AS",
180            "AND",
181            "OR",
182            "NOT",
183            "IN",
184            "LIKE",
185            "ILIKE",
186            "BETWEEN",
187            "IS",
188            "NULL",
189            "CAST",
190            "ARRAY",
191            "TUPLE",
192            "MAP",
193            "Nested",
194            "AggregateFunction",
195            "Array",
196            "String",
197            "Int8",
198            "Int16",
199            "Int32",
200            "Int64",
201            "UInt8",
202            "UInt16",
203            "UInt32",
204            "UInt64",
205            "Float32",
206            "Float64",
207            "Date",
208            "DateTime",
209        ];
210
211        match context {
212            crate::parser::CompletionContext::FromClause
213            | crate::parser::CompletionContext::JoinClause => {
214                let join_keywords: Vec<&str> = keywords
215                    .iter()
216                    .filter(|&&k| matches!(k, "JOIN" | "INNER" | "LEFT" | "RIGHT" | "OUTER" | "ON"))
217                    .copied()
218                    .collect();
219                for keyword in join_keywords {
220                    items.push(self.create_keyword_item(keyword));
221                }
222                if let Some(schema) = schema {
223                    for table in &schema.tables {
224                        items.push(self.create_table_item(table));
225                    }
226                }
227            }
228            crate::parser::CompletionContext::SelectClause => {
229                let select_keywords: Vec<&str> = keywords
230                    .iter()
231                    .filter(|&&k| matches!(k, "SELECT" | "DISTINCT" | "AS" | "FROM"))
232                    .copied()
233                    .collect();
234                for keyword in select_keywords {
235                    items.push(self.create_keyword_item(keyword));
236                }
237                if let Some(schema) = schema {
238                    for table in &schema.tables {
239                        for column in &table.columns {
240                            items.push(self.create_column_item(
241                                column,
242                                Some(&format!("{}.{}", schema.database, table.name)),
243                            ));
244                        }
245                    }
246                }
247            }
248            crate::parser::CompletionContext::WhereClause => {
249                let where_keywords: Vec<&str> = keywords
250                    .iter()
251                    .filter(|&&k| {
252                        matches!(
253                            k,
254                            "AND"
255                                | "OR"
256                                | "NOT"
257                                | "IN"
258                                | "LIKE"
259                                | "ILIKE"
260                                | "BETWEEN"
261                                | "IS"
262                                | "NULL"
263                        )
264                    })
265                    .copied()
266                    .collect();
267                for keyword in where_keywords {
268                    items.push(self.create_keyword_item(keyword));
269                }
270                let operators = vec!["=", "<>", "!=", ">", "<", ">=", "<="];
271                for op in operators {
272                    items.push(CompletionItem {
273                        label: op.to_string(),
274                        kind: Some(CompletionItemKind::OPERATOR),
275                        detail: Some(format!("Operator: {}", op)),
276                        documentation: None,
277                        deprecated: None,
278                        preselect: None,
279                        sort_text: Some(format!("1{}", op)),
280                        filter_text: None,
281                        insert_text: Some(op.to_string()),
282                        insert_text_format: None,
283                        insert_text_mode: None,
284                        text_edit: None,
285                        additional_text_edits: None,
286                        commit_characters: None,
287                        command: None,
288                        data: None,
289                        tags: None,
290                        label_details: None,
291                    });
292                }
293                if let Some(schema) = schema {
294                    for table in &schema.tables {
295                        for column in &table.columns {
296                            items.push(self.create_column_item(
297                                column,
298                                Some(&format!("{}.{}", schema.database, table.name)),
299                            ));
300                        }
301                    }
302                }
303            }
304            crate::parser::CompletionContext::OrderByClause
305            | crate::parser::CompletionContext::GroupByClause => {
306                let keywords_list: Vec<&str> = keywords
307                    .iter()
308                    .filter(|&&k| matches!(k, "ASC" | "DESC" | "BY"))
309                    .copied()
310                    .collect();
311                for keyword in keywords_list {
312                    items.push(self.create_keyword_item(keyword));
313                }
314                if let Some(schema) = schema {
315                    for table in &schema.tables {
316                        for column in &table.columns {
317                            items.push(self.create_column_item(
318                                column,
319                                Some(&format!("{}.{}", schema.database, table.name)),
320                            ));
321                        }
322                    }
323                }
324            }
325            crate::parser::CompletionContext::HavingClause => {
326                let having_keywords: Vec<&str> = keywords
327                    .iter()
328                    .filter(|&&k| {
329                        matches!(
330                            k,
331                            "AND"
332                                | "OR"
333                                | "NOT"
334                                | "IN"
335                                | "LIKE"
336                                | "ILIKE"
337                                | "BETWEEN"
338                                | "IS"
339                                | "NULL"
340                        )
341                    })
342                    .copied()
343                    .collect();
344                for keyword in having_keywords {
345                    items.push(self.create_keyword_item(keyword));
346                }
347                let aggregate_functions = vec!["COUNT", "SUM", "AVG", "MIN", "MAX"];
348                for func in aggregate_functions {
349                    items.push(self.create_keyword_item(func));
350                }
351                if let Some(schema) = schema {
352                    for table in &schema.tables {
353                        for column in &table.columns {
354                            items.push(self.create_column_item(
355                                column,
356                                Some(&format!("{}.{}", schema.database, table.name)),
357                            ));
358                        }
359                    }
360                }
361            }
362            crate::parser::CompletionContext::TableColumn => {
363                if let Some(tree) = &parse_result.tree {
364                    if let Some(node) = parser.get_node_at_position(tree, position) {
365                        if let Some(table_name) = parser.get_table_name_for_column(node, sql) {
366                            if let Some(schema) = schema {
367                                if let Some(table) = schema.tables.iter().find(|t| {
368                                    t.name == table_name
369                                        || format!("{}.{}", schema.database, t.name) == table_name
370                                }) {
371                                    for column in &table.columns {
372                                        items.push(self.create_column_item(column, None));
373                                    }
374                                }
375                            }
376                        }
377                    }
378                }
379            }
380            crate::parser::CompletionContext::Default => {
381                for keyword in keywords {
382                    items.push(self.create_keyword_item(keyword));
383                }
384                if let Some(schema) = schema {
385                    for table in &schema.tables {
386                        items.push(self.create_table_item(table));
387                    }
388                }
389            }
390        }
391
392        items
393    }
394
395    async fn hover(
396        &self,
397        sql: &str,
398        _position: Position,
399        schema: Option<&Schema>,
400    ) -> Option<Hover> {
401        if let Some(schema) = schema {
402            for table in &schema.tables {
403                if sql.contains(&table.name) {
404                    return Some(Hover {
405                        contents: tower_lsp::lsp_types::HoverContents::Scalar(
406                            MarkedString::String(format!(
407                                "ClickHouse Table: {}.{}\n{}",
408                                schema.database,
409                                table.name,
410                                table.comment.as_deref().unwrap_or("No description")
411                            )),
412                        ),
413                        range: None,
414                    });
415                }
416            }
417        }
418        None
419    }
420
421    async fn goto_definition(
422        &self,
423        _sql: &str,
424        _position: Position,
425        _schema: Option<&Schema>,
426    ) -> Option<Location> {
427        None
428    }
429
430    async fn references(
431        &self,
432        _sql: &str,
433        _position: Position,
434        _schema: Option<&Schema>,
435    ) -> Vec<Location> {
436        Vec::new()
437    }
438
439    async fn format(&self, sql: &str) -> String {
440        sql.split_whitespace().collect::<Vec<_>>().join(" ")
441    }
442
443    async fn validate(&self, sql: &str, schema: Option<&Schema>) -> Vec<Diagnostic> {
444        self.parse(sql, schema).await
445    }
446}