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