neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Renumber a migration that collided, and catch the next one
51830b0 · Gabriel Morell · 2026-09-05 20:40
Message
{commit_body(@commit)}
Files changed
renamed
apps/room_sanctum/priv/repo/migrations/20260905120000_add_tint_to_foci.exs → apps/room_sanctum/priv/repo/migrations/20260905122000_add_tint_to_foci.exs
+5
−0
@@ -10,6 +10,11 @@ defmodule RoomSanctum.Repo.Migrations.AddTintToFoci do
| 10 | 10 | a tint of foci gains a new one the moment it is tinted. |
| 11 | 11 | """ |
| 12 | 12 | |
| 13 | + # Renumbered from 20260905120000, which collided with add_plus_to_pythiae. | |
| 14 | + # Ecto keys schema_migrations on the version alone, so the first of the two | |
| 15 | + # to run recorded the number and the second was skipped -- while still | |
| 16 | + # reporting `:up`, because the version it is looked up by was there. | |
| 17 | + # Silent, and it looked exactly like a migration that had run. | |
| 13 | 18 | def change do |
| 14 | 19 | alter table(:cfg_focis) do |
| 15 | 20 | add :tint, :string |
added
apps/room_sanctum/test/room_sanctum/migrations_test.exs
+44
−0
@@ -0,0 +1,44 @@
| 1 | +defmodule RoomSanctum.MigrationsTest do | |
| 2 | + use ExUnit.Case, async: true | |
| 3 | + | |
| 4 | + @migrations Path.join([__DIR__, "..", "..", "priv", "repo", "migrations"]) | |
| 5 | + | |
| 6 | + # What Ecto itself considers a migration: a leading version, then a name. | |
| 7 | + # The directory also holds a .formatter.exs, which is not one. | |
| 8 | + defp migration_files do | |
| 9 | + @migrations | |
| 10 | + |> File.ls!() | |
| 11 | + |> Enum.filter(&Regex.match?(~r/^\d+_.*\.exs$/, &1)) | |
| 12 | + end | |
| 13 | + | |
| 14 | + test "no two migrations share a version" do | |
| 15 | + # Ecto keys schema_migrations on the version alone. Two files with the | |
| 16 | + # same number means the first to run records it and the second is skipped | |
| 17 | + # -- and then *reports itself up*, because the version it is looked up by | |
| 18 | + # is there. Nothing errors, nothing logs, and the column simply does not | |
| 19 | + # exist. That cost an afternoon of a Plani crash-looping in production | |
| 20 | + # against a table it had been told was migrated. | |
| 21 | + duplicates = | |
| 22 | + migration_files() | |
| 23 | + |> Enum.group_by(fn file -> file |> String.split("_") |> hd() end) | |
| 24 | + |> Enum.filter(fn {_version, files} -> length(files) > 1 end) | |
| 25 | + | |
| 26 | + assert duplicates == [], | |
| 27 | + "these migrations share a version, and all but one will be skipped:\n" <> | |
| 28 | + Enum.map_join(duplicates, "\n", fn {version, files} -> | |
| 29 | + " #{version}: #{Enum.join(files, ", ")}" | |
| 30 | + end) | |
| 31 | + end | |
| 32 | + | |
| 33 | + test "every migration is named after a version that sorts" do | |
| 34 | + # Ecto runs them in version order, so a name it cannot parse a number out | |
| 35 | + # of would run somewhere unpredictable. | |
| 36 | + for file <- migration_files() do | |
| 37 | + assert {version, "_" <> _rest} = Integer.parse(file) | |
| 38 | + assert version > 0, "#{file} has no leading version" | |
| 39 | + end | |
| 40 | + | |
| 41 | + # And that the filter is not vacuously true. | |
| 42 | + assert length(migration_files()) > 10 | |
| 43 | + end | |
| 44 | +end |
Parents: b0906f0