diff --git a/Cargo.lock b/Cargo.lock index 78e40f0c12..83b4c49a1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3269,18 +3269,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -3546,6 +3556,7 @@ dependencies = [ "futures-util", "glob", "openssl", + "serde", "serde_json", "sqlx", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 00d5d656c1..f4285b80fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/sqlx-cli/Cargo.toml b/sqlx-cli/Cargo.toml index d69048e698..bf01798ae0 100644 --- a/sqlx-cli/Cargo.toml +++ b/sqlx-cli/Cargo.toml @@ -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" @@ -50,6 +51,7 @@ features = [ "runtime-tokio", "migrate", "any", + "_offline" ] [features] diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index 7a2e41b16f..45d21420dc 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -38,6 +38,7 @@ pub mod completions; pub mod migrate; pub mod opt; pub mod prepare; +pub mod revalidate; pub use crate::opt::Opt; @@ -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(()) diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index cb09bc2ff5..8be892d4dd 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -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, + }, } /// Group of commands for creating and dropping your database. diff --git a/sqlx-cli/src/prepare.rs b/sqlx-cli/src/prepare.rs index 9f3fc67da4..3c1b113c5f 100644 --- a/sqlx-cli/src/prepare.rs +++ b/sqlx-cli/src/prepare.rs @@ -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) -> anyhow::Result> { +pub(crate) fn glob_query_files(path: impl AsRef) -> anyhow::Result> { let path = path.as_ref(); let pattern = path.join("query-*.json"); glob::glob( diff --git a/sqlx-cli/src/revalidate.rs b/sqlx-cli/src/revalidate.rs new file mode 100644 index 0000000000..af13964fe4 --- /dev/null +++ b/sqlx-cli/src/revalidate.rs @@ -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::(database_url).await, + #[cfg(feature = "postgres")] + "postgres" => do_run::(database_url).await, + #[cfg(feature = "sqlite")] + "sqlite" => do_run::(database_url).await, + database => bail!("Unknown database: '{database}'"), + } +} + +async fn do_run(database_url: &str) -> anyhow::Result<()> +where + Describe: 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 = + 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(()) +}