feat: finish v1 of both cli and embedded migrations

This commit is contained in:
Ryan Leckey
2020-07-12 03:43:49 -07:00
parent b920aa1c55
commit 61e4a4f566
37 changed files with 1525 additions and 890 deletions

3
sqlx-cli/.gitignore vendored
View File

@@ -1,3 +0,0 @@
/target
Cargo.lock
.env

View File

@@ -27,15 +27,15 @@ path = "src/bin/cargo-sqlx.rs"
[dependencies]
dotenv = "0.15"
tokio = { version = "0.2", features = ["macros"] }
sqlx = { version = "0.4.0-pre", path = "..", default-features = false, features = [ "runtime-tokio", "offline" ] }
sqlx = { version = "0.4.0-pre", path = "..", default-features = false, features = [ "runtime-async-std", "migrate", "any", "offline" ] }
futures = "0.3"
structopt = "0.3"
clap = "3.0.0-beta.1"
chrono = "0.4"
anyhow = "1.0"
url = { version = "2.1.1", default-features = false }
async-trait = "0.1.30"
console = "0.10.0"
dialoguer = "0.5.0"
dialoguer = "0.6.2"
serde_json = { version = "1.0.53", features = ["preserve_order"] }
serde = "1.0.110"
glob = "0.3.0"
@@ -44,7 +44,7 @@ cargo_metadata = "0.10.0"
[features]
default = [ "postgres", "sqlite", "mysql" ]
# database
# databases
mysql = [ "sqlx/mysql" ]
postgres = [ "sqlx/postgres" ]
sqlite = [ "sqlx/sqlite" ]

View File

@@ -5,6 +5,8 @@ mode with `sqlx::query!()` and friends.
### Install
#### With Rust toolchain
```bash
# supports all databases supported by SQLx
$ cargo install sqlx-cli
@@ -13,7 +15,7 @@ $ cargo install sqlx-cli
$ cargo install sqlx-cli --no-default-features --features postgres
```
### Commands
### Usage
All commands require `DATABASE_URL` to be set, either in the environment or in a `.env` file
in the current working directory.
@@ -49,10 +51,6 @@ $ sqlx migration run
Compares the migration history of the running database against the `migrations/` folder and runs
any scripts that are still pending.
##### Note: Down-Migrations
Down-migrations are currently a non-planned feature as their utility seems dubious but we welcome
any contributions (discussions/code) regarding this matter.
#### Enable building in "offline" mode with `query!()`
Note: must be run as `cargo sqlx`.

View File

@@ -1,18 +1,21 @@
use sqlx_cli::Command;
use structopt::{clap, StructOpt};
use clap::{crate_version, AppSettings, FromArgMatches, IntoApp};
use console::style;
use sqlx_cli::Opt;
use std::env;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
async fn main() {
// when invoked as `cargo sqlx [...]` the args we see are `[...]/sqlx-cli sqlx prepare`
// so we want to notch out that superfluous "sqlx"
let args = env::args_os().skip(2);
let matches = Command::clap()
let matches = Opt::into_app()
.version(crate_version!())
.bin_name("cargo sqlx")
.setting(clap::AppSettings::NoBinaryName)
.setting(AppSettings::NoBinaryName)
.get_matches_from(args);
sqlx_cli::run(Command::from_clap(&matches)).await
if let Err(error) = sqlx_cli::run(Opt::from_arg_matches(&matches)).await {
println!("{} {}", style("error:").bold().red(), error);
}
}

View File

@@ -1,8 +1,13 @@
use sqlx_cli::Command;
use structopt::StructOpt;
use clap::{crate_version, FromArgMatches, IntoApp};
use console::style;
use sqlx_cli::Opt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
async fn main() {
let matches = Opt::into_app().version(crate_version!()).get_matches();
// no special handling here
sqlx_cli::run(Command::from_args()).await
if let Err(error) = sqlx_cli::run(Opt::from_arg_matches(&matches)).await {
println!("{} {}", style("error:").bold().red(), error);
}
}

32
sqlx-cli/src/database.rs Normal file
View File

@@ -0,0 +1,32 @@
use console::style;
use dialoguer::Confirm;
use sqlx::any::Any;
use sqlx::migrate::MigrateDatabase;
pub async fn create(uri: &str) -> anyhow::Result<()> {
if !Any::database_exists(uri).await? {
Any::create_database(uri).await?;
}
Ok(())
}
pub async fn drop(uri: &str, confirm: bool) -> anyhow::Result<()> {
if confirm
&& !Confirm::new()
.with_prompt(format!(
"\nAre you sure you want to drop the database at {}?",
style(uri).cyan()
))
.default(false)
.interact()?
{
return Ok(());
}
if Any::database_exists(uri).await? {
Any::drop_database(uri).await?;
}
Ok(())
}

View File

@@ -1,59 +0,0 @@
use dialoguer::Confirmation;
use anyhow::bail;
pub async fn run_create() -> anyhow::Result<()> {
let migrator = crate::migrator::get()?;
if !migrator.can_create_database() {
bail!(
"Database creation is not implemented for {}",
migrator.database_type()
);
}
let db_name = migrator.get_database_name()?;
let db_exists = migrator.check_if_database_exists(&db_name).await?;
if !db_exists {
println!("Creating database: {}", db_name);
Ok(migrator.create_database(&db_name).await?)
} else {
println!("Database already exists, aborting");
Ok(())
}
}
pub async fn run_drop() -> anyhow::Result<()> {
let migrator = crate::migrator::get()?;
if !migrator.can_drop_database() {
bail!(
"Database drop is not implemented for {}",
migrator.database_type()
);
}
let db_name = migrator.get_database_name()?;
let db_exists = migrator.check_if_database_exists(&db_name).await?;
if db_exists {
if !Confirmation::new()
.with_text(&format!(
"\nAre you sure you want to drop the database: {}?",
db_name
))
.default(false)
.interact()?
{
println!("Aborting");
return Ok(());
}
println!("Dropping database: {}", db_name);
Ok(migrator.drop_database(&db_name).await?)
} else {
println!("Database does not exists, aborting");
Ok(())
}
}

View File

@@ -1,95 +1,38 @@
use crate::opt::{Command, DatabaseCommand, MigrateCommand};
use anyhow::anyhow;
use dotenv::dotenv;
use std::env;
use structopt::StructOpt;
mod migrator;
mod db;
mod migration;
mod database;
// mod migration;
// mod migrator;
mod migrate;
mod opt;
mod prepare;
#[derive(StructOpt, Debug)]
pub enum Command {
#[structopt(alias = "mig")]
Migrate(MigrationCommand),
pub use crate::opt::Opt;
#[structopt(alias = "db")]
Database(DatabaseCommand),
/// Enables offline mode for a project utilizing `query!()` and related macros.
/// May only be run as `cargo sqlx prepare`.
///
/// Saves data for all invocations of `query!()` and friends in the project so that it may be
/// built in offline mode, i.e. so compilation does not require connecting to a running database.
/// Outputs to `sqlx-data.json` in the current directory, overwriting it if it already exists.
///
/// Offline mode can be activated simply by removing `DATABASE_URL` from the environment or
/// building without a `.env` file.
#[structopt(alias = "prep")]
Prepare {
/// If this flag is passed, instead of overwriting `sqlx-data.json` in the current directory,
/// that file is loaded and compared against the current output of the prepare step; if
/// there is a mismatch, an error is reported and the process exits with a nonzero exit code.
///
/// Intended for use in CI.
#[structopt(long)]
check: bool,
/// Any arguments to pass to `cargo rustc`;
/// Cargo args (preceding `--` in `cargo rustc ... -- ...`) only.
#[structopt(name = "Cargo args", last = true)]
cargo_args: Vec<String>,
},
}
/// Generate and run migrations
#[derive(StructOpt, Debug)]
pub enum MigrationCommand {
/// Create a new migration with the given name,
/// using the current time as the version
Add { name: String },
/// Run all pending migrations
Run,
/// List all migrations
List,
}
/// Create or drops database depending on your connection string
#[derive(StructOpt, Debug)]
pub enum DatabaseCommand {
/// Create database in url
Create,
/// Drop database in url
Drop,
}
pub async fn run(cmd: Command) -> anyhow::Result<()> {
pub async fn run(opt: Opt) -> anyhow::Result<()> {
dotenv().ok();
match cmd {
Command::Migrate(migrate) => match migrate {
MigrationCommand::Add { name } => migration::add_file(&name)?,
MigrationCommand::Run => migration::run().await?,
MigrationCommand::List => migration::list().await?,
let database_url = env::var("DATABASE_URL")
.map_err(|_| anyhow!("The DATABASE_URL environment variable must be set"))?;
match opt.command {
Command::Migrate(migrate) => match migrate.command {
MigrateCommand::Add { description } => migrate::add(&description)?,
MigrateCommand::Run => migrate::run(&database_url).await?,
MigrateCommand::Info => migrate::info(&database_url).await?,
},
Command::Database(database) => match database {
DatabaseCommand::Create => db::run_create().await?,
DatabaseCommand::Drop => db::run_drop().await?,
Command::Database(database) => match database.command {
DatabaseCommand::Create => database::create(&database_url).await?,
DatabaseCommand::Drop { yes } => database::drop(&database_url, !yes).await?,
},
Command::Prepare {
check: false,
cargo_args,
} => prepare::run(cargo_args)?,
Command::Prepare { check: false, args } => prepare::run(&database_url, args)?,
Command::Prepare {
check: true,
cargo_args,
} => prepare::check(cargo_args)?,
Command::Prepare { check: true, args } => prepare::check(&database_url, args)?,
};
Ok(())

89
sqlx-cli/src/migrate.rs Normal file
View File

@@ -0,0 +1,89 @@
use anyhow::{bail, Context};
use console::style;
use sqlx::migrate::{Migrate, MigrateError, Migrator};
use sqlx::{AnyConnection, Connection};
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
const MIGRATION_FOLDER: &'static str = "migrations";
pub fn add(description: &str) -> anyhow::Result<()> {
use chrono::prelude::*;
use std::path::PathBuf;
fs::create_dir_all(MIGRATION_FOLDER).context("Unable to create migrations directory")?;
let dt = Utc::now();
let mut file_name = dt.format("%Y%m%d%H%M%S").to_string();
file_name.push_str("_");
file_name.push_str(&description.replace(' ', "_"));
file_name.push_str(".sql");
let mut path = PathBuf::new();
path.push(MIGRATION_FOLDER);
path.push(&file_name);
println!("Creating {}", style(path.display()).cyan());
let mut file = File::create(&path).context("Failed to create migration file")?;
file.write_all(b"-- Add migration script here\n")?;
Ok(())
}
pub async fn info(uri: &str) -> anyhow::Result<()> {
let migrator = Migrator::new(Path::new(MIGRATION_FOLDER)).await?;
let mut conn = AnyConnection::connect(uri).await?;
conn.ensure_migrations_table().await?;
let (version, _) = conn.version().await?.unwrap_or((0, false));
for migration in migrator.iter() {
println!(
"{}/{} {}",
style(migration.version()).cyan(),
if version >= migration.version() {
style("installed").green()
} else {
style("pending").yellow()
},
migration.description(),
);
}
Ok(())
}
pub async fn run(uri: &str) -> anyhow::Result<()> {
let migrator = Migrator::new(Path::new(MIGRATION_FOLDER)).await?;
let mut conn = AnyConnection::connect(uri).await?;
conn.ensure_migrations_table().await?;
let (version, dirty) = conn.version().await?.unwrap_or((0, false));
if dirty {
bail!(MigrateError::Dirty(version));
}
for migration in migrator.iter() {
if migration.version() > version {
let elapsed = conn.apply(migration).await?;
println!(
"{}/{} {} {}",
style(migration.version()).cyan(),
style("migrate").green(),
migration.description(),
style(format!("({:?})", elapsed)).dim()
);
} else {
conn.validate(migration).await?;
}
}
Ok(())
}

View File

@@ -1,75 +0,0 @@
use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use std::env;
use url::Url;
#[cfg(feature = "mysql")]
mod mysql;
#[cfg(feature = "postgres")]
mod postgres;
#[cfg(feature = "sqlite")]
mod sqlite;
#[async_trait]
pub trait MigrationTransaction {
async fn commit(self: Box<Self>) -> Result<()>;
async fn rollback(self: Box<Self>) -> Result<()>;
async fn check_if_applied(&mut self, migration: &str) -> Result<bool>;
async fn execute_migration(&mut self, migration_sql: &str) -> Result<()>;
async fn save_applied_migration(&mut self, migration_name: &str) -> Result<()>;
}
#[async_trait]
pub trait DatabaseMigrator {
// Misc info
fn database_type(&self) -> String;
fn get_database_name(&self) -> Result<String>;
// Features
fn can_migrate_database(&self) -> bool;
fn can_create_database(&self) -> bool;
fn can_drop_database(&self) -> bool;
// Database creation
async fn check_if_database_exists(&self, db_name: &str) -> Result<bool>;
async fn create_database(&self, db_name: &str) -> Result<()>;
async fn drop_database(&self, db_name: &str) -> Result<()>;
// Migration
async fn create_migration_table(&self) -> Result<()>;
async fn get_migrations(&self) -> Result<Vec<String>>;
async fn begin_migration(&self) -> Result<Box<dyn MigrationTransaction>>;
}
pub fn get() -> Result<Box<dyn DatabaseMigrator>> {
let db_url_raw = env::var("DATABASE_URL").context("Failed to find 'DATABASE_URL'")?;
let db_url = Url::parse(&db_url_raw)?;
// This code is taken from: https://github.com/launchbadge/sqlx/blob/master/sqlx-macros/src/lib.rs#L63
match db_url.scheme() {
#[cfg(feature = "sqlite")]
"sqlite" => Ok(Box::new(self::sqlite::Sqlite::new(db_url_raw ))),
#[cfg(not(feature = "sqlite"))]
"sqlite" => bail!("Not implemented. DATABASE_URL {} has the scheme of a SQLite database but the `sqlite` feature of sqlx was not enabled",
db_url),
#[cfg(feature = "postgres")]
"postgresql" | "postgres" => Ok(Box::new(self::postgres::Postgres::new(db_url_raw))),
#[cfg(not(feature = "postgres"))]
"postgresql" | "postgres" => bail!("DATABASE_URL {} has the scheme of a Postgres database but the `postgres` feature of sqlx was not enabled",
db_url),
#[cfg(feature = "mysql")]
"mysql" | "mariadb" => Ok(Box::new(self::mysql::MySql::new(db_url_raw))),
#[cfg(not(feature = "mysql"))]
"mysql" | "mariadb" => bail!(
"DATABASE_URL {} has the scheme of a MySQL/MariaDB database but the `mysql` feature of sqlx was not enabled",
db_url
),
scheme => bail!("unexpected scheme {:?} in DATABASE_URL {}", scheme, db_url),
}
}

View File

@@ -1,203 +0,0 @@
use sqlx::mysql::MySqlRow;
use sqlx::pool::PoolConnection;
use sqlx::Connect;
use sqlx::Executor;
use sqlx::MySqlConnection;
use sqlx::MySqlPool;
use sqlx::Row;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use super::{DatabaseMigrator, MigrationTransaction};
pub struct MySql {
pub db_url: String,
}
impl MySql {
pub fn new(db_url: String) -> Self {
MySql {
db_url: db_url.clone(),
}
}
}
struct DbUrl<'a> {
base_url: &'a str,
db_name: &'a str,
}
fn get_base_url<'a>(db_url: &'a str) -> Result<DbUrl> {
let split: Vec<&str> = db_url.rsplitn(2, '/').collect();
if split.len() != 2 {
return Err(anyhow!("Failed to find database name in connection string"));
}
let db_name = split[0];
let base_url = split[1];
Ok(DbUrl { base_url, db_name })
}
#[async_trait]
impl DatabaseMigrator for MySql {
fn database_type(&self) -> String {
"MySql".to_string()
}
fn can_migrate_database(&self) -> bool {
true
}
fn can_create_database(&self) -> bool {
true
}
fn can_drop_database(&self) -> bool {
true
}
fn get_database_name(&self) -> Result<String> {
let db_url = get_base_url(&self.db_url)?;
Ok(db_url.db_name.to_string())
}
async fn check_if_database_exists(&self, db_name: &str) -> Result<bool> {
let db_url = get_base_url(&self.db_url)?;
let base_url = db_url.base_url;
let mut conn = MySqlConnection::connect(base_url).await?;
let result: bool = sqlx::query(
"select exists(SELECT 1 from INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?)",
)
.bind(db_name)
.try_map(|row: MySqlRow| row.try_get(0))
.fetch_one(&mut conn)
.await
.context("Failed to check if database exists")?;
Ok(result)
}
async fn create_database(&self, db_name: &str) -> Result<()> {
let db_url = get_base_url(&self.db_url)?;
let base_url = db_url.base_url;
let mut conn = MySqlConnection::connect(base_url).await?;
sqlx::query(&format!("CREATE DATABASE `{}`", db_name))
.execute(&mut conn)
.await
.with_context(|| format!("Failed to create database: {}", db_name))?;
Ok(())
}
async fn drop_database(&self, db_name: &str) -> Result<()> {
let db_url = get_base_url(&self.db_url)?;
let base_url = db_url.base_url;
let mut conn = MySqlConnection::connect(base_url).await?;
sqlx::query(&format!("DROP DATABASE `{}`", db_name))
.execute(&mut conn)
.await
.with_context(|| format!("Failed to drop database: {}", db_name))?;
Ok(())
}
async fn create_migration_table(&self) -> Result<()> {
let mut conn = MySqlConnection::connect(&self.db_url).await?;
println!("Create migration table");
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS __migrations (
migration VARCHAR (255) PRIMARY KEY,
created TIMESTAMP NOT NULL DEFAULT current_timestamp
);
"#,
)
.execute(&mut conn)
.await
.context("Failed to create migration table")?;
Ok(())
}
async fn get_migrations(&self) -> Result<Vec<String>> {
let mut conn = MySqlConnection::connect(&self.db_url).await?;
let result = sqlx::query(
r#"
select migration from __migrations order by created
"#,
)
.try_map(|row: MySqlRow| row.try_get(0))
.fetch_all(&mut conn)
.await
.context("Failed to create migration table")?;
Ok(result)
}
async fn begin_migration(&self) -> Result<Box<dyn MigrationTransaction>> {
let pool = MySqlPool::new(&self.db_url)
.await
.context("Failed to connect to pool")?;
let tx = pool.begin().await?;
Ok(Box::new(MySqlMigration { transaction: tx }))
}
}
pub struct MySqlMigration {
transaction: sqlx::Transaction<'static, sqlx::MySql, PoolConnection<sqlx::MySql>>,
}
#[async_trait]
impl MigrationTransaction for MySqlMigration {
async fn commit(self: Box<Self>) -> Result<()> {
self.transaction.commit().await?;
Ok(())
}
async fn rollback(self: Box<Self>) -> Result<()> {
self.transaction.rollback().await?;
Ok(())
}
async fn check_if_applied(&mut self, migration_name: &str) -> Result<bool> {
let result =
sqlx::query("select exists(select migration from __migrations where migration = ?)")
.bind(migration_name.to_string())
.try_map(|row: MySqlRow| row.try_get(0))
.fetch_one(&mut self.transaction)
.await
.context("Failed to check migration table")?;
Ok(result)
}
async fn execute_migration(&mut self, migration_sql: &str) -> Result<()> {
self.transaction.execute(migration_sql).await?;
Ok(())
}
async fn save_applied_migration(&mut self, migration_name: &str) -> Result<()> {
sqlx::query("insert into __migrations (migration) values (?)")
.bind(migration_name.to_string())
.execute(&mut self.transaction)
.await
.context("Failed to insert migration")?;
Ok(())
}
}

View File

@@ -1,209 +0,0 @@
use sqlx::pool::PoolConnection;
use sqlx::postgres::PgRow;
use sqlx::Connect;
use sqlx::Executor;
use sqlx::PgConnection;
use sqlx::PgPool;
use sqlx::Row;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use crate::migrator::{DatabaseMigrator, MigrationTransaction};
pub struct Postgres {
pub db_url: String,
}
impl Postgres {
pub fn new(db_url: String) -> Self {
Postgres {
db_url: db_url.clone(),
}
}
}
struct DbUrl<'a> {
base_url: &'a str,
db_name: &'a str,
}
fn get_base_url<'a>(db_url: &'a str) -> Result<DbUrl> {
let split: Vec<&str> = db_url.rsplitn(2, '/').collect();
if split.len() != 2 {
return Err(anyhow!("Failed to find database name in connection string"));
}
let db_name = split[0];
let base_url = split[1];
Ok(DbUrl { base_url, db_name })
}
#[async_trait]
impl DatabaseMigrator for Postgres {
fn database_type(&self) -> String {
"Postgres".to_string()
}
fn can_migrate_database(&self) -> bool {
true
}
fn can_create_database(&self) -> bool {
true
}
fn can_drop_database(&self) -> bool {
true
}
fn get_database_name(&self) -> Result<String> {
let db_url = get_base_url(&self.db_url)?;
Ok(db_url.db_name.to_string())
}
async fn check_if_database_exists(&self, db_name: &str) -> Result<bool> {
let db_url = get_base_url(&self.db_url)?;
let base_url = db_url.base_url;
let mut conn = PgConnection::connect(base_url).await?;
let result: bool =
sqlx::query("select exists(SELECT 1 from pg_database WHERE datname = $1) as exists")
.bind(db_name)
.try_map(|row: PgRow| row.try_get("exists"))
.fetch_one(&mut conn)
.await
.context("Failed to check if database exists")?;
Ok(result)
}
async fn create_database(&self, db_name: &str) -> Result<()> {
let db_url = get_base_url(&self.db_url)?;
let base_url = db_url.base_url;
let mut conn = PgConnection::connect(base_url).await?;
// quote database name (quotes in the name are escaped with additional quotes)
sqlx::query(&format!(
"CREATE DATABASE \"{}\"",
db_name.replace('"', "\"\"")
))
.execute(&mut conn)
.await
.with_context(|| format!("Failed to create database: {}", db_name))?;
Ok(())
}
async fn drop_database(&self, db_name: &str) -> Result<()> {
let db_url = get_base_url(&self.db_url)?;
let base_url = db_url.base_url;
let mut conn = PgConnection::connect(base_url).await?;
sqlx::query(&format!(
"DROP DATABASE \"{}\"",
db_name.replace('"', "\"\"")
))
.execute(&mut conn)
.await
.with_context(|| format!("Failed to drop database: {}", db_name))?;
Ok(())
}
async fn create_migration_table(&self) -> Result<()> {
let mut conn = PgConnection::connect(&self.db_url).await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS __migrations (
migration VARCHAR (255) PRIMARY KEY,
created TIMESTAMP NOT NULL DEFAULT current_timestamp
);
"#,
)
.execute(&mut conn)
.await
.context("Failed to create migration table")?;
Ok(())
}
async fn get_migrations(&self) -> Result<Vec<String>> {
let mut conn = PgConnection::connect(&self.db_url).await?;
let result = sqlx::query(
r#"
select migration from __migrations order by created
"#,
)
.try_map(|row: PgRow| row.try_get(0))
.fetch_all(&mut conn)
.await
.context("Failed to create migration table")?;
Ok(result)
}
async fn begin_migration(&self) -> Result<Box<dyn MigrationTransaction>> {
let pool = PgPool::new(&self.db_url)
.await
.context("Failed to connect to pool")?;
let tx = pool.begin().await?;
Ok(Box::new(PostgresMigration { transaction: tx }))
}
}
pub struct PostgresMigration {
transaction: sqlx::Transaction<'static, sqlx::Postgres, PoolConnection<sqlx::Postgres>>,
}
#[async_trait]
impl MigrationTransaction for PostgresMigration {
async fn commit(self: Box<Self>) -> Result<()> {
self.transaction.commit().await?;
Ok(())
}
async fn rollback(self: Box<Self>) -> Result<()> {
self.transaction.rollback().await?;
Ok(())
}
async fn check_if_applied(&mut self, migration_name: &str) -> Result<bool> {
let result = sqlx::query(
"select exists(select migration from __migrations where migration = $1) as exists",
)
.bind(migration_name.to_string())
.try_map(|row: PgRow| row.try_get("exists"))
.fetch_one(&mut self.transaction)
.await
.context("Failed to check migration table")?;
Ok(result)
}
async fn execute_migration(&mut self, migration_sql: &str) -> Result<()> {
self.transaction.execute(migration_sql).await?;
Ok(())
}
async fn save_applied_migration(&mut self, migration_name: &str) -> Result<()> {
sqlx::query("insert into __migrations (migration) values ($1)")
.bind(migration_name.to_string())
.execute(&mut self.transaction)
.await
.context("Failed to insert migration")?;
Ok(())
}
}

View File

@@ -1,178 +0,0 @@
// use sqlx::pool::PoolConnection;
use sqlx::sqlite::SqliteRow;
use sqlx::Connect;
use sqlx::Executor;
use sqlx::Row;
use sqlx::SqliteConnection;
// use sqlx::SqlitePool;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use crate::migrator::{DatabaseMigrator, MigrationTransaction};
pub struct Sqlite {
db_url: String,
path: String,
}
impl Sqlite {
pub fn new(db_url: String) -> Self {
let path = crop_letters(&db_url, "sqlite://".len());
Sqlite {
db_url: db_url.clone(),
path: path.to_string(),
}
}
}
fn crop_letters(s: &str, pos: usize) -> &str {
match s.char_indices().skip(pos).next() {
Some((pos, _)) => &s[pos..],
None => "",
}
}
#[async_trait]
impl DatabaseMigrator for Sqlite {
fn database_type(&self) -> String {
"Sqlite".to_string()
}
fn can_migrate_database(&self) -> bool {
true
}
fn can_create_database(&self) -> bool {
true
}
fn can_drop_database(&self) -> bool {
true
}
fn get_database_name(&self) -> Result<String> {
let split: Vec<&str> = self.db_url.rsplitn(2, '/').collect();
if split.len() != 2 {
return Err(anyhow!("Failed to find database name in connection string"));
}
let db_name = split[0];
Ok(db_name.to_string())
}
async fn check_if_database_exists(&self, _db_name: &str) -> Result<bool> {
use std::path::Path;
Ok(Path::new(&self.path).exists())
}
async fn create_database(&self, _db_name: &str) -> Result<()> {
println!("DB {}", self.path);
// Opening a connection to sqlite creates the database.
let _ = SqliteConnection::connect(&self.db_url).await?;
Ok(())
}
async fn drop_database(&self, _db_name: &str) -> Result<()> {
std::fs::remove_file(&self.path)?;
Ok(())
}
async fn create_migration_table(&self) -> Result<()> {
let mut conn = SqliteConnection::connect(&self.db_url).await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS __migrations (
migration TEXT PRIMARY KEY,
created TIMESTAMP NOT NULL DEFAULT current_timestamp
);
"#,
)
.execute(&mut conn)
.await
.context("Failed to create migration table")?;
Ok(())
}
async fn get_migrations(&self) -> Result<Vec<String>> {
let mut conn = SqliteConnection::connect(&self.db_url).await?;
let result = sqlx::query(
r#"
select migration from __migrations order by created
"#,
)
.try_map(|row: SqliteRow| row.try_get(0))
.fetch_all(&mut conn)
.await
.context("Failed to create migration table")?;
Ok(result)
}
async fn begin_migration(&self) -> Result<Box<dyn MigrationTransaction>> {
// let pool = SqlitePool::new(&self.db_url)
// .await
// .context("Failed to connect to pool")?;
// let tx = pool.begin().await?;
// Ok(Box::new(MigrationTransaction { transaction: tx }))
Ok(Box::new(SqliteMigration {
db_url: self.db_url.clone(),
}))
}
}
pub struct SqliteMigration {
db_url: String,
// pub transaction: sqlx::Transaction<'static, sqlx::Sqlite, PoolConnection<sqlx::Sqlite>>,
}
#[async_trait]
impl MigrationTransaction for SqliteMigration {
async fn commit(self: Box<Self>) -> Result<()> {
// self.transaction.commit().await?;
Ok(())
}
async fn rollback(self: Box<Self>) -> Result<()> {
// self.transaction.rollback().await?;
Ok(())
}
async fn check_if_applied(&mut self, migration_name: &str) -> Result<bool> {
let mut conn = SqliteConnection::connect(&self.db_url).await?;
let result =
sqlx::query("select exists(select migration from __migrations where migration = $1)")
.bind(migration_name.to_string())
.try_map(|row: SqliteRow| row.try_get(0))
.fetch_one(&mut conn)
.await?;
Ok(result)
}
async fn execute_migration(&mut self, migration_sql: &str) -> Result<()> {
let mut conn = SqliteConnection::connect(&self.db_url).await?;
conn.execute(migration_sql).await?;
// self.transaction.execute(migration_sql).await?;
Ok(())
}
async fn save_applied_migration(&mut self, migration_name: &str) -> Result<()> {
let mut conn = SqliteConnection::connect(&self.db_url).await?;
sqlx::query("insert into __migrations (migration) values ($1)")
.bind(migration_name.to_string())
.execute(&mut conn)
.await?;
Ok(())
}
}

77
sqlx-cli/src/opt.rs Normal file
View File

@@ -0,0 +1,77 @@
use clap::Clap;
#[derive(Clap, Debug)]
pub struct Opt {
#[clap(subcommand)]
pub command: Command,
}
#[derive(Clap, Debug)]
pub enum Command {
#[clap(alias = "db")]
Database(DatabaseOpt),
/// Generate query metadata to support offline compile-time verification.
///
/// Saves metadata for all invocations of `query!` and related macros to `sqlx-data.json`
/// in the current directory, overwriting if needed.
///
/// During project compilation, the absence of the `DATABASE_URL` environment variable or
/// the presence of `SQLX_OFFLINE` will constrain the compile-time verification to only
/// read from the cached query metadata.
#[clap(alias = "prep")]
Prepare {
/// Run in 'check' mode. Exits with 0 if the query metadata is up-to-date. Exits with
/// 1 if the query metadata needs updating.
#[clap(long)]
check: bool,
/// Arguments to be passed to `cargo rustc ...`.
#[clap(last = true)]
args: Vec<String>,
},
#[clap(alias = "mig")]
Migrate(MigrateOpt),
}
/// Group of commands for creating and dropping your database.
#[derive(Clap, Debug)]
pub struct DatabaseOpt {
#[clap(subcommand)]
pub command: DatabaseCommand,
}
#[derive(Clap, Debug)]
pub enum DatabaseCommand {
/// Creates the database specified in your DATABASE_URL.
Create,
/// Drops the database specified in your DATABASE_URL.
Drop {
/// Automatic confirmation. Without this option, you will be prompted before dropping
/// your database.
#[clap(short)]
yes: bool,
},
}
/// Group of commands for creating and running migrations.
#[derive(Clap, Debug)]
pub struct MigrateOpt {
#[clap(subcommand)]
pub command: MigrateCommand,
}
#[derive(Clap, Debug)]
pub enum MigrateCommand {
/// Create a new migration with the given description,
/// and the current time as the version.
Add { description: String },
/// Run all pending migrations.
Run,
/// List all available migrations.
Info,
}

View File

@@ -1,18 +1,17 @@
use anyhow::{anyhow, bail, Context};
use std::process::Command;
use std::{env, fs};
use anyhow::{bail, Context};
use cargo_metadata::MetadataCommand;
use sqlx::any::{AnyConnectOptions, AnyKind};
use std::collections::BTreeMap;
use std::fs::File;
use std::process::Command;
use std::str::FromStr;
use std::time::SystemTime;
use url::Url;
use std::{env, fs};
type QueryData = BTreeMap<String, serde_json::Value>;
type JsonObject = serde_json::Map<String, serde_json::Value>;
pub fn run(cargo_args: Vec<String>) -> anyhow::Result<()> {
pub fn run(url: &str, cargo_args: Vec<String>) -> anyhow::Result<()> {
#[derive(serde::Serialize)]
struct DataFile {
db: &'static str,
@@ -20,7 +19,7 @@ pub fn run(cargo_args: Vec<String>) -> anyhow::Result<()> {
data: QueryData,
}
let db_kind = get_db_kind()?;
let db_kind = get_db_kind(url)?;
let data = run_prepare_step(cargo_args)?;
serde_json::to_writer_pretty(
@@ -37,8 +36,8 @@ pub fn run(cargo_args: Vec<String>) -> anyhow::Result<()> {
Ok(())
}
pub fn check(cargo_args: Vec<String>) -> anyhow::Result<()> {
let db_kind = get_db_kind()?;
pub fn check(url: &str, cargo_args: Vec<String>) -> anyhow::Result<()> {
let db_kind = get_db_kind(url)?;
let data = run_prepare_step(cargo_args)?;
let data_file = fs::read("sqlx-data.json").context(
@@ -133,17 +132,21 @@ fn run_prepare_step(cargo_args: Vec<String>) -> anyhow::Result<QueryData> {
Ok(data)
}
fn get_db_kind() -> anyhow::Result<&'static str> {
let db_url = dotenv::var("DATABASE_URL")
.map_err(|_| anyhow!("`DATABASE_URL` must be set to use the `prepare` subcommand"))?;
let db_url = Url::parse(&db_url)?;
fn get_db_kind(url: &str) -> anyhow::Result<&'static str> {
let options = AnyConnectOptions::from_str(&url)?;
// these should match the values of `DatabaseExt::NAME` in `sqlx-macros`
match db_url.scheme() {
"postgres" | "postgresql" => Ok("PostgreSQL"),
"mysql" | "mariadb" => Ok("MySQL/MariaDB"),
"sqlite" => Ok("SQLite"),
_ => bail!("unexpected scheme in database URL: {}", db_url.scheme()),
match options.kind() {
#[cfg(feature = "postgres")]
AnyKind::Postgres => Ok("PostgreSQL"),
#[cfg(feature = "mysql")]
AnyKind::MySql => Ok("MySQL"),
#[cfg(feature = "sqlite")]
AnyKind::Sqlite => Ok("SQLite"),
#[cfg(feature = "mssql")]
AnyKind::Mssql => Ok("MSSQL"),
}
}