-
Notifications
You must be signed in to change notification settings - Fork 216
/
commands.rs
195 lines (168 loc) Β· 6.57 KB
/
commands.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
pub(crate) mod error;
#[cfg(test)]
mod tests;
use anyhow::Context;
use error::CliError;
use futures::FutureExt;
use migration_connector::{ConnectorError, ErrorKind, MigrationConnector};
use migration_core::CoreResult;
use quaint::prelude::SqlFamily;
use sql_migration_connector::SqlMigrationConnector;
use std::collections::HashMap;
use structopt::StructOpt;
use url::Url;
#[derive(Debug, StructOpt)]
pub(crate) struct Cli {
/// The connection string to the database
#[structopt(long, short = "d")]
datasource: String,
#[structopt(subcommand)]
command: CliCommand,
}
impl Cli {
pub(crate) async fn run(&self) -> ! {
match std::panic::AssertUnwindSafe(self.run_inner()).catch_unwind().await {
Ok(Ok(msg)) => {
tracing::info!("{}", msg);
std::process::exit(0);
}
Ok(Err(error)) => {
tracing::error!("{}", error);
let exit_code = error.exit_code();
serde_json::to_writer(std::io::stdout(), &error::render_error(error))
.expect("failed to write to stdout");
println!();
std::process::exit(exit_code)
}
Err(panic) => {
serde_json::to_writer(
std::io::stdout(),
&user_facing_errors::Error::from_panic_payload(panic.as_ref()),
)
.expect("failed to write to stdout");
println!();
std::process::exit(255);
}
}
}
pub(crate) async fn run_inner(&self) -> Result<String, CliError> {
match self.command {
CliCommand::CreateDatabase => create_database(&self.datasource).await,
CliCommand::CanConnectToDatabase => {
create_conn(&self.datasource, false).await?;
Ok("Connection successful".to_owned())
}
}
}
}
#[derive(Debug, StructOpt)]
enum CliCommand {
/// Create an empty database defined in the configuration string.
CreateDatabase,
/// Does the database connection string work?
CanConnectToDatabase,
}
async fn create_conn(datasource: &str, admin_mode: bool) -> CoreResult<(String, Box<SqlMigrationConnector>)> {
let mut url = Url::parse(datasource).expect("Invalid url in the datasource");
let sql_family = SqlFamily::from_scheme(url.scheme());
match sql_family {
Some(SqlFamily::Sqlite) => {
let connector = SqlMigrationConnector::new(datasource, "sqlite").await?;
Ok((datasource.to_owned(), Box::new(connector)))
}
Some(SqlFamily::Postgres) => {
let db_name = fetch_db_name(&url, "postgres");
let connector = if admin_mode {
create_postgres_admin_conn(url).await?
} else {
SqlMigrationConnector::new(url.as_str(), "postgres").await?
};
Ok((db_name, Box::new(connector)))
}
Some(SqlFamily::Mysql) => {
let db_name = fetch_db_name(&url, "mysql");
if admin_mode {
url.set_path("");
}
let inner = SqlMigrationConnector::new(url.as_str(), "mysql").await?;
Ok((db_name, Box::new(inner)))
}
None => unimplemented!("Connector {} is not supported yet", url.scheme()),
}
}
fn fetch_db_name(url: &Url, default: &str) -> String {
let result = match url.path_segments() {
Some(mut segments) => segments.next().unwrap_or(default),
None => default,
};
String::from(result)
}
async fn create_database(datasource: &str) -> Result<String, CliError> {
let url = split_database_string(datasource)
.and_then(|(prefix, rest)| SqlFamily::from_scheme(prefix).map(|family| (family, rest)));
match url {
Some((SqlFamily::Sqlite, path)) => {
let path = std::path::Path::new(path);
if path.exists() {
return Ok(String::new());
}
let dir = path.parent();
if let Some((dir, false)) = dir.map(|dir| (dir, dir.exists())) {
std::fs::create_dir_all(dir)
.context("Creating SQLite database parent directory.")
.map_err(|io_err| CliError::Other(io_err.into()))?;
}
create_conn(datasource, true).await?;
Ok(String::new())
}
Some(_) => {
let (db_name, conn) = create_conn(datasource, true).await?;
conn.create_database(&db_name).await?;
Ok(format!("Database '{}' created successfully.", db_name))
}
None => Err(CliError::Other(anyhow::anyhow!(
"Invalid URL or unsupported connector in the datasource ({:?})",
url
))),
}
}
/// Try to connect as an admin to a postgres database. We try to pick a default database from which
/// we can create another database.
async fn create_postgres_admin_conn(mut url: Url) -> CoreResult<SqlMigrationConnector> {
let candidate_default_databases = &["postgres", "template1"];
let mut params: HashMap<String, String> = url.query_pairs().into_owned().collect();
params.remove("schema");
let params: Vec<String> = params.into_iter().map(|(k, v)| format!("{}={}", k, v)).collect();
let params: String = params.join("&");
url.set_query(Some(¶ms));
let mut connector = None;
for database_name in candidate_default_databases {
url.set_path(&format!("/{}", database_name));
match SqlMigrationConnector::new(url.as_str(), "postgresql").await {
// If the database does not exist, try the next one.
Err(err) => match &err.kind {
migration_connector::ErrorKind::DatabaseDoesNotExist { .. } => (),
_other_outcome => {
connector = Some(Err(err));
break;
}
},
// If the outcome is anything else, use this.
other_outcome => {
connector = Some(other_outcome);
break;
}
}
}
let connector = connector
.ok_or_else(|| {
ConnectorError::from_kind(ErrorKind::DatabaseCreationFailed {
explanation: "Prisma could not connect to a default database (`postgres` or `template1`), it cannot create the specified database.".to_owned()
})
})??;
Ok(connector)
}
fn split_database_string(database_string: &str) -> Option<(&str, &str)> {
let mut split = database_string.splitn(2, ':');
split.next().and_then(|prefix| split.next().map(|rest| (prefix, rest)))
}