Skip to content

feat: add rollback/down migrations support#1

Open
roor0 wants to merge 4 commits intomainfrom
feat/down-migrations
Open

feat: add rollback/down migrations support#1
roor0 wants to merge 4 commits intomainfrom
feat/down-migrations

Conversation

@roor0
Copy link
Owner

@roor0 roor0 commented Mar 7, 2026

Summary

  • Adds bidirectional migration support across all 25 drivers — each .sql migration file can now have a corresponding .down.sql that reverses the changes
  • readMigrationFiles extended to parse .down.sql alongside each .sql; MigrationMeta gains optional downSql?: string[]
  • New rollback(db, config, steps?) exported from every driver migrator
  • drizzle-kit auto-generates down SQL by running the diff with swapped snapshots; writeResult writes .down.sql files; embeddedMigrations bundles down SQL imports for embedded drivers (expo-sqlite, op-sqlite, durable-sqlite)
  • Journal entry type gains optional hasDown?: boolean flag
  • SQLite rollback uses rowid for precise single-row deletion and falls back to folderMillis matching for bundled drivers that set hash: ''

Drivers covered

All 25 drivers: node-postgres, postgres-js, pglite, neon-serverless, neon-http, vercel-postgres, aws-data-api/pg, pg-proxy, mysql2, planetscale-serverless, tidb-serverless, singlestore, singlestore-proxy, mysql-proxy, better-sqlite3, bun-sqlite, bun-sql, sql-js, sqlite-proxy, libsql, d1, expo-sqlite, op-sqlite, durable-sqlite, xata-http

Test plan

  • pnpm vitest run tests/migrate/down-sql.test.ts (drizzle-kit) — 10 tests
  • pnpm vitest run tests/sqlite/rollback.test.ts (integration-tests) — 17 tests including bundled empty-hash scenario
  • pnpm vitest run tests/pg/pglite.test.ts (integration-tests) — PGlite rollback tests

roor0 added 4 commits March 6, 2026 17:54
Implements bidirectional migrations across all dialects and drivers.

drizzle-kit:
- `writeResult()` auto-generates and writes `<tag>.down.sql` alongside each `<tag>.sql` by calling the diff function with prev/cur arguments swapped (no-rename auto-resolvers used so it is non-interactive)
- `embeddedMigrations()` bundles `.down.sql` imports for expo-sqlite, op-sqlite, and durable-sqlite
- `Journal` entry type gains optional `hasDown: boolean` flag

drizzle-orm/src/migrator.ts:
- `MigrationMeta` gains `downSql?: string[]`
- `readMigrationFiles()` reads `<tag>.down.sql` alongside each migration if it exists

Dialect rollback() methods (pg-core, mysql-core, singlestore-core, sqlite-core sync+async):
- Query the last `steps` rows from `__drizzle_migrations` DESC
- Match each by hash against the in-memory migration list
- Throw a descriptive error if the migration file or down SQL is missing
- Execute down SQL statements in reverse order inside a transaction
- DELETE the rolled-back rows from `__drizzle_migrations`

Per-driver rollback() exports added to all 22 driver migrator files:
- Standard drivers (node-postgres, postgres-js, pglite, neon-serverless, vercel-postgres, aws-data-api/pg, mysql2, planetscale-serverless, tidb-serverless, singlestore, better-sqlite3, bun-sqlite, bun-sql, sql-js): delegate to dialect.rollback()
- Proxy drivers (pg-proxy, mysql-proxy, sqlite-proxy, singlestore-proxy): build queries array and pass to existing ProxyMigrator callback
- Batch drivers (libsql, d1): collect statements and use session.migrate()/session.batch()
- Embedded drivers (expo-sqlite, op-sqlite, durable-sqlite): MigrationConfig gains optional `downMigrations` bundle field; rollback works with the same embedded bundle pattern as migrate()
- neon-http, xata-http: custom implementations without transactions (noted in JSDoc)
drizzle-kit/tests/migrate/down-sql.test.ts (10 tests):
- writeResult() writes .down.sql when downSqlStatements are provided
- writeResult() does not write .down.sql when downSqlStatements is undefined
- writeResult() sets hasDown: true in journal entry when down SQL is provided
- writeResult() does not set hasDown when downSqlStatements is undefined
- writeResult() does not set hasDown when downSqlStatements is empty array
- writeResult() respects breakpoints delimiter in .down.sql
- embeddedMigrations() includes downMigrations block when entries have hasDown
- embeddedMigrations() omits downMigrations block when no entries have hasDown
- embeddedMigrations() only imports .down.sql for entries that have hasDown
- embeddedMigrations() adds expo header for expo driver

integration-tests/tests/sqlite/rollback.test.ts (13 tests):
- readMigrationFiles() populates downSql when .down.sql exists
- readMigrationFiles() leaves downSql undefined when .down.sql is absent
- readMigrationFiles() leaves downSql undefined when .down.sql is empty
- readMigrationFiles() leaves downSql undefined when .down.sql is whitespace only
- readMigrationFiles() splits downSql on statement-breakpoint delimiter
- readMigrationFiles() reads downSql independently per migration
- rollback(1) removes last migration table and tracking row
- rollback(2) undoes both migrations
- rollback with default steps=1 rolls back one migration
- rollback when no migrations applied is a no-op
- rollback then migrate re-applies the migration
- rollback throws when migration file not found by hash
- rollback throws when migration has no down SQL

Adds migration fixtures in integration-tests/drizzle2/sqlite-rollback/
with two migrations each having .down.sql files.
Wrap the forward resolvers with capture helpers so rename decisions
(table, column, view) made during `generate` are inverted for the
down SQL diff, matching the rename-aware approach already applied to
PostgreSQL, MySQL, and SingleStore.
Use rowid instead of hash for SQLite migration deletion to fix hash
collision bugs with bundled migrations (empty hash). Add PGlite rollback
integration tests and bundled migration rollback tests. Fix down-sql
test assertions for updated key format.
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