When sqlite-utils migrate loads multiple migration sets, an already-applied --stop-before target in a later set causes an error after earlier sets have committed pending migrations. The CLI validates that targets exist before applying anything, but the already-applied check happens inside each set's Migrations.apply(), so it comes too late for earlier sets.
The documented --stop-before behavior says an already-applied target is an error “and no pending migrations are applied.”
Reproduction (Python 3.12.12, sqlite-utils 4.2.1 at 85b1be10c81d9dd3567e36faf8dd411e4a8789bd): save this as repro.py and run python repro.py in an environment with sqlite-utils installed.
from pathlib import Path
from tempfile import TemporaryDirectory
from click.testing import CliRunner
from sqlite_utils import Database
from sqlite_utils.cli import cli
with TemporaryDirectory() as tmp:
root = Path(tmp)
for name in ("a", "b"):
(root / f"{name}.py").write_text(
"from sqlite_utils import Migrations\n"
f'migrations = Migrations("{name}")\n'
"@migrations()\n"
"def first(db):\n"
f' db.table("{name}").insert({{"value": 1}})\n'
)
db_path = str(root / "test.db")
runner = CliRunner()
seed = runner.invoke(cli, ["migrate", db_path, str(root / "b.py")])
if seed.exit_code != 0:
raise seed.exception
result = runner.invoke(cli, [
"migrate", db_path, str(root / "a.py"), str(root / "b.py"),
"--stop-before", "b:first",
])
print("Exit code:", result.exit_code)
print(result.output.strip())
with Database(db_path) as db:
print("Earlier pending migration was applied:", db.table("a").exists())
print("Recorded migrations:", [(r["migration_set"], r["name"]) for r in db.table("_sqlite_migrations").rows])
Output:
Exit code: 1
Error: Cannot stop before migration first in set 'b' - already been applied
Earlier pending migration was applied: True
Recorded migrations: [('b', 'first'), ('a', 'first')]
Expected: the same error, but table a should not exist and only b:first should be recorded. The failed command should leave a:first pending.
The validation in cli.py:migrate could check already-applied targets across all loaded sets before entering the application loop. The current check in Migrations.apply() protects only that individual set.
The reproduction gives the same result on two fresh databases. The existing migration tests pass (31 passed across tests/test_migrations.py and tests/test_cli_migrate.py); the applied-target CLI test currently covers one set.
When
sqlite-utils migrateloads multiple migration sets, an already-applied--stop-beforetarget in a later set causes an error after earlier sets have committed pending migrations. The CLI validates that targets exist before applying anything, but the already-applied check happens inside each set'sMigrations.apply(), so it comes too late for earlier sets.The documented
--stop-beforebehavior says an already-applied target is an error “and no pending migrations are applied.”Reproduction (Python 3.12.12, sqlite-utils 4.2.1 at
85b1be10c81d9dd3567e36faf8dd411e4a8789bd): save this asrepro.pyand runpython repro.pyin an environment with sqlite-utils installed.Output:
Expected: the same error, but table
ashould not exist and onlyb:firstshould be recorded. The failed command should leavea:firstpending.The validation in
cli.py:migratecould check already-applied targets across all loaded sets before entering the application loop. The current check inMigrations.apply()protects only that individual set.The reproduction gives the same result on two fresh databases. The existing migration tests pass (
31 passedacrosstests/test_migrations.pyandtests/test_cli_migrate.py); the applied-target CLI test currently covers one set.