1use crate::sea_orm::{ConnectionTrait, Database, DatabaseBackend, DbErr};
11
12pub async fn create_database(url: &str) -> Result<(), DbErr> {
17 if is_sqlite(url) {
18 return create_sqlite(url).await;
19 }
20
21 let (server_url, db_name) = split_server_and_database(url)?;
22 let db = Database::connect(server_url).await?;
23 let backend = db.get_database_backend();
24 let quoted = quote_identifier(backend, &db_name)?;
25 db.execute_unprepared(&format!("CREATE DATABASE {quoted}"))
26 .await?;
27 Ok(())
28}
29
30fn is_sqlite(url: &str) -> bool {
31 url.starts_with("sqlite:")
32}
33
34async fn create_sqlite(url: &str) -> Result<(), DbErr> {
37 let raw = url
38 .strip_prefix("sqlite://")
39 .or_else(|| url.strip_prefix("sqlite:"))
40 .unwrap_or(url);
41 let path = raw.split('?').next().unwrap_or(raw);
42
43 if !path.is_empty() && !path.contains(":memory:") {
44 if let Some(parent) = std::path::Path::new(path).parent() {
45 if !parent.as_os_str().is_empty() {
46 std::fs::create_dir_all(parent)
47 .map_err(|e| DbErr::Custom(format!("failed to create directory: {e}")))?;
48 }
49 }
50 }
51
52 Database::connect(ensure_sqlite_rwc(url)).await?;
53 Ok(())
54}
55
56fn ensure_sqlite_rwc(url: &str) -> String {
58 if url.contains("mode=") {
59 url.to_string()
60 } else if url.contains('?') {
61 format!("{url}&mode=rwc")
62 } else {
63 format!("{url}?mode=rwc")
64 }
65}
66
67fn split_server_and_database(url: &str) -> Result<(String, String), DbErr> {
70 let mut parsed =
71 url::Url::parse(url).map_err(|e| DbErr::Custom(format!("invalid database URL: {e}")))?;
72 let db_name = parsed.path().trim_start_matches('/').to_string();
73 if db_name.is_empty() {
74 return Err(DbErr::Custom(
75 "database URL is missing a database name".to_string(),
76 ));
77 }
78
79 let scheme = parsed.scheme().to_string();
80 if scheme.starts_with("postgres") {
81 parsed.set_path("/postgres");
84 } else if scheme.starts_with("mysql") {
85 parsed.set_path("/");
87 } else {
88 return Err(DbErr::Custom(format!(
89 "unsupported database scheme '{scheme}' for create"
90 )));
91 }
92
93 Ok((parsed.to_string(), db_name))
94}
95
96fn quote_identifier(backend: DatabaseBackend, name: &str) -> Result<String, DbErr> {
98 match backend {
99 DatabaseBackend::Postgres => Ok(format!("\"{}\"", name.replace('"', "\"\""))),
100 DatabaseBackend::MySql => Ok(format!("`{}`", name.replace('`', "``"))),
101 DatabaseBackend::Sqlite => Err(DbErr::Custom(
102 "SQLite databases are created as files, not via CREATE DATABASE".to_string(),
103 )),
104 other => Err(DbErr::Custom(format!(
105 "unsupported database backend for create: {other:?}"
106 ))),
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn splits_postgres_url_to_maintenance_db() {
116 let (server, name) =
117 split_server_and_database("postgres://user:pass@localhost:5432/my_app_development")
118 .unwrap();
119 assert_eq!(server, "postgres://user:pass@localhost:5432/postgres");
120 assert_eq!(name, "my_app_development");
121 }
122
123 #[test]
124 fn splits_mysql_url_dropping_database() {
125 let (server, name) =
126 split_server_and_database("mysql://root@localhost:3306/my_app_test").unwrap();
127 assert_eq!(server, "mysql://root@localhost:3306/");
128 assert_eq!(name, "my_app_test");
129 }
130
131 #[test]
132 fn errors_when_database_name_missing() {
133 assert!(split_server_and_database("postgres://localhost").is_err());
134 }
135
136 #[test]
137 fn appends_rwc_to_sqlite_url() {
138 assert_eq!(
139 ensure_sqlite_rwc("sqlite://db/development.db"),
140 "sqlite://db/development.db?mode=rwc"
141 );
142 assert_eq!(
143 ensure_sqlite_rwc("sqlite://db/development.db?cache=shared"),
144 "sqlite://db/development.db?cache=shared&mode=rwc"
145 );
146 assert_eq!(
147 ensure_sqlite_rwc("sqlite://db/x.db?mode=rwc"),
148 "sqlite://db/x.db?mode=rwc"
149 );
150 }
151
152 #[test]
153 fn quotes_identifiers_per_backend() {
154 assert_eq!(
155 quote_identifier(DatabaseBackend::Postgres, "app").unwrap(),
156 "\"app\""
157 );
158 assert_eq!(
159 quote_identifier(DatabaseBackend::MySql, "app").unwrap(),
160 "`app`"
161 );
162 }
163}