Skip to main content

nodedb_sql/parser/database_stmt/
parse.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Entry point for the database-statement parser.
4//!
5//! Delegates to `crate::ddl_ast::parse::database::try_parse`, which contains
6//! the full recursive-descent implementation. This module exists to mirror the
7//! `parser/array_stmt/` structure and expose a consistent public API surface
8//! from `parser/`.
9
10use crate::ddl_ast::statement::NodedbStatement;
11use crate::error::SqlError;
12
13/// Try to parse a database-level DDL statement from raw SQL.
14///
15/// Returns `Ok(None)` for SQL that does not match any database-DDL prefix.
16/// Returns `Ok(Some(stmt))` on success. Returns `Err(SqlError::Parse { .. })`
17/// when the SQL matches a database-DDL prefix but contains a parse error (e.g.
18/// missing required name token).
19pub fn try_parse_database_statement(sql: &str) -> Result<Option<NodedbStatement>, SqlError> {
20    let trimmed = sql.trim();
21    if trimmed.is_empty() {
22        return Ok(None);
23    }
24    let upper = trimmed.to_uppercase();
25    let parts: Vec<&str> = trimmed.split_whitespace().collect();
26    if parts.is_empty() {
27        return Ok(None);
28    }
29    crate::ddl_ast::parse::database::try_parse(&upper, &parts, trimmed).transpose()
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use crate::ddl_ast::statement::{AlterDatabaseOperation, DatabaseStmt, NodedbStatement};
36
37    fn ok(sql: &str) -> NodedbStatement {
38        try_parse_database_statement(sql)
39            .expect("expected Ok")
40            .expect("expected Some")
41    }
42
43    #[test]
44    fn parse_create_database() {
45        match ok("CREATE DATABASE mydb") {
46            NodedbStatement::Database(DatabaseStmt::CreateDatabase {
47                name,
48                if_not_exists,
49                ..
50            }) => {
51                assert_eq!(name, "mydb");
52                assert!(!if_not_exists);
53            }
54            other => panic!("unexpected: {other:?}"),
55        }
56    }
57
58    #[test]
59    fn parse_create_database_if_not_exists() {
60        match ok("CREATE DATABASE IF NOT EXISTS mydb") {
61            NodedbStatement::Database(DatabaseStmt::CreateDatabase {
62                name,
63                if_not_exists,
64                ..
65            }) => {
66                assert_eq!(name, "mydb");
67                assert!(if_not_exists);
68            }
69            other => panic!("unexpected: {other:?}"),
70        }
71    }
72
73    #[test]
74    fn parse_drop_database() {
75        match ok("DROP DATABASE mydb CASCADE") {
76            NodedbStatement::Database(DatabaseStmt::DropDatabase { name, cascade, .. }) => {
77                assert_eq!(name, "mydb");
78                assert!(cascade);
79            }
80            other => panic!("unexpected: {other:?}"),
81        }
82    }
83
84    #[test]
85    fn parse_drop_database_if_exists() {
86        match ok("DROP DATABASE IF EXISTS mydb") {
87            NodedbStatement::Database(DatabaseStmt::DropDatabase {
88                name, if_exists, ..
89            }) => {
90                assert_eq!(name, "mydb");
91                assert!(if_exists);
92            }
93            other => panic!("unexpected: {other:?}"),
94        }
95    }
96
97    #[test]
98    fn parse_alter_database_rename() {
99        match ok("ALTER DATABASE mydb RENAME TO newdb") {
100            NodedbStatement::Database(DatabaseStmt::AlterDatabase { name, operation }) => {
101                assert_eq!(name, "mydb");
102                assert_eq!(
103                    operation,
104                    AlterDatabaseOperation::Rename {
105                        new_name: "newdb".into()
106                    }
107                );
108            }
109            other => panic!("unexpected: {other:?}"),
110        }
111    }
112
113    #[test]
114    fn parse_alter_database_set_quota() {
115        match ok("ALTER DATABASE mydb SET QUOTA (max_qps = 500)") {
116            NodedbStatement::Database(DatabaseStmt::AlterDatabase { name, operation }) => {
117                assert_eq!(name, "mydb");
118                match operation {
119                    AlterDatabaseOperation::SetQuota(spec) => {
120                        assert_eq!(spec.max_qps, Some(500));
121                    }
122                    other => panic!("expected SetQuota, got {other:?}"),
123                }
124            }
125            other => panic!("unexpected: {other:?}"),
126        }
127    }
128
129    #[test]
130    fn parse_alter_database_materialize() {
131        match ok("ALTER DATABASE mydb MATERIALIZE") {
132            NodedbStatement::Database(DatabaseStmt::AlterDatabase { operation, .. }) => {
133                assert_eq!(operation, AlterDatabaseOperation::Materialize);
134            }
135            other => panic!("unexpected: {other:?}"),
136        }
137    }
138
139    #[test]
140    fn parse_alter_database_promote() {
141        match ok("ALTER DATABASE mydb PROMOTE") {
142            NodedbStatement::Database(DatabaseStmt::AlterDatabase { operation, .. }) => {
143                assert_eq!(operation, AlterDatabaseOperation::Promote);
144            }
145            other => panic!("unexpected: {other:?}"),
146        }
147    }
148
149    #[test]
150    fn parse_show_databases() {
151        assert_eq!(
152            try_parse_database_statement("SHOW DATABASES")
153                .unwrap()
154                .unwrap(),
155            NodedbStatement::Database(DatabaseStmt::ShowDatabases)
156        );
157    }
158
159    #[test]
160    fn parse_use_database() {
161        match ok("USE DATABASE mydb") {
162            NodedbStatement::Database(DatabaseStmt::UseDatabase { name }) => {
163                assert_eq!(name, "mydb")
164            }
165            other => panic!("unexpected: {other:?}"),
166        }
167    }
168
169    #[test]
170    fn passthrough_non_database_sql() {
171        assert!(
172            try_parse_database_statement("SELECT * FROM t")
173                .unwrap()
174                .is_none()
175        );
176        assert!(
177            try_parse_database_statement("CREATE COLLECTION users")
178                .unwrap()
179                .is_none()
180        );
181        assert!(
182            try_parse_database_statement("INSERT INTO foo VALUES (1)")
183                .unwrap()
184                .is_none()
185        );
186    }
187
188    #[test]
189    fn parse_clone_database() {
190        use crate::ddl_ast::CloneAsOf;
191        match ok("CLONE DATABASE new_db FROM source_db") {
192            NodedbStatement::Database(DatabaseStmt::CloneDatabase {
193                new_name,
194                source_name,
195                as_of,
196            }) => {
197                assert_eq!(new_name, "new_db");
198                assert_eq!(source_name, "source_db");
199                assert_eq!(as_of, CloneAsOf::Latest);
200            }
201            other => panic!("unexpected: {other:?}"),
202        }
203    }
204
205    #[test]
206    fn parse_mirror_database() {
207        use crate::ddl_ast::MirrorMode;
208        match ok("MIRROR DATABASE replica FROM prod-us.source") {
209            NodedbStatement::Database(DatabaseStmt::MirrorDatabase {
210                local_name,
211                source_cluster,
212                source_database,
213                mode,
214            }) => {
215                assert_eq!(local_name, "replica");
216                assert_eq!(source_cluster, "prod-us");
217                assert_eq!(source_database, "source");
218                assert_eq!(mode, MirrorMode::Async);
219            }
220            other => panic!("unexpected: {other:?}"),
221        }
222    }
223
224    #[test]
225    fn parse_backup_database() {
226        match ok("BACKUP DATABASE mydb TO 's3://bucket/path'") {
227            NodedbStatement::Database(DatabaseStmt::BackupDatabase { name, uri }) => {
228                assert_eq!(name, "mydb");
229                assert!(!uri.is_empty());
230            }
231            other => panic!("unexpected: {other:?}"),
232        }
233    }
234
235    #[test]
236    fn parse_restore_database() {
237        match ok("RESTORE DATABASE mydb FROM 's3://bucket/path'") {
238            NodedbStatement::Database(DatabaseStmt::RestoreDatabase { name, uri }) => {
239                assert_eq!(name, "mydb");
240                assert!(!uri.is_empty());
241            }
242            other => panic!("unexpected: {other:?}"),
243        }
244    }
245
246    #[test]
247    fn parse_move_tenant() {
248        match ok("MOVE TENANT t1 FROM db_a TO db_b") {
249            NodedbStatement::Database(DatabaseStmt::MoveTenant {
250                tenant_name,
251                from_db,
252                to_db,
253            }) => {
254                assert_eq!(tenant_name, "t1");
255                assert_eq!(from_db, "db_a");
256                assert_eq!(to_db, "db_b");
257            }
258            other => panic!("unexpected: {other:?}"),
259        }
260    }
261
262    #[test]
263    fn drop_missing_name_returns_error() {
264        let result = try_parse_database_statement("DROP DATABASE");
265        assert!(result.is_err() || result.unwrap().is_some());
266    }
267}