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 ElasticsearchEqlDialect;
11
12impl Default for ElasticsearchEqlDialect {
13 fn default() -> Self {
14 Self::new()
15 }
16}
17
18impl ElasticsearchEqlDialect {
19 pub fn new() -> Self {
20 Self
21 }
22}
23
24#[async_trait]
25impl Dialect for ElasticsearchEqlDialect {
26 fn name(&self) -> &str {
27 "elasticsearch-eql"
28 }
29
30 async fn parse(&self, sql: &str, _schema: Option<&Schema>) -> Vec<Diagnostic> {
31 let mut diagnostics = Vec::new();
32
33 if sql.trim().is_empty() {
34 return diagnostics;
35 }
36
37 let upper_sql = sql.to_uppercase();
40 if upper_sql.contains("SEQUENCE")
41 || upper_sql.contains("JOIN")
42 || upper_sql.contains("EVENT")
43 {
44 if upper_sql.contains("SEQUENCE") && !upper_sql.contains("WHERE") {
46 diagnostics.push(Diagnostic {
47 range: Range {
48 start: Position {
49 line: 0,
50 character: 0,
51 },
52 end: Position {
53 line: 0,
54 character: sql.len() as u32,
55 },
56 },
57 severity: Some(DiagnosticSeverity::WARNING),
58 code: Some(NumberOrString::String("EQL001".to_string())),
59 code_description: None,
60 source: Some("elasticsearch-eql".to_string()),
61 message: "EQL SEQUENCE query should typically include a WHERE clause"
62 .to_string(),
63 related_information: None,
64 tags: None,
65 data: None,
66 });
67 }
68 }
69
70 diagnostics
71 }
72
73 async fn completion(
74 &self,
75 _sql: &str,
76 _position: Position,
77 schema: Option<&Schema>,
78 ) -> Vec<CompletionItem> {
79 let mut items = Vec::new();
80
81 let keywords = vec![
83 "sequence",
84 "join",
85 "event",
86 "where",
87 "with",
88 "maxspan",
89 "until",
90 "by",
91 "any",
92 "all",
93 "in",
94 "not",
95 "and",
96 "or",
97 "true",
98 "false",
99 "null",
100 "length",
101 "start",
102 "end",
103 "key",
104 "value",
105 "count",
106 "head",
107 "tail",
108 "array",
109 "array_length",
110 "array_search",
111 "array_sort",
112 "array_unique",
113 "cidr_match",
114 "concat",
115 "ends_with",
116 "index_of",
117 "length",
118 "match",
119 "number",
120 "starts_with",
121 "string",
122 "string_contains",
123 "string_ends_with",
124 "string_starts_with",
125 "substring",
126 "wildcard",
127 "between",
128 "in",
129 "is_null",
130 "is_not_null",
131 "like",
132 "not_like",
133 "regex",
134 "not_regex",
135 ];
136
137 for keyword in keywords {
138 items.push(CompletionItem {
139 label: keyword.to_string(),
140 kind: Some(CompletionItemKind::KEYWORD),
141 detail: Some(format!("EQL keyword: {}", keyword)),
142 documentation: None,
143 deprecated: None,
144 preselect: None,
145 sort_text: Some(format!("0{}", keyword)),
146 filter_text: None,
147 insert_text: Some(keyword.to_string()),
148 insert_text_format: None,
149 text_edit: None,
150 additional_text_edits: None,
151 commit_characters: None,
152 command: None,
153 data: None,
154 tags: None,
155 insert_text_mode: None,
156 label_details: None,
157 });
158 }
159
160 if let Some(schema) = schema {
161 for table in &schema.tables {
162 items.push(CompletionItem {
163 label: table.name.clone(),
164 kind: Some(CompletionItemKind::CLASS),
165 detail: Some(format!("Elasticsearch Index: {}", table.name)),
166 documentation: table
167 .comment
168 .clone()
169 .map(tower_lsp::lsp_types::Documentation::String),
170 deprecated: None,
171 preselect: None,
172 sort_text: Some(format!("1{}", table.name)),
173 filter_text: None,
174 insert_text: Some(table.name.clone()),
175 insert_text_format: None,
176 text_edit: None,
177 additional_text_edits: None,
178 commit_characters: None,
179 command: None,
180 data: None,
181 tags: None,
182 insert_text_mode: None,
183 label_details: None,
184 });
185 }
186 }
187
188 items
189 }
190
191 async fn hover(
192 &self,
193 sql: &str,
194 _position: Position,
195 schema: Option<&Schema>,
196 ) -> Option<Hover> {
197 if let Some(schema) = schema {
198 for table in &schema.tables {
199 if sql.contains(&table.name) {
200 return Some(Hover {
201 contents: tower_lsp::lsp_types::HoverContents::Scalar(
202 MarkedString::String(format!(
203 "Elasticsearch EQL Index: {}\n{}",
204 table.name,
205 table.comment.as_deref().unwrap_or("No description")
206 )),
207 ),
208 range: None,
209 });
210 }
211 }
212 }
213 None
214 }
215
216 async fn goto_definition(
217 &self,
218 _sql: &str,
219 _position: Position,
220 _schema: Option<&Schema>,
221 ) -> Option<Location> {
222 None
223 }
224
225 async fn references(
226 &self,
227 _sql: &str,
228 _position: Position,
229 _schema: Option<&Schema>,
230 ) -> Vec<Location> {
231 Vec::new()
232 }
233
234 async fn format(&self, sql: &str) -> String {
235 sql.split_whitespace().collect::<Vec<_>>().join(" ")
237 }
238
239 async fn validate(&self, sql: &str, schema: Option<&Schema>) -> Vec<Diagnostic> {
240 self.parse(sql, schema).await
241 }
242}