Skip to main content

waypoint_core/
lib.rs

1//! Lightweight, Flyway-compatible SQL migration library.
2//!
3//! Targets PostgreSQL (default `postgres` feature) and MySQL 8.0+ (opt-in
4//! `mysql` feature). Build with both features for mixed-engine multi-database
5//! configurations.
6//!
7//! # Quick Start
8//!
9//! ```rust,no_run
10//! use waypoint_core::config::WaypointConfig;
11//! use waypoint_core::Waypoint;
12//!
13//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
14//! let config = WaypointConfig::load(None, None)?;
15//! let wp = Waypoint::new(config).await?;
16//! let report = wp.migrate(None).await?;
17//! println!("Applied {} migrations", report.migrations_applied);
18//! # Ok(())
19//! # }
20//! ```
21//!
22//! # Architecture
23//!
24//! - [`config`] — Configuration loading (TOML, env vars, CLI overrides)
25//! - [`dialect`] — Engine-specific dialect (Postgres / MySQL) abstraction
26//! - [`migration`] — Migration file parsing and scanning
27//! - [`db`] — Database connections, TLS, advisory locks
28//! - [`history`] — Schema history table operations
29//! - [`commands`] — Individual command implementations
30//! - [`checksum`] — CRC32 checksums (Flyway-compatible)
31//! - [`placeholder`] — `${key}` placeholder replacement in SQL
32//! - [`hooks`] — SQL callback hooks (before/after migrate)
33//! - [`directive`] — `-- waypoint:*` comment directive parsing
34//! - [`guard`] — Guard expression parser and evaluator for pre/post conditions
35//! - [`sql_parser`] — Regex-based DDL extraction
36//! - [`safety`] — Migration safety analysis (lock levels, impact, verdicts)
37//! - [`schema`] — Schema introspection + diff
38//! - [`dependency`] — Migration dependency graph
39//! - [`preflight`] — Pre-migration health checks
40//! - [`multi`] — Multi-database orchestration
41//! - [`error`] — Error types
42
43pub mod advisor;
44pub mod checksum;
45pub mod commands;
46pub mod config;
47pub mod db;
48pub mod dependency;
49pub mod dialect;
50pub mod directive;
51pub mod engines;
52pub mod error;
53pub mod guard;
54pub mod history;
55pub mod hooks;
56pub mod migration;
57pub mod multi;
58pub mod placeholder;
59pub mod preflight;
60pub mod reversal;
61pub mod safety;
62pub mod schema;
63pub mod sql_parser;
64
65use std::path::PathBuf;
66
67use config::WaypointConfig;
68use db::DbClient;
69use error::Result;
70
71#[cfg(feature = "postgres")]
72use tokio_postgres::Client;
73
74pub use advisor::AdvisorReport;
75pub use commands::changelog::ChangelogReport;
76pub use commands::check_conflicts::ConflictReport;
77pub use commands::diff::DiffReport;
78pub use commands::drift::DriftReport;
79pub use commands::explain::ExplainReport;
80pub use commands::info::{MigrationInfo, MigrationState};
81pub use commands::lint::LintReport;
82pub use commands::migrate::MigrateReport;
83pub use commands::repair::RepairReport;
84pub use commands::safety::SafetyCommandReport;
85pub use commands::simulate::SimulationReport;
86pub use commands::snapshot::{RestoreReport, SnapshotReport};
87pub use commands::undo::{UndoReport, UndoTarget};
88pub use commands::validate::ValidateReport;
89pub use config::CliOverrides;
90pub use dialect::{DatabaseDialect, DialectKind};
91pub use multi::MultiWaypoint;
92pub use preflight::PreflightReport;
93pub use safety::SafetyReport;
94
95/// Main entry point for the Waypoint library.
96///
97/// Create a `Waypoint` instance with a config and use its methods to
98/// run migration commands programmatically.
99pub struct Waypoint {
100    pub config: WaypointConfig,
101    client: DbClient,
102}
103
104impl Waypoint {
105    /// Create a new Waypoint instance, connecting to the database.
106    ///
107    /// Engine is auto-detected from the configured connection URL scheme
108    /// (`postgres://` / `postgresql://` → PostgreSQL, `mysql://` → MySQL).
109    /// If `connect_retries` is configured, retries with exponential backoff.
110    pub async fn new(config: WaypointConfig) -> Result<Self> {
111        let conn_string = config.connection_string()?;
112        let client = connect_for_url(&conn_string, &config).await?;
113        Ok(Self { config, client })
114    }
115
116    /// Create a new Waypoint instance with an existing PostgreSQL client.
117    ///
118    /// Convenience constructor preserved for backwards compatibility. For new
119    /// code or for MySQL connections, use [`Self::with_db_client`].
120    #[cfg(feature = "postgres")]
121    pub fn with_client(config: WaypointConfig, client: Client) -> Self {
122        Self {
123            config,
124            client: DbClient::with_postgres(client),
125        }
126    }
127
128    /// Create a new Waypoint instance with an already-constructed [`DbClient`].
129    pub fn with_db_client(config: WaypointConfig, client: DbClient) -> Self {
130        Self { config, client }
131    }
132
133    /// Get a reference to the underlying database client.
134    pub fn client(&self) -> &DbClient {
135        &self.client
136    }
137
138    /// Get a reference to the underlying PostgreSQL client.
139    ///
140    /// Returns an error if this `Waypoint` was constructed for a non-PostgreSQL
141    /// engine. Most legacy callers can keep using this; new code should prefer
142    /// [`Self::client`] which returns a backend-agnostic [`DbClient`].
143    #[cfg(feature = "postgres")]
144    pub fn postgres_client(&self) -> Result<&Client> {
145        self.client.as_postgres()
146    }
147
148    /// Apply pending migrations.
149    pub async fn migrate(&self, target_version: Option<&str>) -> Result<MigrateReport> {
150        self.migrate_with_options(target_version, false).await
151    }
152
153    /// Apply pending migrations with the additional `force` flag for
154    /// overriding DANGER safety verdicts (PostgreSQL only; MySQL safety
155    /// analysis does not currently gate migrations).
156    pub async fn migrate_with_options(
157        &self,
158        target_version: Option<&str>,
159        force: bool,
160    ) -> Result<MigrateReport> {
161        match self.client.dialect_kind() {
162            #[cfg(feature = "postgres")]
163            DialectKind::Postgres => {
164                commands::migrate::execute_with_options(
165                    self.client.as_postgres()?,
166                    &self.config,
167                    target_version,
168                    force,
169                )
170                .await
171            }
172            #[cfg(not(feature = "postgres"))]
173            DialectKind::Postgres => Err(error::WaypointError::ConfigError(
174                "PostgreSQL support is not compiled in (enable the `postgres` feature)".into(),
175            )),
176            #[cfg(feature = "mysql")]
177            DialectKind::Mysql => {
178                commands::migrate::execute_mysql_with_options(
179                    &self.client,
180                    &self.config,
181                    target_version,
182                    force,
183                )
184                .await
185            }
186            #[cfg(not(feature = "mysql"))]
187            DialectKind::Mysql => Err(error::WaypointError::ConfigError(
188                "MySQL support is not compiled in (enable the `mysql` feature)".into(),
189            )),
190        }
191    }
192
193    /// Show migration status information.
194    pub async fn info(&self) -> Result<Vec<MigrationInfo>> {
195        commands::info::execute_db(&self.client, &self.config).await
196    }
197
198    /// Validate applied migrations against local files.
199    pub async fn validate(&self) -> Result<ValidateReport> {
200        commands::validate::execute_db(&self.client, &self.config).await
201    }
202
203    /// Repair the schema history table.
204    pub async fn repair(&self) -> Result<RepairReport> {
205        commands::repair::execute_db(&self.client, &self.config).await
206    }
207
208    /// Baseline an existing database.
209    pub async fn baseline(&self, version: Option<&str>, description: Option<&str>) -> Result<()> {
210        commands::baseline::execute_db(&self.client, &self.config, version, description).await
211    }
212
213    /// Undo applied migrations.
214    pub async fn undo(&self, target: UndoTarget) -> Result<UndoReport> {
215        commands::undo::execute_db(&self.client, &self.config, target).await
216    }
217
218    /// Drop all objects in managed schemas.
219    pub async fn clean(&self, allow_clean: bool) -> Result<Vec<String>> {
220        commands::clean::execute_db(&self.client, &self.config, allow_clean).await
221    }
222
223    /// Run lint on migration files (no DB required).
224    pub fn lint(locations: &[PathBuf], disabled_rules: &[String]) -> Result<LintReport> {
225        commands::lint::execute(locations, disabled_rules)
226    }
227
228    /// Generate changelog from migration files (no DB required).
229    pub fn changelog(
230        locations: &[PathBuf],
231        from: Option<&str>,
232        to: Option<&str>,
233    ) -> Result<ChangelogReport> {
234        commands::changelog::execute(locations, from, to)
235    }
236
237    /// Compare database schema against a target.
238    pub async fn diff(&self, target: commands::diff::DiffTarget) -> Result<DiffReport> {
239        commands::diff::execute_db(&self.client, &self.config, target).await
240    }
241
242    /// Detect schema drift.
243    pub async fn drift(&self) -> Result<DriftReport> {
244        commands::drift::execute_db(&self.client, &self.config).await
245    }
246
247    /// Take a schema snapshot.
248    pub async fn snapshot(
249        &self,
250        snapshot_config: &commands::snapshot::SnapshotConfig,
251    ) -> Result<SnapshotReport> {
252        commands::snapshot::execute_snapshot_db(&self.client, &self.config, snapshot_config).await
253    }
254
255    /// Restore from a schema snapshot.
256    pub async fn restore(
257        &self,
258        snapshot_config: &commands::snapshot::SnapshotConfig,
259        snapshot_id: &str,
260    ) -> Result<RestoreReport> {
261        commands::snapshot::execute_restore_db(
262            &self.client,
263            &self.config,
264            snapshot_config,
265            snapshot_id,
266        )
267        .await
268    }
269
270    /// Run enhanced dry-run with EXPLAIN.
271    pub async fn explain(&self) -> Result<ExplainReport> {
272        commands::explain::execute_db(&self.client, &self.config).await
273    }
274
275    /// Run pre-flight health checks.
276    pub async fn preflight(&self) -> Result<PreflightReport> {
277        preflight::run_preflight_db(&self.client, &self.config.preflight).await
278    }
279
280    /// Check for branch conflicts (no DB required).
281    pub fn check_conflicts(locations: &[PathBuf], base_branch: &str) -> Result<ConflictReport> {
282        commands::check_conflicts::execute(locations, base_branch)
283    }
284
285    /// Analyze pending migrations for safety (lock analysis, impact estimation).
286    pub async fn safety(&self) -> Result<SafetyCommandReport> {
287        commands::safety::execute_db(&self.client, &self.config).await
288    }
289
290    /// Run schema advisor to suggest improvements.
291    pub async fn advise(&self) -> Result<AdvisorReport> {
292        commands::advisor::execute_db(&self.client, &self.config).await
293    }
294
295    /// Simulate pending migrations in a throwaway schema.
296    pub async fn simulate(&self) -> Result<SimulationReport> {
297        commands::simulate::execute_db(&self.client, &self.config).await
298    }
299}
300
301/// Connect to whichever backend the URL scheme indicates.
302async fn connect_for_url(
303    conn_string: &str,
304    #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] config: &WaypointConfig,
305) -> Result<DbClient> {
306    let kind = DialectKind::from_url(conn_string).unwrap_or(DialectKind::Postgres);
307    match kind {
308        #[cfg(feature = "postgres")]
309        DialectKind::Postgres => {
310            let client = db::connect_with_full_config(
311                conn_string,
312                &config.database.ssl_mode,
313                config.database.connect_retries,
314                config.database.connect_timeout_secs,
315                config.database.statement_timeout_secs,
316                config.database.keepalive_secs,
317            )
318            .await?;
319            Ok(DbClient::with_postgres(client))
320        }
321        #[cfg(not(feature = "postgres"))]
322        DialectKind::Postgres => Err(error::WaypointError::ConfigError(
323            "PostgreSQL support is not compiled in (enable the `postgres` feature)".into(),
324        )),
325        #[cfg(feature = "mysql")]
326        DialectKind::Mysql => {
327            let pool = mysql_async::Pool::from_url(conn_string).map_err(|e| {
328                error::WaypointError::ConfigError(format!("Invalid MySQL connection URL: {}", e))
329            })?;
330            Ok(DbClient::with_mysql(pool))
331        }
332        #[cfg(not(feature = "mysql"))]
333        DialectKind::Mysql => Err(error::WaypointError::ConfigError(
334            "MySQL support is not compiled in (enable the `mysql` feature)".into(),
335        )),
336    }
337}