1use super::{
6 alert, backup, change_stream, cluster_admin, collection, conflict_policy, copy_from, copy_to,
7 custom_type, database, index, maintenance, materialized_view, oidc_provider, retention, rls,
8 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!(conflict_policy::try_parse(&upper, &parts, trimmed));
65 try_family!(collection::try_parse(&upper, &parts, trimmed));
66 try_family!(index::try_parse(&upper, &parts, trimmed));
67 try_family!(trigger::try_parse(&upper, &parts, trimmed));
68 try_family!(schedule::try_parse(&upper, &parts, trimmed));
69 try_family!(sequence::try_parse(&upper, &parts, trimmed));
70 try_family!(alert::try_parse(&upper, &parts, trimmed));
71 try_family!(retention::try_parse(&upper, &parts, trimmed));
72 try_family!(cluster_admin::try_parse(&upper, &parts, trimmed));
73 try_family!(maintenance::try_parse(&upper, &parts, trimmed));
74 try_family!(backup::try_parse(&upper, &parts, trimmed));
75 try_family!(copy_from::try_parse(&upper, &parts, trimmed));
77 try_family!(copy_to::try_parse(&upper, trimmed));
79 try_family!(user_auth::try_parse(&upper, &parts, trimmed));
80 try_family!(oidc_provider::try_parse(&upper, &parts, trimmed));
81 try_family!(change_stream::try_parse(&upper, &parts, trimmed));
82 try_family!(rls::try_parse(&upper, &parts, trimmed));
83 try_family!(materialized_view::try_parse(&upper, &parts, trimmed));
84 try_family!(synonym_group::try_parse(&upper, &parts, trimmed));
85 try_family!(custom_type::try_parse(&upper, &parts, trimmed));
86 try_family!(database::try_parse(&upper, &parts, trimmed));
87 try_family!(tenant::try_parse(&upper, &parts, trimmed));
88 None
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::error::SqlError;
95
96 fn ok(sql: &str) -> NodedbStatement {
98 parse(sql)
99 .expect("expected Some, got None")
100 .expect("expected Ok, got Err")
101 }
102
103 fn assert_reserved(sql: &str) {
105 match parse(sql) {
106 Some(Err(SqlError::ReservedIdentifier { .. })) => {}
107 other => panic!("expected Some(Err(ReservedIdentifier)), got {other:?}"),
108 }
109 }
110
111 #[test]
112 fn parse_create_collection() {
113 let stmt = ok("CREATE COLLECTION users (id INT, name TEXT)");
114 match stmt {
115 NodedbStatement::CreateCollection {
116 name,
117 if_not_exists,
118 ..
119 } => {
120 assert_eq!(name, "users");
121 assert!(!if_not_exists);
122 }
123 other => panic!("expected CreateCollection, got {other:?}"),
124 }
125 }
126
127 #[test]
128 fn parse_create_collection_if_not_exists() {
129 let stmt = ok("CREATE COLLECTION IF NOT EXISTS users");
130 match stmt {
131 NodedbStatement::CreateCollection {
132 name,
133 if_not_exists,
134 ..
135 } => {
136 assert_eq!(name, "users");
137 assert!(if_not_exists);
138 }
139 other => panic!("expected CreateCollection, got {other:?}"),
140 }
141 }
142
143 #[test]
144 fn parse_drop_collection() {
145 let stmt = ok("DROP COLLECTION users");
146 assert_eq!(
147 stmt,
148 NodedbStatement::DropCollection {
149 name: "users".into(),
150 if_exists: false,
151 purge: false,
152 cascade: false,
153 cascade_force: false,
154 }
155 );
156 }
157
158 #[test]
159 fn parse_drop_collection_if_exists() {
160 let stmt = ok("DROP COLLECTION IF EXISTS users");
161 assert_eq!(
162 stmt,
163 NodedbStatement::DropCollection {
164 name: "users".into(),
165 if_exists: true,
166 purge: false,
167 cascade: false,
168 cascade_force: false,
169 }
170 );
171 }
172
173 #[test]
174 fn parse_drop_collection_purge() {
175 let stmt = ok("DROP COLLECTION users PURGE");
176 assert_eq!(
177 stmt,
178 NodedbStatement::DropCollection {
179 name: "users".into(),
180 if_exists: false,
181 purge: true,
182 cascade: false,
183 cascade_force: false,
184 }
185 );
186 }
187
188 #[test]
189 fn parse_drop_collection_cascade() {
190 let stmt = ok("DROP COLLECTION users CASCADE");
191 assert_eq!(
192 stmt,
193 NodedbStatement::DropCollection {
194 name: "users".into(),
195 if_exists: false,
196 purge: false,
197 cascade: true,
198 cascade_force: false,
199 }
200 );
201 }
202
203 #[test]
204 fn parse_drop_collection_purge_cascade() {
205 let stmt = ok("DROP COLLECTION users PURGE CASCADE");
206 assert_eq!(
207 stmt,
208 NodedbStatement::DropCollection {
209 name: "users".into(),
210 if_exists: false,
211 purge: true,
212 cascade: true,
213 cascade_force: false,
214 }
215 );
216 }
217
218 #[test]
219 fn parse_drop_collection_cascade_force() {
220 let stmt = ok("DROP COLLECTION users CASCADE FORCE");
221 assert_eq!(
222 stmt,
223 NodedbStatement::DropCollection {
224 name: "users".into(),
225 if_exists: false,
226 purge: false,
227 cascade: true,
228 cascade_force: true,
229 }
230 );
231 }
232
233 #[test]
234 fn parse_undrop_collection() {
235 let stmt = ok("UNDROP COLLECTION users");
236 assert_eq!(
237 stmt,
238 NodedbStatement::UndropCollection {
239 name: "users".into()
240 }
241 );
242 }
243
244 #[test]
245 fn parse_undrop_table_alias() {
246 let stmt = ok("UNDROP TABLE users");
247 assert_eq!(
248 stmt,
249 NodedbStatement::UndropCollection {
250 name: "users".into()
251 }
252 );
253 }
254
255 #[test]
256 fn parse_show_nodes() {
257 assert_eq!(parse("SHOW NODES"), Some(Ok(NodedbStatement::ShowNodes)));
258 }
259
260 #[test]
261 fn parse_show_cluster() {
262 assert_eq!(
263 parse("SHOW CLUSTER"),
264 Some(Ok(NodedbStatement::ShowCluster))
265 );
266 }
267
268 #[test]
269 fn parse_create_trigger() {
270 let stmt = ok(
271 "CREATE OR REPLACE SYNC TRIGGER on_insert AFTER INSERT ON orders FOR EACH ROW BEGIN RETURN; END",
272 );
273 match stmt {
274 NodedbStatement::CreateTrigger {
275 or_replace,
276 execution_mode,
277 timing,
278 ..
279 } => {
280 assert!(or_replace);
281 assert_eq!(execution_mode, "SYNC");
282 assert_eq!(timing, "AFTER");
283 }
284 other => panic!("expected CreateTrigger, got {other:?}"),
285 }
286 }
287
288 #[test]
289 fn parse_drop_index_if_exists() {
290 let stmt = ok("DROP INDEX IF EXISTS idx_name");
291 match stmt {
292 NodedbStatement::DropIndex {
293 name, if_exists, ..
294 } => {
295 assert_eq!(name, "idx_name");
296 assert!(if_exists);
297 }
298 other => panic!("expected DropIndex, got {other:?}"),
299 }
300 }
301
302 #[test]
303 fn parse_analyze() {
304 assert_eq!(
305 parse("ANALYZE users"),
306 Some(Ok(NodedbStatement::Analyze {
307 collection: Some("users".into()),
308 }))
309 );
310 assert_eq!(
311 parse("ANALYZE"),
312 Some(Ok(NodedbStatement::Analyze { collection: None }))
313 );
314 }
315
316 #[test]
317 fn parse_create_table_plain() {
318 let stmt = ok("CREATE TABLE foo (id INT, name TEXT)");
319 match stmt {
320 NodedbStatement::CreateTable {
321 name,
322 if_not_exists,
323 ..
324 } => {
325 assert_eq!(name, "foo");
326 assert!(!if_not_exists);
327 }
328 other => panic!("expected CreateTable, got {other:?}"),
329 }
330 }
331
332 #[test]
333 fn parse_create_table_if_not_exists() {
334 let stmt = ok("CREATE TABLE IF NOT EXISTS orders (id INT)");
335 match stmt {
336 NodedbStatement::CreateTable {
337 name,
338 if_not_exists,
339 ..
340 } => {
341 assert_eq!(name, "orders");
342 assert!(if_not_exists);
343 }
344 other => panic!("expected CreateTable, got {other:?}"),
345 }
346 }
347
348 #[test]
349 fn create_collection_is_not_create_table() {
350 let stmt = ok("CREATE COLLECTION foo");
351 assert!(matches!(stmt, NodedbStatement::CreateCollection { .. }));
352 }
353
354 #[test]
355 fn non_ddl_returns_none() {
356 assert!(parse("SELECT * FROM users").is_none());
357 assert!(parse("INSERT INTO users VALUES (1)").is_none());
358 }
359
360 #[test]
364 fn graph_dispatch_match_plain() {
365 let _ = parse("MATCH (a)-[]->(b) RETURN a");
366 }
367
368 #[test]
370 fn graph_dispatch_graph_keyword() {
371 let _ = parse("GRAPH something");
372 }
373
374 #[test]
376 fn graph_dispatch_block_comment_before_match() {
377 let _ = parse("/* hint */ MATCH (a) RETURN a");
378 }
379
380 #[test]
382 fn graph_dispatch_optional_match() {
383 let _ = parse("OPTIONAL MATCH (a) RETURN a");
384 }
385
386 #[test]
389 fn graph_dispatch_optional_non_match_does_not_route() {
390 assert!(parse("OPTIONAL FOO").is_none());
391 }
392
393 #[test]
394 fn graph_dispatch_select_with_match_in_string() {
395 assert!(parse("SELECT * FROM t WHERE name = 'MATCH'").is_none());
396 }
397
398 #[test]
399 fn graph_dispatch_select_with_graph_in_string() {
400 assert!(parse("SELECT * FROM t WHERE name = 'GRAPH'").is_none());
401 }
402
403 #[test]
404 fn graph_dispatch_with_cte_does_not_route() {
405 assert!(parse("WITH cte AS (SELECT 1) SELECT * FROM cte").is_none());
406 }
407
408 #[test]
409 fn graph_dispatch_line_comment_match_then_select() {
410 assert!(parse("-- MATCH (a)\nSELECT 1").is_none());
411 }
412
413 #[test]
416 fn match_query_uses_body_field() {
417 let stmt = ok("MATCH (x)-[:l]->(y) RETURN x, y");
418 match stmt {
419 NodedbStatement::MatchQuery { body } => {
420 assert!(body.starts_with("MATCH"), "body must hold the original SQL");
421 }
422 other => panic!("expected MatchQuery, got {other:?}"),
423 }
424 }
425
426 #[test]
429 fn parse_add_materialized_sum_typed() {
430 let stmt = ok(
433 "ALTER COLLECTION accounts ADD COLUMN balance DECIMAL AS MATERIALIZED_SUM \
434 SOURCE orders ON orders.account_id = accounts.id VALUE orders.amount",
435 );
436 match stmt {
437 NodedbStatement::AlterCollection { name, operation } => {
438 assert_eq!(name, "accounts");
439 match operation {
440 crate::ddl_ast::AlterCollectionOp::AddMaterializedSum {
441 target_collection,
442 target_column,
443 source_collection,
444 join_column,
445 value_expr,
446 } => {
447 assert_eq!(target_collection, "accounts");
448 assert_eq!(target_column, "balance");
449 assert_eq!(source_collection, "orders");
450 assert_eq!(join_column, "account_id");
451 assert_eq!(value_expr, "amount");
452 }
453 other => panic!("expected AddMaterializedSum, got {other:?}"),
454 }
455 }
456 other => panic!("expected AlterCollection, got {other:?}"),
457 }
458 }
459
460 #[test]
461 fn parse_grant_role() {
462 let stmt = ok("GRANT ROLE admin TO alice");
463 match stmt {
464 NodedbStatement::GrantRole { role, username } => {
465 assert_eq!(role, "admin");
466 assert_eq!(username, "alice");
467 }
468 other => panic!("expected GrantRole, got {other:?}"),
469 }
470 }
471
472 #[test]
473 fn parse_create_sequence_if_not_exists() {
474 let stmt = ok("CREATE SEQUENCE IF NOT EXISTS my_seq START 1");
475 match stmt {
476 NodedbStatement::CreateSequence {
477 name,
478 if_not_exists,
479 ..
480 } => {
481 assert_eq!(name, "my_seq");
482 assert!(if_not_exists);
483 }
484 other => panic!("expected CreateSequence, got {other:?}"),
485 }
486 }
487
488 #[test]
489 fn parse_restore_dry_run() {
490 let stmt = ok("RESTORE TENANT 1 FROM '/tmp/backup' DRY RUN");
491 match stmt {
492 NodedbStatement::RestoreTenant { dry_run, tenant_id } => {
493 assert!(dry_run);
494 assert_eq!(tenant_id, "1");
495 }
496 other => panic!("expected RestoreTenant, got {other:?}"),
497 }
498 }
499
500 #[test]
503 fn create_table_reserved_name_is_err() {
504 assert_reserved("CREATE TABLE match (id INT)");
505 }
506
507 #[test]
508 fn create_table_quoted_reserved_name_is_ok() {
509 let stmt = ok(r#"CREATE TABLE "match" (id INT)"#);
510 match stmt {
511 NodedbStatement::CreateTable { name, .. } => assert_eq!(name, "match"),
512 other => panic!("expected CreateTable, got {other:?}"),
513 }
514 }
515
516 #[test]
517 fn create_collection_reserved_name_is_err() {
518 assert_reserved("CREATE COLLECTION upsert (id INT)");
519 }
520
521 #[test]
522 fn create_table_reserved_column_is_err() {
523 assert_reserved("CREATE TABLE foo (graph INT)");
524 }
525
526 #[test]
527 fn create_table_quoted_reserved_column_is_ok() {
528 let stmt = ok(r#"CREATE TABLE foo ("graph" INT)"#);
529 match stmt {
530 NodedbStatement::CreateTable { columns, .. } => {
531 assert_eq!(columns[0].0, "graph");
532 }
533 other => panic!("expected CreateTable, got {other:?}"),
534 }
535 }
536
537 #[test]
540 fn reserved_graph() {
541 assert_reserved("CREATE TABLE graph (id INT)");
542 let stmt = ok(r#"CREATE TABLE "graph" (id INT)"#);
543 assert!(matches!(stmt, NodedbStatement::CreateTable { .. }));
544 }
545
546 #[test]
547 fn reserved_match() {
548 assert_reserved("CREATE TABLE match (id INT)");
549 let stmt = ok(r#"CREATE TABLE "match" (id INT)"#);
550 assert!(matches!(stmt, NodedbStatement::CreateTable { .. }));
551 }
552
553 #[test]
554 fn reserved_optional() {
555 assert_reserved("CREATE TABLE optional (id INT)");
556 let stmt = ok(r#"CREATE TABLE "optional" (id INT)"#);
557 assert!(matches!(stmt, NodedbStatement::CreateTable { .. }));
558 }
559
560 #[test]
561 fn reserved_upsert() {
562 assert_reserved("CREATE COLLECTION upsert (id INT)");
563 let stmt = ok(r#"CREATE COLLECTION "upsert" (id INT)"#);
564 assert!(matches!(stmt, NodedbStatement::CreateCollection { .. }));
565 }
566
567 #[test]
568 fn reserved_undrop() {
569 assert_reserved("CREATE TABLE undrop (id INT)");
570 let stmt = ok(r#"CREATE TABLE "undrop" (id INT)"#);
571 assert!(matches!(stmt, NodedbStatement::CreateTable { .. }));
572 }
573
574 #[test]
575 fn reserved_purge() {
576 assert_reserved("CREATE TABLE purge (id INT)");
577 let stmt = ok(r#"CREATE TABLE "purge" (id INT)"#);
578 assert!(matches!(stmt, NodedbStatement::CreateTable { .. }));
579 }
580
581 #[test]
582 fn reserved_cascade() {
583 assert_reserved("CREATE TABLE cascade (id INT)");
584 let stmt = ok(r#"CREATE TABLE "cascade" (id INT)"#);
585 assert!(matches!(stmt, NodedbStatement::CreateTable { .. }));
586 }
587
588 #[test]
589 fn reserved_search() {
590 assert_reserved("CREATE TABLE search (id INT)");
591 let stmt = ok(r#"CREATE TABLE "search" (id INT)"#);
592 assert!(matches!(stmt, NodedbStatement::CreateTable { .. }));
593 }
594
595 #[test]
596 fn reserved_crdt() {
597 assert_reserved("CREATE TABLE crdt (id INT)");
598 let stmt = ok(r#"CREATE TABLE "crdt" (id INT)"#);
599 assert!(matches!(stmt, NodedbStatement::CreateTable { .. }));
600 }
601}