sql_lsp/dialects/
redis.rs

1use crate::dialect::Dialect;
2use crate::schema::Schema;
3use async_trait::async_trait;
4use tower_lsp::lsp_types::{
5    CompletionItem, CompletionItemKind, Diagnostic, DiagnosticSeverity, Hover, Location,
6    MarkedString, NumberOrString, Position, Range,
7};
8
9pub struct RedisDialect;
10
11impl Default for RedisDialect {
12    fn default() -> Self {
13        Self::new()
14    }
15}
16
17impl RedisDialect {
18    pub fn new() -> Self {
19        Self
20    }
21}
22
23#[async_trait]
24impl Dialect for RedisDialect {
25    fn name(&self) -> &str {
26        "redis"
27    }
28
29    async fn parse(&self, sql: &str, _schema: Option<&Schema>) -> Vec<Diagnostic> {
30        let mut diagnostics = Vec::new();
31
32        if sql.trim().is_empty() {
33            return diagnostics;
34        }
35
36        // Redis 查询语言(RediSearch/RedisGraph)特定的语法检查
37        // Redis 支持多种查询语言,这里以 RediSearch 为例
38        if sql.to_uppercase().contains("FT.SEARCH") && !sql.contains("@") {
39            diagnostics.push(Diagnostic {
40                range: Range {
41                    start: Position {
42                        line: 0,
43                        character: 0,
44                    },
45                    end: Position {
46                        line: 0,
47                        character: sql.len() as u32,
48                    },
49                },
50                severity: Some(DiagnosticSeverity::WARNING),
51                code: Some(NumberOrString::String("REDIS001".to_string())),
52                code_description: None,
53                source: Some("redis".to_string()),
54                message: "FT.SEARCH query might benefit from field filters using @field"
55                    .to_string(),
56                related_information: None,
57                tags: None,
58                data: None,
59            });
60        }
61
62        diagnostics
63    }
64
65    async fn completion(
66        &self,
67        _sql: &str,
68        _position: Position,
69        schema: Option<&Schema>,
70    ) -> Vec<CompletionItem> {
71        let mut items = Vec::new();
72
73        // Redis 基础命令
74        let basic_commands = vec![
75            "SET",
76            "GET",
77            "DEL",
78            "EXISTS",
79            "EXPIRE",
80            "TTL",
81            "PERSIST",
82            "HSET",
83            "HGET",
84            "HGETALL",
85            "HDEL",
86            "HKEYS",
87            "HVALS",
88            "HINCRBY",
89            "LPUSH",
90            "RPUSH",
91            "LPOP",
92            "RPOP",
93            "LLEN",
94            "LRANGE",
95            "LINDEX",
96            "SADD",
97            "SMEMBERS",
98            "SREM",
99            "SCARD",
100            "SISMEMBER",
101            "SINTER",
102            "ZADD",
103            "ZRANGE",
104            "ZREM",
105            "ZCARD",
106            "ZSCORE",
107            "ZRANK",
108        ];
109
110        // RediSearch 命令
111        let search_commands = [
112            "FT.SEARCH",
113            "FT.AGGREGATE",
114            "FT.CREATE",
115            "FT.DROPINDEX",
116            "FT.INFO",
117            "FT.ALIASADD",
118            "FT.ALIASDEL",
119            "FT.ALIASUPDATE",
120            "FT.SUGADD",
121            "FT.SUGGET",
122            "FT.SUGDEL",
123            "FT.SUGLEN",
124        ];
125
126        // RedisGraph 命令
127        let graph_commands = [
128            "GRAPH.QUERY",
129            "GRAPH.DELETE",
130            "GRAPH.EXPLAIN",
131            "GRAPH.PROFILE",
132        ];
133
134        // RedisJSON 命令
135        let json_commands = [
136            "JSON.GET",
137            "JSON.SET",
138            "JSON.DEL",
139            "JSON.MGET",
140            "JSON.KEYS",
141            "JSON.ARRAPPEND",
142            "JSON.ARRINDEX",
143            "JSON.ARRINSERT",
144            "JSON.ARRLEN",
145            "JSON.ARRPOP",
146            "JSON.OBJKEYS",
147            "JSON.OBJLEN",
148        ];
149
150        let commands: Vec<&str> = basic_commands
151            .iter()
152            .chain(search_commands.iter())
153            .chain(graph_commands.iter())
154            .chain(json_commands.iter())
155            .copied()
156            .collect();
157
158        for cmd in commands {
159            let (kind, detail_prefix) = if cmd.starts_with("FT.") {
160                (CompletionItemKind::FUNCTION, "RediSearch command")
161            } else if cmd.starts_with("GRAPH.") {
162                (CompletionItemKind::FUNCTION, "RedisGraph command")
163            } else if cmd.starts_with("JSON.") {
164                (CompletionItemKind::FUNCTION, "RedisJSON command")
165            } else {
166                (CompletionItemKind::FUNCTION, "Redis command")
167            };
168
169            items.push(CompletionItem {
170                label: cmd.to_string(),
171                kind: Some(kind),
172                detail: Some(format!("{}: {}", detail_prefix, cmd)),
173                documentation: None,
174                deprecated: None,
175                preselect: None,
176                sort_text: Some(format!("0{}", cmd)),
177                filter_text: None,
178                insert_text: Some(cmd.to_string()),
179                insert_text_format: None,
180                text_edit: None,
181                additional_text_edits: None,
182                commit_characters: None,
183                command: None,
184                data: None,
185                tags: None,
186                insert_text_mode: None,
187                label_details: None,
188            });
189        }
190
191        // RediSearch 查询语法关键字和操作符
192        // 注意:Redis 是命令式语言,这些是 RediSearch 查询字符串中的操作符
193        let keywords = vec![
194            "@",   // 字段前缀,如 @field:value
195            "AND", // 逻辑与
196            "OR",  // 逻辑或
197            "NOT", // 逻辑非
198            "-",   // 排除操作符
199            "+",   // 必须包含
200            "~",   // 模糊匹配
201            "|",   // 或操作符(在聚合中使用)
202            "(",   // 分组开始
203            ")",   // 分组结束
204            "{",   // 范围查询开始
205            "}",   // 范围查询结束
206            "[",   // 范围查询开始(包含)
207            "]",   // 范围查询结束(包含)
208        ];
209
210        for keyword in keywords {
211            let detail = match keyword {
212                "@" => "Field prefix for RediSearch queries (e.g., @field:value)",
213                "AND" => "Logical AND operator",
214                "OR" => "Logical OR operator",
215                "NOT" => "Logical NOT operator",
216                "-" => "Exclude operator (must not contain)",
217                "+" => "Must contain operator",
218                "~" => "Fuzzy match operator",
219                "|" => "OR operator (used in aggregations)",
220                "(" | ")" => "Grouping parentheses",
221                "{" | "}" => "Range query braces (exclusive)",
222                "[" | "]" => "Range query brackets (inclusive)",
223                _ => "RediSearch query operator",
224            };
225
226            items.push(CompletionItem {
227                label: keyword.to_string(),
228                kind: Some(CompletionItemKind::OPERATOR),
229                detail: Some(detail.to_string()),
230                documentation: None,
231                deprecated: None,
232                preselect: None,
233                sort_text: Some(format!("1{}", keyword)),
234                filter_text: None,
235                insert_text: Some(keyword.to_string()),
236                insert_text_format: None,
237                insert_text_mode: None,
238                text_edit: None,
239                additional_text_edits: None,
240                commit_characters: None,
241                command: None,
242                data: None,
243                tags: None,
244                label_details: None,
245            });
246        }
247
248        if let Some(schema) = schema {
249            for table in &schema.tables {
250                items.push(CompletionItem {
251                    label: table.name.clone(),
252                    kind: Some(CompletionItemKind::CLASS),
253                    detail: Some(format!("Redis Index/Key: {}", table.name)),
254                    documentation: table
255                        .comment
256                        .clone()
257                        .map(tower_lsp::lsp_types::Documentation::String),
258                    deprecated: None,
259                    preselect: None,
260                    sort_text: Some(format!("2{}", table.name)),
261                    filter_text: None,
262                    insert_text: Some(table.name.clone()),
263                    insert_text_format: None,
264                    insert_text_mode: None,
265                    text_edit: None,
266                    additional_text_edits: None,
267                    commit_characters: None,
268                    command: None,
269                    data: None,
270                    tags: None,
271                    label_details: None,
272                });
273            }
274        }
275
276        items
277    }
278
279    async fn hover(
280        &self,
281        sql: &str,
282        _position: Position,
283        schema: Option<&Schema>,
284    ) -> Option<Hover> {
285        if let Some(schema) = schema {
286            for table in &schema.tables {
287                if sql.contains(&table.name) {
288                    return Some(Hover {
289                        contents: tower_lsp::lsp_types::HoverContents::Scalar(
290                            MarkedString::String(format!(
291                                "Redis Index/Key: {}\n{}",
292                                table.name,
293                                table.comment.as_deref().unwrap_or("No description")
294                            )),
295                        ),
296                        range: None,
297                    });
298                }
299            }
300        }
301        None
302    }
303
304    async fn goto_definition(
305        &self,
306        _sql: &str,
307        _position: Position,
308        _schema: Option<&Schema>,
309    ) -> Option<Location> {
310        None
311    }
312
313    async fn references(
314        &self,
315        _sql: &str,
316        _position: Position,
317        _schema: Option<&Schema>,
318    ) -> Vec<Location> {
319        Vec::new()
320    }
321
322    async fn format(&self, sql: &str) -> String {
323        sql.split_whitespace().collect::<Vec<_>>().join(" ")
324    }
325
326    async fn validate(&self, sql: &str, schema: Option<&Schema>) -> Vec<Diagnostic> {
327        self.parse(sql, schema).await
328    }
329}