mirror of
https://github.com/launchbadge/sqlx.git
synced 2026-03-21 17:44:06 +00:00
feat: finish v1 of both cli and embedded migrations
This commit is contained in:
3
sqlx-cli/.gitignore
vendored
3
sqlx-cli/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
.env
|
||||
@@ -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" ]
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
32
sqlx-cli/src/database.rs
Normal 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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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
89
sqlx-cli/src/migrate.rs
Normal 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(())
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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
77
sqlx-cli/src/opt.rs
Normal 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,
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user