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 kv ")
139 || lower.starts_with("set tenant")
140 || lower.starts_with("show create ")
141 || lower.starts_with("show config")
142 || lower.starts_with("show collections")
143 || lower.starts_with("show tables")
144 || lower.starts_with("show queues")
145 || lower.starts_with("show vectors")
146 || lower.starts_with("show documents")
147 || lower.starts_with("show timeseries")
148 || lower.starts_with("show graphs")
149 || lower.starts_with("kv ")
150 || lower.starts_with("show kv")
151 || lower.starts_with("show configs")
152 || lower.starts_with("show vaults")
153 || lower.starts_with("show schema")
154 || lower.starts_with("show indices")
155 || lower.starts_with("show indexes")
156 || lower.starts_with("show sample ")
157 || lower.starts_with("show secret")
158 || lower.starts_with("show stats")
159 || lower.starts_with("show tenant")
160 || lower.starts_with("show policies")
161 || lower.starts_with("show effective ")
162 || lower.starts_with("rank of ")
163 || lower.starts_with("rank range ")
164 || lower.starts_with("approx rank of ")
165 || lower.starts_with("approximate rank of ")
166 || lower.starts_with("zrank ")
167 || lower.starts_with("zrange ")
168 || lower.starts_with("describe ")
169 || lower.starts_with("desc ")
170 {
171 if lower.starts_with("select ") && has_sparql_variable(&lower) {
175 return QueryMode::Sparql;
176 }
177 return QueryMode::Sql;
178 }
179
180 if is_natural_language(&lower) {
182 return QueryMode::Natural;
183 }
184
185 QueryMode::Unknown
186}
187
188fn has_sparql_pattern(lower: &str) -> bool {
190 let has_var = has_sparql_variable(lower);
196
197 let has_triple_pattern = lower.contains(" where {") || lower.contains(" where{");
199
200 let has_prefix_pattern = lower.contains(":")
202 && (lower.contains(":<")
203 || lower.contains("> :")
204 || lower.contains(" :") && lower.contains("?"));
205
206 has_var || has_triple_pattern || has_prefix_pattern
207}
208
209fn has_sparql_variable(input: &str) -> bool {
210 let bytes = input.as_bytes();
211 bytes
212 .windows(2)
213 .any(|pair| pair[0] == b'?' && is_sparql_variable_start(pair[1]))
214}
215
216fn is_sparql_variable_start(byte: u8) -> bool {
217 byte.is_ascii_alphabetic() || byte == b'_'
218}
219
220fn is_natural_language(lower: &str) -> bool {
222 let question_starters = [
224 "find ", "show ", "list ", "what ", "which ", "where ", "how ", "who ", "get ", "give ",
225 "tell ", "display ", "search ", "look ",
226 ];
227
228 let nl_patterns = [
230 " with ",
231 " for ",
232 " that ",
233 " have ",
234 " has ",
235 " can ",
236 " are ",
237 " is ",
238 " all ",
239 " me ",
240 " the ",
241 " from ",
242 " to ",
243 " on ",
244 " in ",
245 "vulnerable",
246 "credential",
247 "password",
248 "user",
249 "host",
250 "service",
251 "connected",
252 "reachable",
253 "exposed",
254 "critical",
255 ];
256
257 for starter in question_starters.iter() {
259 if lower.starts_with(starter) {
260 return true;
261 }
262 }
263
264 let pattern_count = nl_patterns.iter().filter(|p| lower.contains(*p)).count();
266
267 pattern_count >= 2
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_sql_detection() {
276 assert_eq!(
277 detect_mode("SELECT * FROM users WHERE id = 1"),
278 QueryMode::Sql
279 );
280 assert_eq!(detect_mode("select name, age from hosts"), QueryMode::Sql);
281 assert_eq!(
282 detect_mode("FROM hosts h WHERE h.os = 'Linux'"),
283 QueryMode::Sql
284 );
285 assert_eq!(
286 detect_mode("INSERT INTO users VALUES (1, 'alice')"),
287 QueryMode::Sql
288 );
289 assert_eq!(
290 detect_mode("UPDATE hosts SET status = 'active'"),
291 QueryMode::Sql
292 );
293 assert_eq!(
294 detect_mode("DELETE FROM logs WHERE age > 30"),
295 QueryMode::Sql
296 );
297 assert_eq!(
298 detect_mode("QUEUE GROUP CREATE tasks workers"),
299 QueryMode::Sql
300 );
301 assert_eq!(
302 detect_mode("EVENTS BACKFILL users TO audit"),
303 QueryMode::Sql
304 );
305 assert_eq!(detect_mode("TREE VALIDATE forest.org"), QueryMode::Sql);
306 assert_eq!(
307 detect_mode("VECTOR SEARCH embeddings SIMILAR TO [1.0, 0.0] LIMIT 5"),
308 QueryMode::Sql
309 );
310 assert_eq!(
311 detect_mode("HYBRID FROM hosts VECTOR SEARCH embeddings SIMILAR TO [1.0, 0.0] LIMIT 5"),
312 QueryMode::Sql
313 );
314 assert_eq!(
315 detect_mode("ASK 'what happened on host 10.0.0.1?' USING groq"),
316 QueryMode::Sql
317 );
318 assert_eq!(
319 detect_mode("SELECT name FROM t WHERE id = ?"),
320 QueryMode::Sql
321 );
322 assert_eq!(
323 detect_mode("SELECT name FROM t WHERE id = ?1"),
324 QueryMode::Sql
325 );
326 assert_eq!(
327 detect_mode("INSERT INTO t (id, name) VALUES (?, ?)"),
328 QueryMode::Sql
329 );
330 assert_eq!(
331 detect_mode("SET SECRET red.secret.api = 'x'"),
332 QueryMode::Sql
333 );
334 assert_eq!(detect_mode("SHOW SECRET red.secret"), QueryMode::Sql);
335 assert_eq!(detect_mode("SHOW SECRETS"), QueryMode::Sql);
336 assert_eq!(detect_mode("VAULT PUT secrets.api = 'x'"), QueryMode::Sql);
337 assert_eq!(
338 detect_mode("LIST KV settings PREFIX feature LIMIT 10"),
339 QueryMode::Sql
340 );
341 assert_eq!(detect_mode("SHOW SAMPLE users"), QueryMode::Sql);
342 assert_eq!(detect_mode("SHOW TABLES"), QueryMode::Sql);
343 assert_eq!(detect_mode("SHOW QUEUES"), QueryMode::Sql);
344 assert_eq!(detect_mode("SHOW VECTORS"), QueryMode::Sql);
345 assert_eq!(detect_mode("SHOW DOCUMENTS"), QueryMode::Sql);
346 assert_eq!(detect_mode("SHOW TIMESERIES"), QueryMode::Sql);
347 assert_eq!(detect_mode("SHOW GRAPHS"), QueryMode::Sql);
348 assert_eq!(detect_mode("SHOW KV"), QueryMode::Sql);
349 assert_eq!(detect_mode("SHOW KVS"), QueryMode::Sql);
350 assert_eq!(detect_mode("SHOW CONFIGS"), QueryMode::Sql);
351 assert_eq!(detect_mode("SHOW VAULTS"), QueryMode::Sql);
352 assert_eq!(detect_mode("SHOW SCHEMA users"), QueryMode::Sql);
353 assert_eq!(detect_mode("SHOW CREATE TABLE users"), QueryMode::Sql);
354 assert_eq!(detect_mode("DESCRIBE users"), QueryMode::Sql);
355 assert_eq!(detect_mode("DESC users"), QueryMode::Sql);
356 assert_eq!(detect_mode("SHOW INDICES"), QueryMode::Sql);
357 assert_eq!(detect_mode("SHOW INDEXES"), QueryMode::Sql);
358 assert_eq!(detect_mode("SHOW STATS users"), QueryMode::Sql);
359 assert_eq!(detect_mode("EXPLAIN MIGRATION *"), QueryMode::Sql);
360 }
361
362 #[test]
363 fn test_gremlin_detection() {
364 assert_eq!(detect_mode("g.V()"), QueryMode::Gremlin);
365 assert_eq!(detect_mode("g.V().hasLabel('host')"), QueryMode::Gremlin);
366 assert_eq!(
367 detect_mode("g.V().out('connects').in('has_service')"),
368 QueryMode::Gremlin
369 );
370 assert_eq!(
371 detect_mode("g.E().hasLabel('auth_access')"),
372 QueryMode::Gremlin
373 );
374 assert_eq!(
375 detect_mode("__.out('knows').has('name', 'bob')"),
376 QueryMode::Gremlin
377 );
378 assert_eq!(
379 detect_mode("g.V('host:10.0.0.1').repeat(out()).times(3)"),
380 QueryMode::Gremlin
381 );
382 }
383
384 #[test]
385 fn test_cypher_detection() {
386 assert_eq!(
387 detect_mode("MATCH (a)-[r]->(b) RETURN a, b"),
388 QueryMode::Cypher
389 );
390 assert_eq!(
391 detect_mode("MATCH (h:Host)-[:HAS_SERVICE]->(s:Service)"),
392 QueryMode::Cypher
393 );
394 assert_eq!(
395 detect_mode("match (n) where n.ip = '10.0.0.1' return n"),
396 QueryMode::Cypher
397 );
398 assert_eq!(
399 detect_mode("MATCH(a:User) RETURN a.name"),
400 QueryMode::Cypher
401 );
402 }
403
404 #[test]
405 fn test_sparql_detection() {
406 assert_eq!(
407 detect_mode("SELECT ?name WHERE { ?s :name ?name }"),
408 QueryMode::Sparql
409 );
410 assert_eq!(
411 detect_mode("PREFIX ex: <http://example.org/> SELECT ?x WHERE { ?x ex:type ?t }"),
412 QueryMode::Sparql
413 );
414 assert_eq!(
415 detect_mode("SELECT ?host ?ip WHERE { ?host :hasIP ?ip }"),
416 QueryMode::Sparql
417 );
418 assert_eq!(
419 detect_mode("SELECT ?x WHERE { ?x rdf:type :Foo }"),
420 QueryMode::Sparql
421 );
422 }
423
424 #[test]
425 fn test_path_detection() {
426 assert_eq!(
427 detect_mode("PATH FROM host('10.0.0.1') TO host('10.0.0.2')"),
428 QueryMode::Path
429 );
430 assert_eq!(
431 detect_mode("PATHS ALL FROM credential('admin') TO host('db')"),
432 QueryMode::Path
433 );
434 assert_eq!(
435 detect_mode("path from user('root') to service('ssh') via auth_access"),
436 QueryMode::Path
437 );
438 }
439
440 #[test]
441 fn test_natural_detection() {
442 assert_eq!(
443 detect_mode("find all hosts with ssh open"),
444 QueryMode::Natural
445 );
446 assert_eq!(
447 detect_mode("show me vulnerable services"),
448 QueryMode::Natural
449 );
450 assert_eq!(
451 detect_mode("what credentials can reach the database?"),
452 QueryMode::Natural
453 );
454 assert_eq!(
455 detect_mode("list users with weak passwords"),
456 QueryMode::Natural
457 );
458 assert_eq!(
459 detect_mode("\"find hosts connected to 10.0.0.1\""),
460 QueryMode::Natural
461 );
462 assert_eq!(
463 detect_mode("which hosts have critical vulnerabilities?"),
464 QueryMode::Natural
465 );
466 }
467
468 #[test]
469 fn test_edge_cases() {
470 assert_eq!(detect_mode(""), QueryMode::Unknown);
472
473 assert_eq!(detect_mode(" "), QueryMode::Unknown);
475
476 assert_eq!(detect_mode("SELECT"), QueryMode::Unknown); assert_eq!(detect_mode("G.V()"), QueryMode::Gremlin);
479 assert_eq!(detect_mode("Match (a) RETURN a"), QueryMode::Cypher);
480 }
481}