Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ time = ["sqlx-core/time", "sqlx-macros?/time", "sqlx-mysql?/time", "sqlx-postgre
uuid = ["sqlx-core/uuid", "sqlx-macros?/uuid", "sqlx-mysql?/uuid", "sqlx-postgres?/uuid", "sqlx-sqlite?/uuid"]
regexp = ["sqlx-sqlite?/regexp"]
bstr = ["sqlx-core/bstr"]
_offline = ["sqlx-core/offline", "sqlx-mysql?/offline", "sqlx-postgres?/offline", "sqlx-sqlite?/offline"]

[workspace.dependencies]
# Core Crates
Expand Down
2 changes: 2 additions & 0 deletions sqlx-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ anyhow = "1.0.52"
console = "0.15.0"
dialoguer = { version = "0.11", default-features = false }
serde_json = "1.0.73"
serde = {version = "1.0.228", features = ["derive"] }
glob = "0.3.0"
openssl = { version = "0.10.38", optional = true }
cargo_metadata = "0.18.1"
Expand All @@ -50,6 +51,7 @@ features = [
"runtime-tokio",
"migrate",
"any",
"_offline"
]

[features]
Expand Down
11 changes: 11 additions & 0 deletions sqlx-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub mod completions;
pub mod migrate;
pub mod opt;
pub mod prepare;
pub mod revalidate;

pub use crate::opt::Opt;

Expand Down Expand Up @@ -206,6 +207,16 @@ async fn do_run(opt: Opt) -> anyhow::Result<()> {

#[cfg(feature = "completions")]
Command::Completions { shell } => completions::run(shell),

Command::Revalidate {
mut connect_opts,
config,
database,
} => {
let config = config.load_config().await?;
connect_opts.populate_db_url(&config)?;
revalidate::run_revalidate(connect_opts, database.as_deref()).await?;
}
};

Ok(())
Expand Down
9 changes: 9 additions & 0 deletions sqlx-cli/src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ pub enum Command {
#[cfg(feature = "completions")]
/// Generate shell completions for the specified shell
Completions { shell: Shell },

/// Revalidate the cached files in `.sqlx/` against the database
Revalidate {
#[clap(flatten)]
connect_opts: ConnectOpts,
#[clap(flatten)]
config: ConfigOpt,
database: Option<String>,
},
}

/// Group of commands for creating and dropping your database.
Expand Down
2 changes: 1 addition & 1 deletion sqlx-cli/src/prepare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ fn minimal_project_recompile_action(metadata: &Metadata, all: bool) -> ProjectRe
}

/// Find all `query-*.json` files in a directory.
fn glob_query_files(path: impl AsRef<Path>) -> anyhow::Result<Vec<PathBuf>> {
pub(crate) fn glob_query_files(path: impl AsRef<Path>) -> anyhow::Result<Vec<PathBuf>> {
let path = path.as_ref();
let pattern = path.join("query-*.json");
glob::glob(
Expand Down
87 changes: 87 additions & 0 deletions sqlx-cli/src/revalidate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::str::FromStr;

use anyhow::bail;
use console::style;
use serde::Serialize;
use sqlx::any::AnyConnectOptions;
use sqlx::Connection;
use sqlx::{Database, Describe, Executor, MySql, Postgres, SqlStr, Sqlite};

use crate::opt::ConnectOpts;
use crate::prepare::glob_query_files;

/// Offline query data.
#[derive(Clone, serde::Deserialize)]
pub struct DynQueryData {
pub db_name: String,
pub query: String,
pub describe: serde_json::Value,
pub hash: String,
}

pub async fn run_revalidate(
connect_opts: ConnectOpts,
database: Option<&str>,
) -> anyhow::Result<()> {
let Some(database_url) = &connect_opts.database_url else {
bail!("DATABASE_URL must be set!");
};

let database = match database {
Some(database) => database.to_lowercase(),
None => {
let url = AnyConnectOptions::from_str(database_url)?;
url.database_url.scheme().to_lowercase()
}
};

match database.as_str() {
#[cfg(feature = "mysql")]
"mysql" => do_run::<MySql>(database_url).await,
#[cfg(feature = "postgres")]
"postgres" => do_run::<Postgres>(database_url).await,
#[cfg(feature = "sqlite")]
"sqlite" => do_run::<Sqlite>(database_url).await,
database => bail!("Unknown database: '{database}'"),
}
}

async fn do_run<DB: Database>(database_url: &str) -> anyhow::Result<()>
where
Describe<DB>: Serialize,
for<'ex> &'ex mut DB::Connection: Executor<'ex, Database = DB>,
{
let mut connection = DB::Connection::connect(database_url).await?;

let files = glob_query_files(".sqlx")?;
if files.is_empty() {
println!("{} no queries found", style("warning:").yellow());
return Ok(());
}

for file in files {
println!(
"{} re-validating query file {}",
style("info:").blue(),
file.display()
);
let expected_config = tokio::fs::read_to_string(&file).await?;
let config: DynQueryData = serde_json::from_str(&expected_config)?;

let sql_str = config.query;
let description: Describe<DB> =
Executor::describe(&mut connection, SqlStr::from_static(sql_str.leak()))
.await
.unwrap();
let description = serde_json::to_value(description)?;

if dbg!(description) != dbg!(config.describe) {
bail!(
"Query result for query {} is not up-to-date!",
file.display()
);
}
}

Ok(())
}
Loading