Skip to main content

nodedb_sql/ddl_ast/parse/
dispatch.rs

1//! Dispatcher: try each DDL family's `try_parse` in turn.
2
3use super::{
4    alert, backup, change_stream, cluster_admin, collection, index, maintenance, materialized_view,
5    retention, rls, schedule, sequence, trigger, user_auth,
6};
7use crate::ddl_ast::graph_parse;
8use crate::ddl_ast::statement::NodedbStatement;
9
10/// Try to parse a DDL statement from raw SQL. Returns `None` for
11/// non-DDL queries (SELECT, INSERT, etc.) that should flow through
12/// the normal planner.
13pub fn parse(sql: &str) -> Option<NodedbStatement> {
14    let trimmed = sql.trim();
15    if trimmed.is_empty() {
16        return None;
17    }
18    let upper = trimmed.to_uppercase();
19    let parts: Vec<&str> = trimmed.split_whitespace().collect();
20    if parts.is_empty() {
21        return None;
22    }
23
24    // Graph DSL (`GRAPH ...`, `MATCH ...`) has its own tokenising
25    // parser — delegate early so the string-prefix branches below
26    // cannot accidentally shadow quoted values that contain DSL
27    // keywords.
28    if upper.starts_with("GRAPH ")
29        || upper.starts_with("MATCH ")
30        || upper.starts_with("OPTIONAL MATCH ")
31    {
32        return graph_parse::try_parse(trimmed);
33    }
34
35    // Dispatch by family. Order matters only where prefixes overlap
36    // (e.g. DESCRIBE vs DESCRIBE SEQUENCE — handled inside each
37    // family's `try_parse`).
38    collection::try_parse(&upper, &parts, trimmed)
39        .or_else(|| index::try_parse(&upper, &parts, trimmed))
40        .or_else(|| trigger::try_parse(&upper, &parts, trimmed))
41        .or_else(|| schedule::try_parse(&upper, &parts, trimmed))
42        .or_else(|| sequence::try_parse(&upper, &parts, trimmed))
43        .or_else(|| alert::try_parse(&upper, &parts, trimmed))
44        .or_else(|| retention::try_parse(&upper, &parts, trimmed))
45        .or_else(|| cluster_admin::try_parse(&upper, &parts, trimmed))
46        .or_else(|| maintenance::try_parse(&upper, &parts, trimmed))
47        .or_else(|| backup::try_parse(&upper, &parts, trimmed))
48        .or_else(|| user_auth::try_parse(&upper, &parts, trimmed))
49        .or_else(|| change_stream::try_parse(&upper, &parts, trimmed))
50        .or_else(|| rls::try_parse(&upper, &parts, trimmed))
51        .or_else(|| materialized_view::try_parse(&upper, &parts, trimmed))
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn parse_create_collection() {
60        let stmt = parse("CREATE COLLECTION users (id INT, name TEXT)").unwrap();
61        match stmt {
62            NodedbStatement::CreateCollection {
63                name,
64                if_not_exists,
65                ..
66            } => {
67                assert_eq!(name, "users");
68                assert!(!if_not_exists);
69            }
70            other => panic!("expected CreateCollection, got {other:?}"),
71        }
72    }
73
74    #[test]
75    fn parse_create_collection_if_not_exists() {
76        let stmt = parse("CREATE COLLECTION IF NOT EXISTS users").unwrap();
77        match stmt {
78            NodedbStatement::CreateCollection {
79                name,
80                if_not_exists,
81                ..
82            } => {
83                assert_eq!(name, "users");
84                assert!(if_not_exists);
85            }
86            other => panic!("expected CreateCollection, got {other:?}"),
87        }
88    }
89
90    #[test]
91    fn parse_drop_collection() {
92        let stmt = parse("DROP COLLECTION users").unwrap();
93        assert_eq!(
94            stmt,
95            NodedbStatement::DropCollection {
96                name: "users".into(),
97                if_exists: false,
98                purge: false,
99                cascade: false,
100                cascade_force: false,
101            }
102        );
103    }
104
105    #[test]
106    fn parse_drop_collection_if_exists() {
107        let stmt = parse("DROP COLLECTION IF EXISTS users").unwrap();
108        assert_eq!(
109            stmt,
110            NodedbStatement::DropCollection {
111                name: "users".into(),
112                if_exists: true,
113                purge: false,
114                cascade: false,
115                cascade_force: false,
116            }
117        );
118    }
119
120    #[test]
121    fn parse_drop_collection_purge() {
122        let stmt = parse("DROP COLLECTION users PURGE").unwrap();
123        assert_eq!(
124            stmt,
125            NodedbStatement::DropCollection {
126                name: "users".into(),
127                if_exists: false,
128                purge: true,
129                cascade: false,
130                cascade_force: false,
131            }
132        );
133    }
134
135    #[test]
136    fn parse_drop_collection_cascade() {
137        let stmt = parse("DROP COLLECTION users CASCADE").unwrap();
138        assert_eq!(
139            stmt,
140            NodedbStatement::DropCollection {
141                name: "users".into(),
142                if_exists: false,
143                purge: false,
144                cascade: true,
145                cascade_force: false,
146            }
147        );
148    }
149
150    #[test]
151    fn parse_drop_collection_purge_cascade() {
152        let stmt = parse("DROP COLLECTION users PURGE CASCADE").unwrap();
153        assert_eq!(
154            stmt,
155            NodedbStatement::DropCollection {
156                name: "users".into(),
157                if_exists: false,
158                purge: true,
159                cascade: true,
160                cascade_force: false,
161            }
162        );
163    }
164
165    #[test]
166    fn parse_drop_collection_cascade_force() {
167        let stmt = parse("DROP COLLECTION users CASCADE FORCE").unwrap();
168        assert_eq!(
169            stmt,
170            NodedbStatement::DropCollection {
171                name: "users".into(),
172                if_exists: false,
173                purge: false,
174                cascade: true,
175                cascade_force: true,
176            }
177        );
178    }
179
180    #[test]
181    fn parse_undrop_collection() {
182        let stmt = parse("UNDROP COLLECTION users").unwrap();
183        assert_eq!(
184            stmt,
185            NodedbStatement::UndropCollection {
186                name: "users".into()
187            }
188        );
189    }
190
191    #[test]
192    fn parse_undrop_table_alias() {
193        let stmt = parse("UNDROP TABLE users").unwrap();
194        assert_eq!(
195            stmt,
196            NodedbStatement::UndropCollection {
197                name: "users".into()
198            }
199        );
200    }
201
202    #[test]
203    fn parse_show_nodes() {
204        assert_eq!(parse("SHOW NODES"), Some(NodedbStatement::ShowNodes));
205    }
206
207    #[test]
208    fn parse_show_cluster() {
209        assert_eq!(parse("SHOW CLUSTER"), Some(NodedbStatement::ShowCluster));
210    }
211
212    #[test]
213    fn parse_create_trigger() {
214        let stmt = parse("CREATE OR REPLACE SYNC TRIGGER on_insert ...").unwrap();
215        match stmt {
216            NodedbStatement::CreateTrigger {
217                or_replace,
218                sync,
219                deferred,
220                ..
221            } => {
222                assert!(or_replace);
223                assert!(sync);
224                assert!(!deferred);
225            }
226            other => panic!("expected CreateTrigger, got {other:?}"),
227        }
228    }
229
230    #[test]
231    fn parse_drop_index_if_exists() {
232        let stmt = parse("DROP INDEX IF EXISTS idx_name").unwrap();
233        match stmt {
234            NodedbStatement::DropIndex {
235                name, if_exists, ..
236            } => {
237                assert_eq!(name, "idx_name");
238                assert!(if_exists);
239            }
240            other => panic!("expected DropIndex, got {other:?}"),
241        }
242    }
243
244    #[test]
245    fn parse_analyze() {
246        assert_eq!(
247            parse("ANALYZE users"),
248            Some(NodedbStatement::Analyze {
249                collection: Some("users".into()),
250            })
251        );
252        assert_eq!(
253            parse("ANALYZE"),
254            Some(NodedbStatement::Analyze { collection: None })
255        );
256    }
257
258    #[test]
259    fn non_ddl_returns_none() {
260        assert!(parse("SELECT * FROM users").is_none());
261        assert!(parse("INSERT INTO users VALUES (1)").is_none());
262    }
263
264    #[test]
265    fn parse_grant_role() {
266        let stmt = parse("GRANT ROLE admin TO alice").unwrap();
267        match stmt {
268            NodedbStatement::GrantRole { raw_sql } => {
269                assert!(raw_sql.contains("admin"));
270            }
271            other => panic!("expected GrantRole, got {other:?}"),
272        }
273    }
274
275    #[test]
276    fn parse_create_sequence_if_not_exists() {
277        let stmt = parse("CREATE SEQUENCE IF NOT EXISTS my_seq START 1").unwrap();
278        match stmt {
279            NodedbStatement::CreateSequence {
280                name,
281                if_not_exists,
282                ..
283            } => {
284                assert_eq!(name, "my_seq");
285                assert!(if_not_exists);
286            }
287            other => panic!("expected CreateSequence, got {other:?}"),
288        }
289    }
290
291    #[test]
292    fn parse_restore_dry_run() {
293        let stmt = parse("RESTORE TENANT 1 FROM '/tmp/backup' DRY RUN").unwrap();
294        match stmt {
295            NodedbStatement::RestoreTenant { dry_run, .. } => {
296                assert!(dry_run);
297            }
298            other => panic!("expected RestoreTenant, got {other:?}"),
299        }
300    }
301}