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;
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")]
33#[deprecated(
34    since = "0.6.0",
35    note = "Unused PostgreSQL-only entry point superseded by `execute_db`, which handles both engines. Will be removed in 1.0."
36)]
37pub async fn execute(
38    client: &Client,
39    config: &WaypointConfig,
40    target: DiffTarget,
41) -> Result<DiffReport> {
42    let schema_name = &config.migrations.schema;
43
44    let current = schema::introspect(client, schema_name).await?;
45
46    let target_snapshot = match target {
47        DiffTarget::Database(ref url) => {
48            let target_client =
49                crate::db::connect_with_transport(url, &crate::db::TransportConfig::default())
50                    .await?;
51            schema::introspect(&target_client, schema_name).await?
52        }
53    };
54
55    let diffs = schema::diff(&current, &target_snapshot);
56    let generated_sql = schema::generate_ddl(&diffs);
57    let has_changes = !diffs.is_empty();
58
59    Ok(DiffReport {
60        diffs,
61        generated_sql,
62        has_changes,
63    })
64}
65
66/// Execute the diff command (dialect-aware entry).
67///
68/// Generated SQL is PostgreSQL-flavored when comparing PG schemas. On MySQL
69/// the structural `diffs` list is populated correctly but `generated_sql` is
70/// best-effort PG-shaped — consume the structured diffs for MySQL until a
71/// MySQL DDL generator lands.
72pub async fn execute_db(
73    client: &DbClient,
74    config: &WaypointConfig,
75    target: DiffTarget,
76) -> Result<DiffReport> {
77    let schema_name = client.resolve_schema(&config.migrations.schema).await?;
78
79    let current = schema::introspect_db(client, &schema_name).await?;
80
81    let target_snapshot = match target {
82        DiffTarget::Database(ref url) => {
83            // The --target-url connection reuses the caller's [database]
84            // transport settings (SSL mode, timeouts, keepalive) rather than
85            // silently connecting with hardcoded defaults.
86            let target_client = crate::db::connect_for_url(url, config).await?;
87            // Schema resolution for --target-url differs by engine:
88            //   PG: schemas are namespaces *within* a database, so the
89            //       configured `schema` (e.g. "public") applies to both sides.
90            //   MySQL: "schema" === "database", and the target URL specifies a
91            //       different database. We introspect whatever the target
92            //       connection actually points at, not the source's configured
93            //       db name.
94            let target_schema = match target_client.dialect_kind() {
95                DialectKind::Mysql => target_client.current_database().await?,
96                DialectKind::Postgres => {
97                    target_client
98                        .resolve_schema(&config.migrations.schema)
99                        .await?
100                }
101            };
102            schema::introspect_db(&target_client, &target_schema).await?
103        }
104    };
105
106    let diffs = schema::diff(&current, &target_snapshot);
107    let generated_sql = schema::generate_ddl(&diffs);
108    let has_changes = !diffs.is_empty();
109
110    Ok(DiffReport {
111        diffs,
112        generated_sql,
113        has_changes,
114    })
115}