1use super::graph_parse;
4use super::statement::NodedbStatement;
5
6pub fn parse(sql: &str) -> Option<NodedbStatement> {
10 let trimmed = sql.trim();
11 if trimmed.is_empty() {
12 return None;
13 }
14 let upper = trimmed.to_uppercase();
15 let parts: Vec<&str> = trimmed.split_whitespace().collect();
16 if parts.is_empty() {
17 return None;
18 }
19
20 if upper.starts_with("GRAPH ")
25 || upper.starts_with("MATCH ")
26 || upper.starts_with("OPTIONAL MATCH ")
27 {
28 return graph_parse::try_parse(trimmed);
29 }
30
31 if upper.starts_with("CREATE COLLECTION ") || upper.starts_with("CREATE TABLE ") {
33 let if_not_exists = upper.contains("IF NOT EXISTS");
34 let name = extract_name_after_keyword(&parts, "COLLECTION")
35 .or_else(|| extract_name_after_keyword(&parts, "TABLE"))?;
36 return Some(NodedbStatement::CreateCollection {
37 name,
38 if_not_exists,
39 raw_sql: trimmed.to_string(),
40 });
41 }
42 if upper.starts_with("DROP COLLECTION ") || upper.starts_with("DROP TABLE ") {
43 let if_exists = upper.contains("IF EXISTS");
44 let name = extract_name_after_if_exists(&parts, "COLLECTION")
45 .or_else(|| extract_name_after_if_exists(&parts, "TABLE"))?;
46 return Some(NodedbStatement::DropCollection { name, if_exists });
47 }
48 if upper.starts_with("ALTER COLLECTION ") || upper.starts_with("ALTER TABLE ") {
49 let name = extract_name_after_keyword(&parts, "COLLECTION")
50 .or_else(|| extract_name_after_keyword(&parts, "TABLE"))?;
51 return Some(NodedbStatement::AlterCollection {
52 name,
53 raw_sql: trimmed.to_string(),
54 });
55 }
56 if upper.starts_with("DESCRIBE ") && !upper.starts_with("DESCRIBE SEQUENCE") {
57 let name = parts.get(1)?.to_string();
58 return Some(NodedbStatement::DescribeCollection { name });
59 }
60 if upper == "\\D" || upper == "SHOW COLLECTIONS" || upper.starts_with("SHOW COLLECTIONS") {
61 return Some(NodedbStatement::ShowCollections);
62 }
63
64 if upper.starts_with("CREATE UNIQUE INDEX ") || upper.starts_with("CREATE UNIQUE IND") {
66 return Some(NodedbStatement::CreateIndex {
67 unique: true,
68 raw_sql: trimmed.to_string(),
69 });
70 }
71 if upper.starts_with("CREATE INDEX ") {
72 return Some(NodedbStatement::CreateIndex {
73 unique: false,
74 raw_sql: trimmed.to_string(),
75 });
76 }
77 if upper.starts_with("DROP INDEX ") {
78 let if_exists = upper.contains("IF EXISTS");
79 let name = extract_name_after_if_exists(&parts, "INDEX")?;
80 return Some(NodedbStatement::DropIndex {
81 name,
82 collection: None,
83 if_exists,
84 });
85 }
86 if upper.starts_with("SHOW INDEX") {
87 let collection = parts.get(2).map(|s| s.to_string());
88 return Some(NodedbStatement::ShowIndexes { collection });
89 }
90 if upper.starts_with("REINDEX ") {
91 let collection = parts.get(1)?.to_string();
92 return Some(NodedbStatement::Reindex { collection });
93 }
94
95 if upper.starts_with("CREATE ") && upper.contains("TRIGGER ") {
97 let or_replace = upper.contains("OR REPLACE");
98 let deferred = upper.contains("DEFERRED");
99 let sync = upper.contains("SYNC");
100 return Some(NodedbStatement::CreateTrigger {
101 or_replace,
102 deferred,
103 sync,
104 raw_sql: trimmed.to_string(),
105 });
106 }
107 if upper.starts_with("DROP TRIGGER ") {
108 let if_exists = upper.contains("IF EXISTS");
109 let name = extract_name_after_if_exists(&parts, "TRIGGER")?;
110 let collection = extract_after_keyword(&parts, "ON").unwrap_or_default();
111 return Some(NodedbStatement::DropTrigger {
112 name,
113 collection,
114 if_exists,
115 });
116 }
117 if upper.starts_with("ALTER TRIGGER ") {
118 return Some(NodedbStatement::AlterTrigger {
119 raw_sql: trimmed.to_string(),
120 });
121 }
122 if upper.starts_with("SHOW TRIGGERS") {
123 let collection = if upper.starts_with("SHOW TRIGGERS ON ") {
124 parts.get(3).map(|s| s.to_string())
125 } else {
126 None
127 };
128 return Some(NodedbStatement::ShowTriggers { collection });
129 }
130
131 if upper.starts_with("CREATE SCHEDULE ") {
133 return Some(NodedbStatement::CreateSchedule {
134 raw_sql: trimmed.to_string(),
135 });
136 }
137 if upper.starts_with("DROP SCHEDULE ") {
138 let if_exists = upper.contains("IF EXISTS");
139 let name = extract_name_after_if_exists(&parts, "SCHEDULE")?;
140 return Some(NodedbStatement::DropSchedule { name, if_exists });
141 }
142 if upper.starts_with("ALTER SCHEDULE ") {
143 return Some(NodedbStatement::AlterSchedule {
144 raw_sql: trimmed.to_string(),
145 });
146 }
147 if upper.starts_with("SHOW SCHEDULE HISTORY ") {
148 let name = parts.get(3)?.to_string();
149 return Some(NodedbStatement::ShowScheduleHistory { name });
150 }
151 if upper == "SHOW SCHEDULES" || upper.starts_with("SHOW SCHEDULES") {
152 return Some(NodedbStatement::ShowSchedules);
153 }
154
155 if upper.starts_with("CREATE SEQUENCE ") {
157 let if_not_exists = upper.contains("IF NOT EXISTS");
158 let name = extract_name_after_if_exists(&parts, "SEQUENCE")?;
159 return Some(NodedbStatement::CreateSequence {
160 name,
161 if_not_exists,
162 raw_sql: trimmed.to_string(),
163 });
164 }
165 if upper.starts_with("DROP SEQUENCE ") {
166 let if_exists = upper.contains("IF EXISTS");
167 let name = extract_name_after_if_exists(&parts, "SEQUENCE")?;
168 return Some(NodedbStatement::DropSequence { name, if_exists });
169 }
170 if upper.starts_with("ALTER SEQUENCE ") {
171 return Some(NodedbStatement::AlterSequence {
172 raw_sql: trimmed.to_string(),
173 });
174 }
175 if upper.starts_with("DESCRIBE SEQUENCE ") {
176 let name = parts.get(2)?.to_string();
177 return Some(NodedbStatement::DescribeSequence { name });
178 }
179 if upper == "SHOW SEQUENCES" || upper.starts_with("SHOW SEQUENCES") {
180 return Some(NodedbStatement::ShowSequences);
181 }
182
183 if upper.starts_with("CREATE ALERT ") {
185 return Some(NodedbStatement::CreateAlert {
186 raw_sql: trimmed.to_string(),
187 });
188 }
189 if upper.starts_with("DROP ALERT ") {
190 let if_exists = upper.contains("IF EXISTS");
191 let name = extract_name_after_if_exists(&parts, "ALERT")?;
192 return Some(NodedbStatement::DropAlert { name, if_exists });
193 }
194 if upper.starts_with("ALTER ALERT ") {
195 return Some(NodedbStatement::AlterAlert {
196 raw_sql: trimmed.to_string(),
197 });
198 }
199 if upper.starts_with("SHOW ALERT STATUS ") {
200 let name = parts.get(3)?.to_string();
201 return Some(NodedbStatement::ShowAlertStatus { name });
202 }
203 if upper.starts_with("SHOW ALERT") && !upper.starts_with("SHOW ALERT STATUS") {
204 return Some(NodedbStatement::ShowAlerts);
205 }
206
207 if upper.starts_with("CREATE RETENTION POLICY ") {
209 return Some(NodedbStatement::CreateRetentionPolicy {
210 raw_sql: trimmed.to_string(),
211 });
212 }
213 if upper.starts_with("DROP RETENTION POLICY ") {
214 let if_exists = upper.contains("IF EXISTS");
215 let name = extract_name_after_if_exists(&parts, "POLICY")?;
216 return Some(NodedbStatement::DropRetentionPolicy { name, if_exists });
217 }
218 if upper.starts_with("ALTER RETENTION POLICY ") {
219 return Some(NodedbStatement::AlterRetentionPolicy {
220 raw_sql: trimmed.to_string(),
221 });
222 }
223 if upper.starts_with("SHOW RETENTION POLIC") {
224 return Some(NodedbStatement::ShowRetentionPolicies);
225 }
226
227 if upper.starts_with("SHOW CLUSTER") {
229 return Some(NodedbStatement::ShowCluster);
230 }
231 if upper.starts_with("SHOW MIGRATIONS") {
232 return Some(NodedbStatement::ShowMigrations);
233 }
234 if upper.starts_with("SHOW RANGES") {
235 return Some(NodedbStatement::ShowRanges);
236 }
237 if upper.starts_with("SHOW ROUTING") {
238 return Some(NodedbStatement::ShowRouting);
239 }
240 if upper.starts_with("SHOW SCHEMA VERSION") {
241 return Some(NodedbStatement::ShowSchemaVersion);
242 }
243 if upper.starts_with("SHOW PEER HEALTH") {
244 return Some(NodedbStatement::ShowPeerHealth);
245 }
246 if upper.starts_with("REBALANCE") {
247 return Some(NodedbStatement::Rebalance);
248 }
249 if upper.starts_with("SHOW RAFT GROUP ") {
250 let id = parts.get(3)?.to_string();
251 return Some(NodedbStatement::ShowRaftGroup { group_id: id });
252 }
253 if upper.starts_with("SHOW RAFT GROUPS") || upper.starts_with("SHOW RAFT") {
254 return Some(NodedbStatement::ShowRaftGroups);
255 }
256 if upper.starts_with("ALTER RAFT GROUP ") {
257 return Some(NodedbStatement::AlterRaftGroup {
258 raw_sql: trimmed.to_string(),
259 });
260 }
261 if upper.starts_with("REMOVE NODE ") {
262 let id = parts.get(2)?.to_string();
263 return Some(NodedbStatement::RemoveNode { node_id: id });
264 }
265 if upper.starts_with("SHOW NODE ") {
266 let id = parts.get(2)?.to_string();
267 return Some(NodedbStatement::ShowNode { node_id: id });
268 }
269 if upper.starts_with("SHOW NODES") {
270 return Some(NodedbStatement::ShowNodes);
271 }
272
273 if upper.starts_with("ANALYZE") {
275 let collection = parts.get(1).map(|s| s.to_string());
276 return Some(NodedbStatement::Analyze { collection });
277 }
278 if upper.starts_with("COMPACT ") {
279 let collection = parts.get(1)?.to_string();
280 return Some(NodedbStatement::Compact { collection });
281 }
282 if upper.starts_with("SHOW COMPACTION ST") {
283 return Some(NodedbStatement::ShowCompactionStatus);
284 }
285 if upper.starts_with("SHOW STORAGE") {
286 let collection = parts.get(2).map(|s| s.to_string());
287 return Some(NodedbStatement::ShowStorage { collection });
288 }
289
290 if upper.starts_with("BACKUP TENANT ") {
292 return Some(NodedbStatement::BackupTenant {
293 raw_sql: trimmed.to_string(),
294 });
295 }
296 if upper.starts_with("RESTORE TENANT ") {
297 let dry_run = upper.ends_with(" DRY RUN") || upper.ends_with(" DRYRUN");
298 return Some(NodedbStatement::RestoreTenant {
299 dry_run,
300 raw_sql: trimmed.to_string(),
301 });
302 }
303
304 if upper.starts_with("CREATE USER ") {
306 return Some(NodedbStatement::CreateUser {
307 raw_sql: trimmed.to_string(),
308 });
309 }
310 if upper.starts_with("DROP USER ") {
311 let username = parts.get(2)?.to_string();
312 return Some(NodedbStatement::DropUser { username });
313 }
314 if upper.starts_with("ALTER USER ") {
315 return Some(NodedbStatement::AlterUser {
316 raw_sql: trimmed.to_string(),
317 });
318 }
319 if upper.starts_with("SHOW USERS") {
320 return Some(NodedbStatement::ShowUsers);
321 }
322 if upper.starts_with("GRANT ROLE ") {
323 return Some(NodedbStatement::GrantRole {
324 raw_sql: trimmed.to_string(),
325 });
326 }
327 if upper.starts_with("REVOKE ROLE ") {
328 return Some(NodedbStatement::RevokeRole {
329 raw_sql: trimmed.to_string(),
330 });
331 }
332 if upper.starts_with("GRANT ") {
333 return Some(NodedbStatement::GrantPermission {
334 raw_sql: trimmed.to_string(),
335 });
336 }
337 if upper.starts_with("REVOKE ") {
338 return Some(NodedbStatement::RevokePermission {
339 raw_sql: trimmed.to_string(),
340 });
341 }
342 if upper.starts_with("SHOW PERMISSIONS") {
343 let collection = parts.get(2).map(|s| s.to_string());
344 return Some(NodedbStatement::ShowPermissions { collection });
345 }
346 if upper.starts_with("SHOW GRANTS") {
347 let username = parts.get(2).map(|s| s.to_string());
348 return Some(NodedbStatement::ShowGrants { username });
349 }
350 if upper.starts_with("SHOW TENANTS") {
351 return Some(NodedbStatement::ShowTenants);
352 }
353 if upper.starts_with("SHOW AUDIT") {
354 return Some(NodedbStatement::ShowAuditLog);
355 }
356 if upper.starts_with("SHOW CONSTRAINTS ") {
357 let collection = parts.get(2)?.to_string();
358 return Some(NodedbStatement::ShowConstraints { collection });
359 }
360 if upper.starts_with("SHOW TYPEGUARD") {
361 let collection = parts.get(2)?.to_string();
362 return Some(NodedbStatement::ShowTypeGuards { collection });
363 }
364
365 if upper.starts_with("CREATE CHANGE STREAM ") {
367 return Some(NodedbStatement::CreateChangeStream {
368 raw_sql: trimmed.to_string(),
369 });
370 }
371 if upper.starts_with("DROP CHANGE STREAM ") {
372 let if_exists = upper.contains("IF EXISTS");
373 let name = extract_name_after_if_exists(&parts, "STREAM")?;
374 return Some(NodedbStatement::DropChangeStream { name, if_exists });
375 }
376
377 if upper.starts_with("CREATE RLS POLICY ") {
379 return Some(NodedbStatement::CreateRlsPolicy {
380 raw_sql: trimmed.to_string(),
381 });
382 }
383 if upper.starts_with("DROP RLS POLICY ") {
384 let if_exists = upper.contains("IF EXISTS");
385 let name = extract_name_after_if_exists(&parts, "POLICY")?;
386 let collection = extract_after_keyword(&parts, "ON").unwrap_or_default();
387 return Some(NodedbStatement::DropRlsPolicy {
388 name,
389 collection,
390 if_exists,
391 });
392 }
393 if upper.starts_with("SHOW RLS POLI") {
394 let collection = parts.get(3).map(|s| s.to_string());
395 return Some(NodedbStatement::ShowRlsPolicies { collection });
396 }
397
398 if upper.starts_with("CREATE MATERIALIZED VIEW ") {
400 return Some(NodedbStatement::CreateMaterializedView {
401 raw_sql: trimmed.to_string(),
402 });
403 }
404 if upper.starts_with("DROP MATERIALIZED VIEW ") {
405 let if_exists = upper.contains("IF EXISTS");
406 let name = extract_name_after_if_exists(&parts, "VIEW")?;
407 return Some(NodedbStatement::DropMaterializedView { name, if_exists });
408 }
409
410 if upper.starts_with("CREATE CONTINUOUS AGGREGATE ") {
412 return Some(NodedbStatement::CreateContinuousAggregate {
413 raw_sql: trimmed.to_string(),
414 });
415 }
416 if upper.starts_with("DROP CONTINUOUS AGGREGATE ") {
417 let if_exists = upper.contains("IF EXISTS");
418 let name = extract_name_after_if_exists(&parts, "AGGREGATE")?;
419 return Some(NodedbStatement::DropContinuousAggregate { name, if_exists });
420 }
421
422 None
423}
424
425fn extract_name_after_keyword(parts: &[&str], keyword: &str) -> Option<String> {
429 let kw_upper = keyword.to_uppercase();
430 let pos = parts.iter().position(|p| p.to_uppercase() == kw_upper)?;
431 let mut idx = pos + 1;
432 if parts.get(idx).map(|s| s.to_uppercase()) == Some("IF".to_string()) {
434 idx += 1; if parts.get(idx).map(|s| s.to_uppercase()) == Some("NOT".to_string()) {
436 idx += 1; }
438 if parts.get(idx).map(|s| s.to_uppercase()) == Some("EXISTS".to_string()) {
439 idx += 1;
440 }
441 }
442 parts.get(idx).map(|s| s.to_string())
443}
444
445fn extract_name_after_if_exists(parts: &[&str], keyword: &str) -> Option<String> {
448 extract_name_after_keyword(parts, keyword)
449}
450
451fn extract_after_keyword(parts: &[&str], keyword: &str) -> Option<String> {
453 let kw_upper = keyword.to_uppercase();
454 let pos = parts.iter().position(|p| p.to_uppercase() == kw_upper)?;
455 parts.get(pos + 1).map(|s| s.to_string())
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
463 fn parse_create_collection() {
464 let stmt = parse("CREATE COLLECTION users (id INT, name TEXT)").unwrap();
465 match stmt {
466 NodedbStatement::CreateCollection {
467 name,
468 if_not_exists,
469 ..
470 } => {
471 assert_eq!(name, "users");
472 assert!(!if_not_exists);
473 }
474 other => panic!("expected CreateCollection, got {other:?}"),
475 }
476 }
477
478 #[test]
479 fn parse_create_collection_if_not_exists() {
480 let stmt = parse("CREATE COLLECTION IF NOT EXISTS users").unwrap();
481 match stmt {
482 NodedbStatement::CreateCollection {
483 name,
484 if_not_exists,
485 ..
486 } => {
487 assert_eq!(name, "users");
488 assert!(if_not_exists);
489 }
490 other => panic!("expected CreateCollection, got {other:?}"),
491 }
492 }
493
494 #[test]
495 fn parse_drop_collection() {
496 let stmt = parse("DROP COLLECTION users").unwrap();
497 assert_eq!(
498 stmt,
499 NodedbStatement::DropCollection {
500 name: "users".into(),
501 if_exists: false,
502 }
503 );
504 }
505
506 #[test]
507 fn parse_drop_collection_if_exists() {
508 let stmt = parse("DROP COLLECTION IF EXISTS users").unwrap();
509 assert_eq!(
510 stmt,
511 NodedbStatement::DropCollection {
512 name: "users".into(),
513 if_exists: true,
514 }
515 );
516 }
517
518 #[test]
519 fn parse_show_nodes() {
520 assert_eq!(parse("SHOW NODES"), Some(NodedbStatement::ShowNodes));
521 }
522
523 #[test]
524 fn parse_show_cluster() {
525 assert_eq!(parse("SHOW CLUSTER"), Some(NodedbStatement::ShowCluster));
526 }
527
528 #[test]
529 fn parse_create_trigger() {
530 let stmt = parse("CREATE OR REPLACE SYNC TRIGGER on_insert ...").unwrap();
531 match stmt {
532 NodedbStatement::CreateTrigger {
533 or_replace,
534 sync,
535 deferred,
536 ..
537 } => {
538 assert!(or_replace);
539 assert!(sync);
540 assert!(!deferred);
541 }
542 other => panic!("expected CreateTrigger, got {other:?}"),
543 }
544 }
545
546 #[test]
547 fn parse_drop_index_if_exists() {
548 let stmt = parse("DROP INDEX IF EXISTS idx_name").unwrap();
549 match stmt {
550 NodedbStatement::DropIndex {
551 name, if_exists, ..
552 } => {
553 assert_eq!(name, "idx_name");
554 assert!(if_exists);
555 }
556 other => panic!("expected DropIndex, got {other:?}"),
557 }
558 }
559
560 #[test]
561 fn parse_analyze() {
562 assert_eq!(
563 parse("ANALYZE users"),
564 Some(NodedbStatement::Analyze {
565 collection: Some("users".into()),
566 })
567 );
568 assert_eq!(
569 parse("ANALYZE"),
570 Some(NodedbStatement::Analyze { collection: None })
571 );
572 }
573
574 #[test]
575 fn non_ddl_returns_none() {
576 assert!(parse("SELECT * FROM users").is_none());
577 assert!(parse("INSERT INTO users VALUES (1)").is_none());
578 }
579
580 #[test]
581 fn parse_grant_role() {
582 let stmt = parse("GRANT ROLE admin TO alice").unwrap();
583 match stmt {
584 NodedbStatement::GrantRole { raw_sql } => {
585 assert!(raw_sql.contains("admin"));
586 }
587 other => panic!("expected GrantRole, got {other:?}"),
588 }
589 }
590
591 #[test]
592 fn parse_create_sequence_if_not_exists() {
593 let stmt = parse("CREATE SEQUENCE IF NOT EXISTS my_seq START 1").unwrap();
594 match stmt {
595 NodedbStatement::CreateSequence {
596 name,
597 if_not_exists,
598 ..
599 } => {
600 assert_eq!(name, "my_seq");
601 assert!(if_not_exists);
602 }
603 other => panic!("expected CreateSequence, got {other:?}"),
604 }
605 }
606
607 #[test]
608 fn parse_restore_dry_run() {
609 let stmt = parse("RESTORE TENANT 1 FROM '/tmp/backup' DRY RUN").unwrap();
610 match stmt {
611 NodedbStatement::RestoreTenant { dry_run, .. } => {
612 assert!(dry_run);
613 }
614 other => panic!("expected RestoreTenant, got {other:?}"),
615 }
616 }
617}