Skip to main content

waypoint_core/commands/
diff.rs

1//! Compare live database schema against a target and generate migration SQL.
2
3use serde::Serialize;
4
5#[cfg(feature = "postgres")]
6use tokio_postgres::Client;
7
8use crate::config::WaypointConfig;
9use crate::db::DbClient;
10use crate::dialect::DialectKind;
11use crate::error::{Result, WaypointError};
12use crate::schema::{self, SchemaDiff};
13
14/// Target to compare the current schema against.
15pub enum DiffTarget {
16    /// Compare against another database identified by its connection URL.
17    Database(String),
18}
19
20/// Report produced by the diff command.
21#[derive(Debug, Serialize)]
22pub struct DiffReport {
23    /// List of individual schema differences found.
24    pub diffs: Vec<SchemaDiff>,
25    /// DDL SQL statements generated to reconcile the differences.
26    pub generated_sql: String,
27    /// Whether any differences were detected.
28    pub has_changes: bool,
29}
30
31/// Execute the diff command (PostgreSQL legacy entry).
32#[cfg(feature = "postgres")]
33pub async fn execute(
34    client: &Client,
35    config: &WaypointConfig,
36    target: DiffTarget,
37) -> Result<DiffReport> {
38    let schema_name = &config.migrations.schema;
39
40    let current = schema::introspect(client, schema_name).await?;
41
42    let target_snapshot = match target {
43        DiffTarget::Database(ref url) => {
44            let target_client = crate::db::connect(url).await?;
45            schema::introspect(&target_client, schema_name).await?
46        }
47    };
48
49    let diffs = schema::diff(&current, &target_snapshot);
50    let generated_sql = schema::generate_ddl(&diffs);
51    let has_changes = !diffs.is_empty();
52
53    Ok(DiffReport {
54        diffs,
55        generated_sql,
56        has_changes,
57    })
58}
59
60/// Execute the diff command (dialect-aware entry).
61///
62/// Generated SQL is PostgreSQL-flavored when comparing PG schemas. On MySQL
63/// the structural `diffs` list is populated correctly but `generated_sql` is
64/// best-effort PG-shaped — consume the structured diffs for MySQL until a
65/// MySQL DDL generator lands.
66pub async fn execute_db(
67    client: &DbClient,
68    config: &WaypointConfig,
69    target: DiffTarget,
70) -> Result<DiffReport> {
71    let schema_name = client.resolve_schema(&config.migrations.schema).await?;
72
73    let current = schema::introspect_db(client, &schema_name).await?;
74
75    let target_snapshot = match target {
76        DiffTarget::Database(ref url) => {
77            let target_client = connect_for_url(url).await?;
78            // Schema resolution for --target-url differs by engine:
79            //   PG: schemas are namespaces *within* a database, so the
80            //       configured `schema` (e.g. "public") applies to both sides.
81            //   MySQL: "schema" === "database", and the target URL specifies a
82            //       different database. We introspect whatever the target
83            //       connection actually points at, not the source's configured
84            //       db name.
85            let target_schema = match target_client.dialect_kind() {
86                DialectKind::Mysql => target_client.current_database().await?,
87                DialectKind::Postgres => {
88                    target_client
89                        .resolve_schema(&config.migrations.schema)
90                        .await?
91                }
92            };
93            schema::introspect_db(&target_client, &target_schema).await?
94        }
95    };
96
97    let diffs = schema::diff(&current, &target_snapshot);
98    let generated_sql = schema::generate_ddl(&diffs);
99    let has_changes = !diffs.is_empty();
100
101    Ok(DiffReport {
102        diffs,
103        generated_sql,
104        has_changes,
105    })
106}
107
108async fn connect_for_url(url: &str) -> Result<DbClient> {
109    let kind = DialectKind::from_url(url).unwrap_or(DialectKind::Postgres);
110    match kind {
111        #[cfg(feature = "postgres")]
112        DialectKind::Postgres => {
113            let c = crate::db::connect(url).await?;
114            Ok(DbClient::with_postgres(c))
115        }
116        #[cfg(not(feature = "postgres"))]
117        DialectKind::Postgres => Err(WaypointError::ConfigError(
118            "PostgreSQL support is not compiled in".into(),
119        )),
120        #[cfg(feature = "mysql")]
121        DialectKind::Mysql => {
122            let pool = mysql_async::Pool::from_url(url)
123                .map_err(|e| WaypointError::ConfigError(format!("Invalid MySQL URL: {}", e)))?;
124            Ok(DbClient::with_mysql(pool))
125        }
126        #[cfg(not(feature = "mysql"))]
127        DialectKind::Mysql => Err(WaypointError::ConfigError(
128            "MySQL support is not compiled in".into(),
129        )),
130    }
131}