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 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 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 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 let graph_commands = [
128 "GRAPH.QUERY",
129 "GRAPH.DELETE",
130 "GRAPH.EXPLAIN",
131 "GRAPH.PROFILE",
132 ];
133
134 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 let keywords = vec![
194 "@", "AND", "OR", "NOT", "-", "+", "~", "|", "(", ")", "{", "}", "[", "]", ];
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}