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