Skip to content

feat(migrate): add MigrateRender trait and Migrator::render_pending_sql - #2

Open
knutties wants to merge 1 commit into
feat/migrator-pendingfrom
feat/migrate-render-sql
Open

knutties wants to merge 1 commit into
feat/migrator-pendingfrom
feat/migrate-render-sql

Conversation

@knutties

Copy link
Copy Markdown
Owner

Summary

Adds a per-driver SQL-rendering helper so a library user can dump the SQL that apply() would execute for pending migrations — for DBA review, out-of-band application via psql/mysql/sqlite3, or pre-deploy inspection. Today this requires copying private execute_migration SQL from each driver crate (INSERT into _sqlx_migrations, bytea-literal syntax, checksum hex, transaction framing, MySQL's success=FALSE/UPDATE TRUE two-step). The CLI's existing --dry-run only prints status lines.

Targets upstream issue transact-rs#357 (the original "SQL that would have been executed, including the adjustments to the migrations table" ask was never delivered; transact-rs#357 was closed by transact-rs#649 which only added status-line output).

Stacking

Stacked on PR #1 (feat/migrator-pending). This PR's base is set to that branch so its diff shows phase-2-only changes (~317 lines). Once PR #1 lands in main, GitHub will auto-rebase this PR's base.

Migrator::render_pending_sql calls pending_direct (introduced in PR #1) rather than duplicating the diff loop.

API

pub trait MigrateRender {
    fn append_apply_sql(&self, table_name: &str, migration: &Migration, buf: &mut String);
}

impl Migrator {
    pub async fn render_pending_sql<'a, A>(&self, migrator: A) -> Result<String, MigrateError>
    where
        A: Acquire<'a>,
        <A::Connection as Deref>::Target: Migrate + MigrateRender;
}

Plus #[doc(hidden)] pub fn append_hex and append_sql_string in sqlx-core::migrate::render for driver-crate reuse.

Per-driver behavior (mirrors each apply() byte-for-byte)

Driver Framing Bytea literal Notes
Postgres BEGIN; … COMMIT; (skipped on Migration::no_tx) '\xHEX'::bytea Mirrors execute_migration exactly
SQLite BEGIN; … COMMIT; (skipped on no_tx) X'HEX' Mirrors execute_migration exactly
MySQL START TRANSACTION; INSERT(success=FALSE); <sql>; UPDATE TRUE; COMMIT; X'HEX' Mirrors the FALSE→TRUE two-step needed because MySQL's DDL implicitly commits; no_tx is intentionally ignored, matching apply()

Self-applying guarantee

Output includes the bookkeeping INSERT INTO _sqlx_migrations row with the migration's SHA-384 checksum and execution_time = -1. Executing the rendered SQL produces the same post-state as apply(), so a subsequent Migrator::run() sees those versions as applied (no checksum mismatch, no re-execution).

Test plan

End-to-end test in each of the three driver test suites: render the pending SQL, execute it via raw_sql(AssertSqlSafe(...)), assert pending is then empty, assert a follow-up run is a no-op (proves checksums match byte-for-byte what sqlx computes), assert re-rendering yields the empty string.

  • cargo check -p sqlx-core clean
  • cargo clippy -p sqlx-core clean (no new warnings)
  • SQLite test runs locally and passes (full 7-test suite passes — 5 existing + pending from PR feat(migrate): add Migrator::pending to inspect unapplied migrations #1 + new render_pending_sql).
  • Postgres test runs locally against PG 16 — both pending and render_pending_sql pass end-to-end.
  • MySQL test compiles (cargo test --no-run); not exercised against a live MySQL locally. Implementation validated by symmetry with PG/SQLite and direct comparison against MySqlConnection::apply().

Two design decisions worth review

  1. Separate trait MigrateRender rather than method on Migrate. Migrate's methods are all async + &mut self + BoxFuture; a sync &self-ignoring sibling method would clash with that shape. Easy to fold into Migrate if reviewers prefer.
  2. append_apply_sql ignores &self. Pure formatting, no connection state needed. &self is required for trait dispatch via the connection type. Static method (no &self) was an option but verbose at call sites. Easy to flip.

…g_sql`

A library user wanting to dump the SQL that pending migrations would
execute — for DBA review, out-of-band application, or pre-deploy
inspection — currently has to copy `sqlx-postgres`/`sqlx-mysql`/
`sqlx-sqlite`'s private `execute_migration`/`apply()` body verbatim:
the per-driver INSERT into `_sqlx_migrations`, bytea-literal syntax
(`'\xHEX'::bytea` / `X'HEX'`), checksum hex encoding, and per-driver
transaction framing (Postgres/SQLite honor `Migration::no_tx`; MySQL
always wraps in `START TRANSACTION` and uses a `success=FALSE` →
`UPDATE TRUE` pair due to implicit DDL commits). The CLI's existing
`--dry-run` only prints status lines; it does not emit SQL.

Add a sibling-of-`Migrate` trait, `MigrateRender`, with a single sync
method:

    fn append_apply_sql(&self, table_name, migration, buf: &mut String)

implemented per driver to mirror that driver's `apply()` exactly. The
output is **self-applying**: executing it produces the same post-state
as `apply()` (bookkeeping rows included, with the migration's SHA-384
checksum), so a subsequent `Migrator::run()` sees those versions as
applied and does not re-execute.

`Migrator::render_pending_sql(&conn)` ties phase 1's `pending` to the
new trait: returns a single `String` containing every pending migration
preceded by a `-- migration <version> (<description>)` header.
`pending_direct`'s validation (dirty DB, checksum drift) is shared, so
errors match `run`'s.

Two small `pub` helpers (`append_hex`, `append_sql_string`) live in
`sqlx-core::migrate::render` under `#[doc(hidden)]` for the driver
crates to reuse. They are not part of the documented public API.

Tests: end-to-end across all three drivers — render the pending SQL,
execute it via `raw_sql(AssertSqlSafe(...))`, assert `pending` is then
empty (proves bookkeeping rows are correct), assert a follow-up `run`
is a no-op (proves checksums match byte-for-byte what sqlx computes),
assert re-rendering yields the empty string. SQLite tests run locally
via `cargo test`; Postgres test verified locally against PG 16 (`pending`
+ `render_pending_sql` both pass); MySQL test compiles but was not
exercised against a live MySQL server.

Closes transact-rs#357.

Stacked on the `Migrator::pending` change (separate commit, separate
upstream PR) — `render_pending_sql` calls `pending_direct` rather than
duplicating the diff loop.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant