1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum QueryMode {
8 Sql,
10 Gremlin,
12 Cypher,
14 Sparql,
16 Path,
18 Natural,
20 Unknown,
22}
23
24pub fn detect_mode(input: &str) -> QueryMode {
26 let trimmed = input.trim();
27 let lower = trimmed.to_lowercase();
28
29 if trimmed.starts_with('"') || trimmed.starts_with('\'') {
31 return QueryMode::Natural;
32 }
33
34 if lower.starts_with("g.") || lower.starts_with("__.") {
36 return QueryMode::Gremlin;
37 }
38
39 if lower.starts_with("path ") || lower.starts_with("paths ") {
41 return QueryMode::Path;
42 }
43
44 if lower.starts_with("prefix ") || has_sparql_pattern(&lower) {
46 return QueryMode::Sparql;
47 }
48
49 if lower.starts_with("match ") || lower.starts_with("match(") {
51 return QueryMode::Cypher;
52 }
53
54 let first_token = lower.split_whitespace().next().unwrap_or("");
59 if matches!(
60 first_token,
61 "begin"
62 | "start"
63 | "commit"
64 | "rollback"
65 | "savepoint"
66 | "release"
67 | "end"
68 | "vacuum"
69 | "analyze"
70 | "reset"
71 | "checkpoint"
72 | "checkout"
73 | "merge"
74 | "cherry"
75 | "revert"
76 | "resolve"
77 | "copy"
78 | "refresh"
79 | "explain"
80 | "grant"
81 | "revoke"
82 | "attach"
83 | "detach"
84 | "simulate"
85 | "lint"
86 | "migrate"
87 | "apply"
88 | "events"
89 | "describe"
90 | "desc"
91 ) {
92 return QueryMode::Sql;
93 }
94 if lower.starts_with("select ")
95 || lower.starts_with("from ")
96 || lower.starts_with("insert ")
97 || lower.starts_with("update ")
98 || lower.starts_with("delete ")
99 || lower.starts_with("truncate ")
100 || lower.starts_with("create ")
101 || lower.starts_with("drop ")
102 || lower.starts_with("alter ")
103 || lower.starts_with("vector ")
104 || lower.starts_with("hybrid ")
105 || lower.starts_with("graph ")
106 || lower.starts_with("queue ")
107 || lower.starts_with("events ")
108 || lower.starts_with("tree ")
109 || lower.starts_with("hll ")
110 || lower.starts_with("sketch ")
111 || lower.starts_with("filter ")
112 || lower.starts_with("vault ")
113 || lower.starts_with("unseal vault ")
114 || lower.starts_with("rotate vault ")
115 || lower.starts_with("history vault ")
116 || lower.starts_with("list vault ")
117 || lower.starts_with("list kv ")
118 || lower.starts_with("watch vault ")
119 || lower.starts_with("delete vault ")
120 || lower.starts_with("purge vault ")
121 || lower.starts_with("search ")
122 || lower.starts_with("ask ")
123 || lower.starts_with("put config ")
124 || lower.starts_with("get config ")
125 || lower.starts_with("resolve config ")
126 || lower.starts_with("rotate config ")
127 || lower.starts_with("delete config ")
128 || lower.starts_with("history config ")
129 || lower.starts_with("list config ")
130 || lower.starts_with("watch config ")
131 || lower.starts_with("incr config ")
132 || lower.starts_with("decr config ")
133 || lower.starts_with("add config ")
134 || lower.starts_with("invalidate config ")
135 || lower.starts_with("invalidate tags ")
136 || lower.starts_with("set config ")
137 || lower.starts_with("set secret ")
138 || lower.starts_with("set tenant")
139 || lower.starts_with("show create ")
140 || lower.starts_with("show config")
141 || lower.starts_with("show collections")
142 || lower.starts_with("show tables")
143 || lower.starts_with("show queues")
144 || lower.starts_with("show vectors")
145 || lower.starts_with("show documents")
146 || lower.starts_with("show timeseries")
147 || lower.starts_with("show graphs")
148 || lower.starts_with("kv ")
149 || lower.starts_with("show kv")
150 || lower.starts_with("show configs")
151 || lower.starts_with("show vaults")
152 || lower.starts_with("show schema")
153 || lower.starts_with("show indices")
154 || lower.starts_with("show indexes")
155 || lower.starts_with("show sample ")
156 || lower.starts_with("show secret")
157 || lower.starts_with("show stats")
158 || lower.starts_with("show tenant")
159 || lower.starts_with("show policies")
160 || lower.starts_with("show effective ")
161 || lower.starts_with("rank of ")
162 || lower.starts_with("rank range ")
163 || lower.starts_with("approx rank of ")
164 || lower.starts_with("approximate rank of ")
165 || lower.starts_with("zrank ")
166 || lower.starts_with("zrange ")
167 || lower.starts_with("describe ")
168 || lower.starts_with("desc ")
169 {
170 if lower.starts_with("select ") && has_sparql_variable(&lower) {
174 return QueryMode::Sparql;
175 }
176 return QueryMode::Sql;
177 }
178
179 if is_natural_language(&lower) {
181 return QueryMode::Natural;
182 }
183
184 QueryMode::Unknown
185}
186
187fn has_sparql_pattern(lower: &str) -> bool {
189 let has_var = has_sparql_variable(lower);
195
196 let has_triple_pattern = lower.contains(" where {") || lower.contains(" where{");
198
199 let has_prefix_pattern = lower.contains(":")
201 && (lower.contains(":<")
202 || lower.contains("> :")
203 || lower.contains(" :") && lower.contains("?"));
204
205 has_var || has_triple_pattern || has_prefix_pattern
206}
207
208fn has_sparql_variable(input: &str) -> bool {
209 let bytes = input.as_bytes();
210 bytes
211 .windows(2)
212 .any(|pair| pair[0] == b'?' && is_sparql_variable_start(pair[1]))
213}
214
215fn is_sparql_variable_start(byte: u8) -> bool {
216 byte.is_ascii_alphabetic() || byte == b'_'
217}
218
219fn is_natural_language(lower: &str) -> bool {
221 let question_starters = [
223 "find ", "show ", "list ", "what ", "which ", "where ", "how ", "who ", "get ", "give ",
224 "tell ", "display ", "search ", "look ",
225 ];
226
227 let nl_patterns = [
229 " with ",
230 " for ",
231 " that ",
232 " have ",
233 " has ",
234 " can ",
235 " are ",
236 " is ",
237 " all ",
238 " me ",
239 " the ",
240 " from ",
241 " to ",
242 " on ",
243 " in ",
244 "vulnerable",
245 "credential",
246 "password",
247 "user",
248 "host",
249 "service",
250 "connected",
251 "reachable",
252 "exposed",
253 "critical",
254 ];
255
256 for starter in question_starters.iter() {
258 if lower.starts_with(starter) {
259 return true;
260 }
261 }
262
263 let pattern_count = nl_patterns.iter().filter(|p| lower.contains(*p)).count();
265
266 pattern_count >= 2
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_sql_detection() {
275 assert_eq!(
276 detect_mode("SELECT * FROM users WHERE id = 1"),
277 QueryMode::Sql
278 );
279 assert_eq!(detect_mode("select name, age from hosts"), QueryMode::Sql);
280 assert_eq!(
281 detect_mode("FROM hosts h WHERE h.os = 'Linux'"),
282 QueryMode::Sql
283 );
284 assert_eq!(
285 detect_mode("INSERT INTO users VALUES (1, 'alice')"),
286 QueryMode::Sql
287 );
288 assert_eq!(
289 detect_mode("UPDATE hosts SET status = 'active'"),
290 QueryMode::Sql
291 );
292 assert_eq!(
293 detect_mode("DELETE FROM logs WHERE age > 30"),
294 QueryMode::Sql
295 );
296 assert_eq!(
297 detect_mode("QUEUE GROUP CREATE tasks workers"),
298 QueryMode::Sql
299 );
300 assert_eq!(
301 detect_mode("EVENTS BACKFILL users TO audit"),
302 QueryMode::Sql
303 );
304 assert_eq!(detect_mode("TREE VALIDATE forest.org"), QueryMode::Sql);
305 assert_eq!(
306 detect_mode("VECTOR SEARCH embeddings SIMILAR TO [1.0, 0.0] LIMIT 5"),
307 QueryMode::Sql
308 );
309 assert_eq!(
310 detect_mode("HYBRID FROM hosts VECTOR SEARCH embeddings SIMILAR TO [1.0, 0.0] LIMIT 5"),
311 QueryMode::Sql
312 );
313 assert_eq!(
314 detect_mode("ASK 'what happened on host 10.0.0.1?' USING groq"),
315 QueryMode::Sql
316 );
317 assert_eq!(
318 detect_mode("SELECT name FROM t WHERE id = ?"),
319 QueryMode::Sql
320 );
321 assert_eq!(
322 detect_mode("SELECT name FROM t WHERE id = ?1"),
323 QueryMode::Sql
324 );
325 assert_eq!(
326 detect_mode("INSERT INTO t (id, name) VALUES (?, ?)"),
327 QueryMode::Sql
328 );
329 assert_eq!(
330 detect_mode("SET SECRET red.secret.api = 'x'"),
331 QueryMode::Sql
332 );
333 assert_eq!(detect_mode("SHOW SECRET red.secret"), QueryMode::Sql);
334 assert_eq!(detect_mode("SHOW SECRETS"), QueryMode::Sql);
335 assert_eq!(detect_mode("VAULT PUT secrets.api = 'x'"), QueryMode::Sql);
336 assert_eq!(
337 detect_mode("LIST KV settings PREFIX feature LIMIT 10"),
338 QueryMode::Sql
339 );
340 assert_eq!(detect_mode("SHOW SAMPLE users"), QueryMode::Sql);
341 assert_eq!(detect_mode("SHOW TABLES"), QueryMode::Sql);
342 assert_eq!(detect_mode("SHOW QUEUES"), QueryMode::Sql);
343 assert_eq!(detect_mode("SHOW VECTORS"), QueryMode::Sql);
344 assert_eq!(detect_mode("SHOW DOCUMENTS"), QueryMode::Sql);
345 assert_eq!(detect_mode("SHOW TIMESERIES"), QueryMode::Sql);
346 assert_eq!(detect_mode("SHOW GRAPHS"), QueryMode::Sql);
347 assert_eq!(detect_mode("SHOW KV"), QueryMode::Sql);
348 assert_eq!(detect_mode("SHOW KVS"), QueryMode::Sql);
349 assert_eq!(detect_mode("SHOW CONFIGS"), QueryMode::Sql);
350 assert_eq!(detect_mode("SHOW VAULTS"), QueryMode::Sql);
351 assert_eq!(detect_mode("SHOW SCHEMA users"), QueryMode::Sql);
352 assert_eq!(detect_mode("SHOW CREATE TABLE users"), QueryMode::Sql);
353 assert_eq!(detect_mode("DESCRIBE users"), QueryMode::Sql);
354 assert_eq!(detect_mode("DESC users"), QueryMode::Sql);
355 assert_eq!(detect_mode("SHOW INDICES"), QueryMode::Sql);
356 assert_eq!(detect_mode("SHOW INDEXES"), QueryMode::Sql);
357 assert_eq!(detect_mode("SHOW STATS users"), QueryMode::Sql);
358 assert_eq!(detect_mode("EXPLAIN MIGRATION *"), QueryMode::Sql);
359 }
360
361 #[test]
362 fn test_gremlin_detection() {
363 assert_eq!(detect_mode("g.V()"), QueryMode::Gremlin);
364 assert_eq!(detect_mode("g.V().hasLabel('host')"), QueryMode::Gremlin);
365 assert_eq!(
366 detect_mode("g.V().out('connects').in('has_service')"),
367 QueryMode::Gremlin
368 );
369 assert_eq!(
370 detect_mode("g.E().hasLabel('auth_access')"),
371 QueryMode::Gremlin
372 );
373 assert_eq!(
374 detect_mode("__.out('knows').has('name', 'bob')"),
375 QueryMode::Gremlin
376 );
377 assert_eq!(
378 detect_mode("g.V('host:10.0.0.1').repeat(out()).times(3)"),
379 QueryMode::Gremlin
380 );
381 }
382
383 #[test]
384 fn test_cypher_detection() {
385 assert_eq!(
386 detect_mode("MATCH (a)-[r]->(b) RETURN a, b"),
387 QueryMode::Cypher
388 );
389 assert_eq!(
390 detect_mode("MATCH (h:Host)-[:HAS_SERVICE]->(s:Service)"),
391 QueryMode::Cypher
392 );
393 assert_eq!(
394 detect_mode("match (n) where n.ip = '10.0.0.1' return n"),
395 QueryMode::Cypher
396 );
397 assert_eq!(
398 detect_mode("MATCH(a:User) RETURN a.name"),
399 QueryMode::Cypher
400 );
401 }
402
403 #[test]
404 fn test_sparql_detection() {
405 assert_eq!(
406 detect_mode("SELECT ?name WHERE { ?s :name ?name }"),
407 QueryMode::Sparql
408 );
409 assert_eq!(
410 detect_mode("PREFIX ex: <http://example.org/> SELECT ?x WHERE { ?x ex:type ?t }"),
411 QueryMode::Sparql
412 );
413 assert_eq!(
414 detect_mode("SELECT ?host ?ip WHERE { ?host :hasIP ?ip }"),
415 QueryMode::Sparql
416 );
417 assert_eq!(
418 detect_mode("SELECT ?x WHERE { ?x rdf:type :Foo }"),
419 QueryMode::Sparql
420 );
421 }
422
423 #[test]
424 fn test_path_detection() {
425 assert_eq!(
426 detect_mode("PATH FROM host('10.0.0.1') TO host('10.0.0.2')"),
427 QueryMode::Path
428 );
429 assert_eq!(
430 detect_mode("PATHS ALL FROM credential('admin') TO host('db')"),
431 QueryMode::Path
432 );
433 assert_eq!(
434 detect_mode("path from user('root') to service('ssh') via auth_access"),
435 QueryMode::Path
436 );
437 }
438
439 #[test]
440 fn test_natural_detection() {
441 assert_eq!(
442 detect_mode("find all hosts with ssh open"),
443 QueryMode::Natural
444 );
445 assert_eq!(
446 detect_mode("show me vulnerable services"),
447 QueryMode::Natural
448 );
449 assert_eq!(
450 detect_mode("what credentials can reach the database?"),
451 QueryMode::Natural
452 );
453 assert_eq!(
454 detect_mode("list users with weak passwords"),
455 QueryMode::Natural
456 );
457 assert_eq!(
458 detect_mode("\"find hosts connected to 10.0.0.1\""),
459 QueryMode::Natural
460 );
461 assert_eq!(
462 detect_mode("which hosts have critical vulnerabilities?"),
463 QueryMode::Natural
464 );
465 }
466
467 #[test]
468 fn test_edge_cases() {
469 assert_eq!(detect_mode(""), QueryMode::Unknown);
471
472 assert_eq!(detect_mode(" "), QueryMode::Unknown);
474
475 assert_eq!(detect_mode("SELECT"), QueryMode::Unknown); assert_eq!(detect_mode("G.V()"), QueryMode::Gremlin);
478 assert_eq!(detect_mode("Match (a) RETURN a"), QueryMode::Cypher);
479 }
480}