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