sql_lsp/dialects/
elasticsearch_eql.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
9/// Elasticsearch EQL (Event Query Language) 方言
10pub 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        // EQL 特定的语法检查
38        // EQL 查询通常以 sequence, join, event 等关键字开始
39        let upper_sql = sql.to_uppercase();
40        if upper_sql.contains("SEQUENCE")
41            || upper_sql.contains("JOIN")
42            || upper_sql.contains("EVENT")
43        {
44            // 检查基本的 EQL 语法结构
45            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        // EQL 关键字和命令
82        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        // EQL 格式化:保持基本格式
236        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}