Allow going down while in an inconsistent state.

Previously we prevent the user migrating the database in either
direction if there were missing or out-of-order migrations. However,
going down should always be safe (assuming proper down scripts were
written) and often going down is the proper way to resolve out-of-order
migrations. Going down should always be done thoughtfully (it is highly
likely to lead to data loss), and really should not be done in a
production setting.

However, in a development environment, it is not unusual to get into
states where you have missing scripts. For example, if there are test
scripts that apply test data to a development environment on top of the
formal application schema, you may need to back out the test migrations
before creating new application migrations to avoid getting into an
"inconsistent state." Consider the following migrations:

- 1-initial-schema
- 2-add-feature-x
- T1-test-user-data

If you then add *3-add-feature-y*, you will  be in an inconsistent
state, as the expected ordering from db_migrate's point of view will be:

- 1-initial-schema
- 2-add-feature-x
- 3-add-feature-y
- T1-test-user-data

With the previous behavior db_migrate refuses to do anything and you
must manually back-out the test migrations before applying the new
migration. With the new behavior you can easily go down to back out the
test migrations automatically before going back up with the new migrations.

As an aside:

Another way to avoid this is to name test migrations in such a way that
they stay inline with the changes they are built on top of:

- 1-initial-schema
- 1.1-test-user-data
- 2-add-feature-x
- 2.1-feature-x-test-data

But sometimes the previous pattern is preferable, as it allows you to
have test migrations that evolve and match the "current state of test
data" rather than a developer needing to layer the contents of multiple
migrations to get a clear picture of the test data. This same issue
applies to non-test migrations, but db_migrate exists to solve the
use-case where you generally prefer to have traversible, immutable
layers that are used to manage production database schema migrations
downtown.
This commit is contained in:
2025-08-13 08:12:35 -05:00
parent 2dbe3ea07c
commit f5943a69f0
2 changed files with 12 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
# Package # Package
bin = @["db_migrate"] bin = @["db_migrate"]
version = "0.3.2" version = "0.4.0"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Simple tool to handle database migrations." description = "Simple tool to handle database migrations."
license = "BSD" license = "BSD"

View File

@@ -249,7 +249,7 @@ Options:
""" """
# Parse arguments # Parse arguments
let args = docopt(doc, version = "db-migrate (Nim) 0.3.2\nhttps://git.jdb-software.com/jdb/db-migrate") let args = docopt(doc, version = "db-migrate (Nim) 0.4.0\nhttps://git.jdb-software.com/jdb/db-migrate")
let exitErr = proc(msg: string): void = let exitErr = proc(msg: string): void =
fatal("db_migrate: " & msg) fatal("db_migrate: " & msg)
@@ -307,12 +307,12 @@ Options:
let (available, run, notRun, missing) = diffMigrations(pgConn, config) let (available, run, notRun, missing) = diffMigrations(pgConn, config)
if args["up"]:
# Make sure we have no gaps (database is in an unknown state) # Make sure we have no gaps (database is in an unknown state)
if missing.len > 0: if missing.len > 0:
exitErr "Database is in an inconsistent state. Migrations have been " & exitErr "Database is in an inconsistent state. Migrations have been " &
"run that are not sequential." "run that are not sequential."
if args["up"]:
try: try:
let count = if args["<count>"]: parseInt($args["<count>"]) else: high(int) let count = if args["<count>"]: parseInt($args["<count>"]) else: high(int)
let toRun = if count < notRun.len: notRun[0..<count] else: notRun let toRun = if count < notRun.len: notRun[0..<count] else: notRun
@@ -334,6 +334,11 @@ Options:
elif args["init"]: discard elif args["init"]: discard
let newResults = diffMigrations(pgConn, config) let newResults = diffMigrations(pgConn, config)
if newResults.missing.len > 0:
exitErr "Database is in an inconsistent state. Migrations have been " &
"run that are not sequential."
if newResults.notRun.len > 0: if newResults.notRun.len > 0:
info "Database is behind by " & $(newResults.notRun.len) & " migrations." info "Database is behind by " & $(newResults.notRun.len) & " migrations."
else: info "Database is up to date." else: info "Database is up to date."