1use super::{
6 alert, backup, change_stream, cluster_admin, collection, conflict_policy, copy_from, copy_to,
7 custom_type, database, grant, graph_stats, index, maintenance, materialized_view,
8 oidc_provider, redaction, retention, rls, schedule, sequence, synonym_group, tenant, trigger,
9 user_auth,
10};
11use crate::ddl_ast::graph_parse;
12use crate::ddl_ast::statement::NodedbStatement;
13use crate::error::SqlError;
14use crate::parser::preprocess::lex;
15
16pub fn parse(sql: &str) -> Option<Result<NodedbStatement, SqlError>> {
23 let trimmed = sql.trim();
24 if trimmed.is_empty() {
25 return None;
26 }
27 let upper = trimmed.to_uppercase();
28 let parts: Vec<&str> = trimmed.split_whitespace().collect();
29 if parts.is_empty() {
30 return None;
31 }
32
33 let first = lex::first_sql_word(trimmed).map(|w| w.to_uppercase());
38 let is_graph = match first.as_deref() {
39 Some("GRAPH") | Some("MATCH") => true,
40 Some("OPTIONAL") => lex::second_sql_word(trimmed)
41 .map(|w| w.eq_ignore_ascii_case("MATCH"))
42 .unwrap_or(false),
43 _ => false,
44 };
45 if is_graph {
46 return graph_parse::try_parse(trimmed);
50 }
51
52 macro_rules! try_family {
58 ($result:expr) => {{
59 let r = $result;
60 if r.is_some() {
61 return r;
62 }
63 }};
64 }
65
66 try_family!(graph_stats::try_parse(&upper, &parts, trimmed));
69 try_family!(conflict_policy::try_parse(&upper, &parts, trimmed));
72 try_family!(collection::try_parse(&upper, &parts, trimmed));
73 try_family!(index::try_parse(&upper, &parts, trimmed));
74 try_family!(trigger::try_parse(&upper, &parts, trimmed));
75 try_family!(schedule::try_parse(&upper, &parts, trimmed));
76 try_family!(sequence::try_parse(&upper, &parts, trimmed));
77 try_family!(alert::try_parse(&upper, &parts, trimmed));
78 try_family!(retention::try_parse(&upper, &parts, trimmed));
79 try_family!(cluster_admin::try_parse(&upper, &parts, trimmed));
80 try_family!(maintenance::try_parse(&upper, &parts, trimmed));
81 try_family!(backup::try_parse(&upper, &parts, trimmed));
82 try_family!(copy_from::try_parse(&upper, &parts, trimmed));
84 try_family!(copy_to::try_parse(&upper, trimmed));
86 try_family!(grant::try_parse(&upper, &parts, trimmed));
87 try_family!(user_auth::try_parse(&upper, &parts, trimmed));
88 try_family!(oidc_provider::try_parse(&upper, &parts, trimmed));
89 try_family!(change_stream::try_parse(&upper, &parts, trimmed));
90 try_family!(rls::try_parse(&upper, &parts, trimmed));
91 try_family!(redaction::try_parse(&upper, &parts, trimmed));
92 try_family!(materialized_view::try_parse(&upper, &parts, trimmed));
93 try_family!(synonym_group::try_parse(&upper, &parts, trimmed));
94 try_family!(custom_type::try_parse(&upper, &parts, trimmed));
95 try_family!(database::try_parse(&upper, &parts, trimmed));
96 try_family!(tenant::try_parse(&upper, &parts, trimmed));
97 None
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::ddl_ast::statement::{
104 AuthStmt, AutomationStmt, ClusterStmt, CollectionStmt, DatabaseStmt, GraphStmt,
105 };
106 use crate::error::SqlError;
107
108 fn ok(sql: &str) -> NodedbStatement {
110 parse(sql)
111 .expect("expected Some, got None")
112 .expect("expected Ok, got Err")
113 }
114
115 fn assert_reserved(sql: &str) {
117 match parse(sql) {
118 Some(Err(SqlError::ReservedIdentifier { .. })) => {}
119 other => panic!("expected Some(Err(ReservedIdentifier)), got {other:?}"),
120 }
121 }
122
123 #[test]
124 fn parse_create_collection() {
125 let stmt = ok("CREATE COLLECTION users (id INT, name TEXT)");
126 match stmt {
127 NodedbStatement::Collection(CollectionStmt::CreateCollection {
128 name,
129 if_not_exists,
130 ..
131 }) => {
132 assert_eq!(name, "users");
133 assert!(!if_not_exists);
134 }
135 other => panic!("expected CreateCollection, got {other:?}"),
136 }
137 }
138
139 #[test]
140 fn parse_create_collection_if_not_exists() {
141 let stmt = ok("CREATE COLLECTION IF NOT EXISTS users");
142 match stmt {
143 NodedbStatement::Collection(CollectionStmt::CreateCollection {
144 name,
145 if_not_exists,
146 ..
147 }) => {
148 assert_eq!(name, "users");
149 assert!(if_not_exists);
150 }
151 other => panic!("expected CreateCollection, got {other:?}"),
152 }
153 }
154
155 #[test]
156 fn parse_drop_collection() {
157 let stmt = ok("DROP COLLECTION users");
158 assert_eq!(
159 stmt,
160 NodedbStatement::Collection(CollectionStmt::DropCollection {
161 name: "users".into(),
162 if_exists: false,
163 purge: false,
164 cascade: false,
165 cascade_force: false,
166 })
167 );
168 }
169
170 #[test]
171 fn parse_drop_collection_if_exists() {
172 let stmt = ok("DROP COLLECTION IF EXISTS users");
173 assert_eq!(
174 stmt,
175 NodedbStatement::Collection(CollectionStmt::DropCollection {
176 name: "users".into(),
177 if_exists: true,
178 purge: false,
179 cascade: false,
180 cascade_force: false,
181 })
182 );
183 }
184
185 #[test]
186 fn parse_drop_collection_purge() {
187 let stmt = ok("DROP COLLECTION users PURGE");
188 assert_eq!(
189 stmt,
190 NodedbStatement::Collection(CollectionStmt::DropCollection {
191 name: "users".into(),
192 if_exists: false,
193 purge: true,
194 cascade: false,
195 cascade_force: false,
196 })
197 );
198 }
199
200 #[test]
201 fn parse_drop_collection_cascade() {
202 let stmt = ok("DROP COLLECTION users CASCADE");
203 assert_eq!(
204 stmt,
205 NodedbStatement::Collection(CollectionStmt::DropCollection {
206 name: "users".into(),
207 if_exists: false,
208 purge: false,
209 cascade: true,
210 cascade_force: false,
211 })
212 );
213 }
214
215 #[test]
216 fn parse_drop_collection_purge_cascade() {
217 let stmt = ok("DROP COLLECTION users PURGE CASCADE");
218 assert_eq!(
219 stmt,
220 NodedbStatement::Collection(CollectionStmt::DropCollection {
221 name: "users".into(),
222 if_exists: false,
223 purge: true,
224 cascade: true,
225 cascade_force: false,
226 })
227 );
228 }
229
230 #[test]
231 fn parse_drop_collection_cascade_force() {
232 let stmt = ok("DROP COLLECTION users CASCADE FORCE");
233 assert_eq!(
234 stmt,
235 NodedbStatement::Collection(CollectionStmt::DropCollection {
236 name: "users".into(),
237 if_exists: false,
238 purge: false,
239 cascade: true,
240 cascade_force: true,
241 })
242 );
243 }
244
245 #[test]
246 fn parse_undrop_collection() {
247 let stmt = ok("UNDROP COLLECTION users");
248 assert_eq!(
249 stmt,
250 NodedbStatement::Collection(CollectionStmt::UndropCollection {
251 name: "users".into()
252 })
253 );
254 }
255
256 #[test]
257 fn parse_undrop_table_alias() {
258 let stmt = ok("UNDROP TABLE users");
259 assert_eq!(
260 stmt,
261 NodedbStatement::Collection(CollectionStmt::UndropCollection {
262 name: "users".into()
263 })
264 );
265 }
266
267 #[test]
268 fn parse_show_nodes() {
269 assert_eq!(
270 parse("SHOW NODES"),
271 Some(Ok(NodedbStatement::Cluster(ClusterStmt::ShowNodes)))
272 );
273 }
274
275 #[test]
276 fn parse_show_cluster() {
277 assert_eq!(
278 parse("SHOW CLUSTER"),
279 Some(Ok(NodedbStatement::Cluster(ClusterStmt::ShowCluster)))
280 );
281 }
282
283 #[test]
284 fn parse_create_trigger() {
285 let stmt = ok(
286 "CREATE OR REPLACE SYNC TRIGGER on_insert AFTER INSERT ON orders FOR EACH ROW BEGIN RETURN; END",
287 );
288 match stmt {
289 NodedbStatement::Automation(AutomationStmt::CreateTrigger {
290 or_replace,
291 execution_mode,
292 timing,
293 ..
294 }) => {
295 assert!(or_replace);
296 assert_eq!(execution_mode, "SYNC");
297 assert_eq!(timing, "AFTER");
298 }
299 other => panic!("expected CreateTrigger, got {other:?}"),
300 }
301 }
302
303 #[test]
304 fn parse_drop_index_if_exists() {
305 let stmt = ok("DROP INDEX IF EXISTS idx_name");
306 match stmt {
307 NodedbStatement::Collection(CollectionStmt::DropIndex {
308 name, if_exists, ..
309 }) => {
310 assert_eq!(name, "idx_name");
311 assert!(if_exists);
312 }
313 other => panic!("expected DropIndex, got {other:?}"),
314 }
315 }
316
317 #[test]
318 fn parse_analyze() {
319 assert_eq!(
320 parse("ANALYZE users"),
321 Some(Ok(NodedbStatement::Cluster(ClusterStmt::Analyze {
322 collection: Some("users".into()),
323 })))
324 );
325 assert_eq!(
326 parse("ANALYZE"),
327 Some(Ok(NodedbStatement::Cluster(ClusterStmt::Analyze {
328 collection: None
329 })))
330 );
331 }
332
333 #[test]
334 fn parse_create_table_plain() {
335 let stmt = ok("CREATE TABLE foo (id INT, name TEXT)");
336 match stmt {
337 NodedbStatement::Collection(CollectionStmt::CreateTable {
338 name,
339 if_not_exists,
340 ..
341 }) => {
342 assert_eq!(name, "foo");
343 assert!(!if_not_exists);
344 }
345 other => panic!("expected CreateTable, got {other:?}"),
346 }
347 }
348
349 #[test]
350 fn parse_create_table_if_not_exists() {
351 let stmt = ok("CREATE TABLE IF NOT EXISTS orders (id INT)");
352 match stmt {
353 NodedbStatement::Collection(CollectionStmt::CreateTable {
354 name,
355 if_not_exists,
356 ..
357 }) => {
358 assert_eq!(name, "orders");
359 assert!(if_not_exists);
360 }
361 other => panic!("expected CreateTable, got {other:?}"),
362 }
363 }
364
365 #[test]
366 fn create_collection_is_not_create_table() {
367 let stmt = ok("CREATE COLLECTION foo");
368 assert!(matches!(
369 stmt,
370 NodedbStatement::Collection(CollectionStmt::CreateCollection { .. })
371 ));
372 }
373
374 #[test]
375 fn non_ddl_returns_none() {
376 assert!(parse("SELECT * FROM users").is_none());
377 assert!(parse("INSERT INTO users VALUES (1)").is_none());
378 }
379
380 #[test]
383 fn keyword_inside_a_literal_does_not_pick_the_family() {
384 assert!(matches!(
385 ok("CREATE SCHEDULE s CRON '* * * * *' AS INSERT INTO log VALUES ('TRIGGER fired')"),
386 NodedbStatement::Automation(AutomationStmt::CreateSchedule { .. })
387 ));
388 assert!(matches!(
389 ok("CREATE USER u WITH PASSWORD 'a trigger b'"),
390 NodedbStatement::Auth(AuthStmt::CreateUser { .. })
391 ));
392 }
393
394 #[test]
395 fn create_function_returns_none() {
396 assert!(
399 parse("CREATE OR REPLACE FUNCTION double_int(x INT) RETURNS INT AS SELECT x * 2")
400 .is_none(),
401 "expected None for CREATE FUNCTION"
402 );
403 assert!(
404 parse("CREATE FUNCTION foo(x INT) RETURNS INT AS SELECT x").is_none(),
405 "expected None for CREATE FUNCTION"
406 );
407 assert!(
408 parse("CREATE OR REPLACE PROCEDURE noop_proc() AS BEGIN END").is_none(),
409 "expected None for CREATE PROCEDURE"
410 );
411 }
412
413 #[test]
417 fn graph_dispatch_match_plain() {
418 let _ = parse("MATCH (a)-[]->(b) RETURN a");
419 }
420
421 #[test]
423 fn graph_dispatch_graph_keyword() {
424 let _ = parse("GRAPH something");
425 }
426
427 #[test]
429 fn graph_dispatch_block_comment_before_match() {
430 let _ = parse("/* hint */ MATCH (a) RETURN a");
431 }
432
433 #[test]
435 fn graph_dispatch_optional_match() {
436 let _ = parse("OPTIONAL MATCH (a) RETURN a");
437 }
438
439 #[test]
442 fn graph_dispatch_optional_non_match_does_not_route() {
443 assert!(parse("OPTIONAL FOO").is_none());
444 }
445
446 #[test]
447 fn graph_dispatch_select_with_match_in_string() {
448 assert!(parse("SELECT * FROM t WHERE name = 'MATCH'").is_none());
449 }
450
451 #[test]
452 fn graph_dispatch_select_with_graph_in_string() {
453 assert!(parse("SELECT * FROM t WHERE name = 'GRAPH'").is_none());
454 }
455
456 #[test]
457 fn graph_dispatch_with_cte_does_not_route() {
458 assert!(parse("WITH cte AS (SELECT 1) SELECT * FROM cte").is_none());
459 }
460
461 #[test]
462 fn graph_dispatch_line_comment_match_then_select() {
463 assert!(parse("-- MATCH (a)\nSELECT 1").is_none());
464 }
465
466 #[test]
469 fn match_query_uses_body_field() {
470 let stmt = ok("MATCH (x)-[:l]->(y) RETURN x, y");
471 match stmt {
472 NodedbStatement::Graph(GraphStmt::MatchQuery { body }) => {
473 assert!(body.starts_with("MATCH"), "body must hold the original SQL");
474 }
475 other => panic!("expected MatchQuery, got {other:?}"),
476 }
477 }
478
479 #[test]
482 fn parse_add_materialized_sum_typed() {
483 let stmt = ok(
486 "ALTER COLLECTION accounts ADD COLUMN balance DECIMAL AS MATERIALIZED_SUM \
487 SOURCE orders ON orders.account_id = accounts.id VALUE orders.amount",
488 );
489 match stmt {
490 NodedbStatement::Collection(CollectionStmt::AlterCollection { name, operation }) => {
491 assert_eq!(name, "accounts");
492 match operation {
493 crate::ddl_ast::AlterCollectionOp::AddMaterializedSum {
494 target_collection,
495 target_column,
496 target_column_type,
497 source_collection,
498 join_column,
499 value_expr,
500 } => {
501 assert_eq!(target_collection, "accounts");
502 assert_eq!(target_column, "balance");
503 assert_eq!(target_column_type, "DECIMAL");
504 assert_eq!(source_collection, "orders");
505 assert_eq!(join_column, "account_id");
506 assert_eq!(value_expr, "amount");
507 }
508 other => panic!("expected AddMaterializedSum, got {other:?}"),
509 }
510 }
511 other => panic!("expected AlterCollection, got {other:?}"),
512 }
513 }
514
515 #[test]
516 fn parse_grant_role() {
517 let stmt = ok("GRANT ROLE admin TO alice");
518 match stmt {
519 NodedbStatement::Auth(AuthStmt::GrantRole { roles, grantee }) => {
520 assert_eq!(roles, vec!["admin"]);
521 assert_eq!(grantee, "alice");
522 }
523 other => panic!("expected GrantRole, got {other:?}"),
524 }
525 }
526
527 #[test]
528 fn parse_create_sequence_if_not_exists() {
529 let stmt = ok("CREATE SEQUENCE IF NOT EXISTS my_seq START 1");
530 match stmt {
531 NodedbStatement::Collection(CollectionStmt::CreateSequence {
532 name,
533 if_not_exists,
534 ..
535 }) => {
536 assert_eq!(name, "my_seq");
537 assert!(if_not_exists);
538 }
539 other => panic!("expected CreateSequence, got {other:?}"),
540 }
541 }
542
543 #[test]
544 fn parse_restore_dry_run() {
545 let stmt = ok("RESTORE TENANT 1 FROM '/tmp/backup' DRY RUN");
546 match stmt {
547 NodedbStatement::Database(DatabaseStmt::RestoreTenant {
548 dry_run,
549 force,
550 tenant_id,
551 }) => {
552 assert!(dry_run);
553 assert!(!force);
554 assert_eq!(tenant_id, "1");
555 }
556 other => panic!("expected RestoreTenant, got {other:?}"),
557 }
558 }
559
560 #[test]
561 fn parse_restore_force() {
562 let stmt = ok("RESTORE TENANT 1 FROM '/tmp/backup' FORCE");
563 match stmt {
564 NodedbStatement::Database(DatabaseStmt::RestoreTenant {
565 dry_run,
566 force,
567 tenant_id,
568 }) => {
569 assert!(!dry_run);
570 assert!(force);
571 assert_eq!(tenant_id, "1");
572 }
573 other => panic!("expected RestoreTenant, got {other:?}"),
574 }
575 }
576
577 #[test]
580 fn create_table_reserved_name_is_err() {
581 assert_reserved("CREATE TABLE match (id INT)");
582 }
583
584 #[test]
585 fn create_table_quoted_reserved_name_is_ok() {
586 let stmt = ok(r#"CREATE TABLE "match" (id INT)"#);
587 match stmt {
588 NodedbStatement::Collection(CollectionStmt::CreateTable { name, .. }) => {
589 assert_eq!(name, "match")
590 }
591 other => panic!("expected CreateTable, got {other:?}"),
592 }
593 }
594
595 #[test]
596 fn create_collection_reserved_name_is_err() {
597 assert_reserved("CREATE COLLECTION upsert (id INT)");
598 }
599
600 #[test]
601 fn create_table_reserved_column_is_err() {
602 assert_reserved("CREATE TABLE foo (graph INT)");
603 }
604
605 #[test]
606 fn create_table_quoted_reserved_column_is_ok() {
607 let stmt = ok(r#"CREATE TABLE foo ("graph" INT)"#);
608 match stmt {
609 NodedbStatement::Collection(CollectionStmt::CreateTable { columns, .. }) => {
610 assert_eq!(columns[0].0, "graph");
611 }
612 other => panic!("expected CreateTable, got {other:?}"),
613 }
614 }
615
616 #[test]
619 fn reserved_graph() {
620 assert_reserved("CREATE TABLE graph (id INT)");
621 let stmt = ok(r#"CREATE TABLE "graph" (id INT)"#);
622 assert!(matches!(
623 stmt,
624 NodedbStatement::Collection(CollectionStmt::CreateTable { .. })
625 ));
626 }
627
628 #[test]
629 fn reserved_match() {
630 assert_reserved("CREATE TABLE match (id INT)");
631 let stmt = ok(r#"CREATE TABLE "match" (id INT)"#);
632 assert!(matches!(
633 stmt,
634 NodedbStatement::Collection(CollectionStmt::CreateTable { .. })
635 ));
636 }
637
638 #[test]
639 fn reserved_optional() {
640 assert_reserved("CREATE TABLE optional (id INT)");
641 let stmt = ok(r#"CREATE TABLE "optional" (id INT)"#);
642 assert!(matches!(
643 stmt,
644 NodedbStatement::Collection(CollectionStmt::CreateTable { .. })
645 ));
646 }
647
648 #[test]
649 fn reserved_upsert() {
650 assert_reserved("CREATE COLLECTION upsert (id INT)");
651 let stmt = ok(r#"CREATE COLLECTION "upsert" (id INT)"#);
652 assert!(matches!(
653 stmt,
654 NodedbStatement::Collection(CollectionStmt::CreateCollection { .. })
655 ));
656 }
657
658 #[test]
659 fn reserved_undrop() {
660 assert_reserved("CREATE TABLE undrop (id INT)");
661 let stmt = ok(r#"CREATE TABLE "undrop" (id INT)"#);
662 assert!(matches!(
663 stmt,
664 NodedbStatement::Collection(CollectionStmt::CreateTable { .. })
665 ));
666 }
667
668 #[test]
669 fn reserved_purge() {
670 assert_reserved("CREATE TABLE purge (id INT)");
671 let stmt = ok(r#"CREATE TABLE "purge" (id INT)"#);
672 assert!(matches!(
673 stmt,
674 NodedbStatement::Collection(CollectionStmt::CreateTable { .. })
675 ));
676 }
677
678 #[test]
679 fn reserved_cascade() {
680 assert_reserved("CREATE TABLE cascade (id INT)");
681 let stmt = ok(r#"CREATE TABLE "cascade" (id INT)"#);
682 assert!(matches!(
683 stmt,
684 NodedbStatement::Collection(CollectionStmt::CreateTable { .. })
685 ));
686 }
687
688 #[test]
689 fn reserved_search() {
690 assert_reserved("CREATE TABLE search (id INT)");
691 let stmt = ok(r#"CREATE TABLE "search" (id INT)"#);
692 assert!(matches!(
693 stmt,
694 NodedbStatement::Collection(CollectionStmt::CreateTable { .. })
695 ));
696 }
697
698 #[test]
699 fn reserved_crdt() {
700 assert_reserved("CREATE TABLE crdt (id INT)");
701 let stmt = ok(r#"CREATE TABLE "crdt" (id INT)"#);
702 assert!(matches!(
703 stmt,
704 NodedbStatement::Collection(CollectionStmt::CreateTable { .. })
705 ));
706 }
707}