Skip to content

Commit 6dbea1f

Browse files
committed
Verify migrations at startup in testing mode
When Oban starts in a testing mode, it now verifies that migrations have been run and are up to date. This catches migration issues early in CI rather than failing with confusing database errors during test execution or worse, in production. For Postgres, the check verifies the migration version is current, while for SQLite and MySQL, the check verifies the `oban_jobs` table exists. Addresses #1429
1 parent b991d4f commit 6dbea1f

File tree

7 files changed

+153
-10
lines changed

7 files changed

+153
-10
lines changed

lib/oban.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,9 +511,17 @@ defmodule Oban do
511511
def start_link(opts) when is_list(opts) do
512512
conf = Config.new(opts)
513513

514+
verify_migrated!(conf)
515+
514516
Supervisor.start_link(__MODULE__, conf, name: Registry.via(conf.name, nil, conf))
515517
end
516518

519+
defp verify_migrated!(conf) do
520+
if conf.testing != :disabled do
521+
Oban.Migration.verify_migrated!(repo: conf.repo, prefix: conf.prefix)
522+
end
523+
end
524+
517525
@doc false
518526
@spec child_spec([option]) :: Supervisor.child_spec()
519527
def child_spec(opts) do

lib/oban/migration.ex

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,18 @@ defmodule Oban.Migration do
159159
Oban.Migration.up(unlogged: false)
160160
"""
161161
def up(opts \\ []) when is_list(opts) do
162-
migrator().up(opts)
162+
migrator(opts).up(opts)
163+
end
164+
165+
@doc """
166+
Get the current migration version.
167+
168+
## Example
169+
170+
Oban.Migration.current_version()
171+
"""
172+
def current_version(opts \\ []) when is_list(opts) do
173+
migrator(opts).current_version()
163174
end
164175

165176
@doc """
@@ -180,7 +191,7 @@ defmodule Oban.Migration do
180191
Oban.Migration.down(prefix: "payments")
181192
"""
182193
def down(opts \\ []) when is_list(opts) do
183-
migrator().down(opts)
194+
migrator(opts).down(opts)
184195
end
185196

186197
@doc """
@@ -191,11 +202,60 @@ defmodule Oban.Migration do
191202
Oban.Migration.migrated_version()
192203
"""
193204
def migrated_version(opts \\ []) when is_list(opts) do
194-
migrator().migrated_version(opts)
205+
migrator(opts).migrated_version(opts)
206+
end
207+
208+
@doc false
209+
def verify_migrated!(opts \\ []) when is_list(opts) do
210+
current = current_version(opts)
211+
version = migrated_version(opts)
212+
213+
cond do
214+
version == 0 ->
215+
raise RuntimeError, """
216+
Oban migrations have not been run. The oban_jobs table does not exist.
217+
218+
Run migrations before starting Oban:
219+
220+
mix ecto.migrate
221+
222+
Or add a migration:
223+
224+
defmodule MyApp.Repo.Migrations.AddOban do
225+
use Ecto.Migration
226+
227+
def up, do: Oban.Migrations.up()
228+
def down, do: Oban.Migrations.down()
229+
end
230+
"""
231+
232+
version < current ->
233+
raise RuntimeError, """
234+
Oban migrations are outdated. Found version #{version}, but version #{current} is required.
235+
236+
Run migrations to update:
237+
238+
mix ecto.migrate
239+
240+
Or add a migration:
241+
242+
defmodule MyApp.Repo.Migrations.UpdateObanToV#{current} do
243+
use Ecto.Migration
244+
245+
def up, do: Oban.Migrations.up(version: #{current})
246+
def down, do: Oban.Migrations.down(version: #{version})
247+
end
248+
"""
249+
250+
true ->
251+
:ok
252+
end
195253
end
196254

197-
defp migrator do
198-
case repo().__adapter__() do
255+
defp migrator(opts) do
256+
repo = Keyword.get_lazy(opts, :repo, &repo/0)
257+
258+
case repo.__adapter__() do
199259
Ecto.Adapters.Postgres -> Oban.Migrations.Postgres
200260
Ecto.Adapters.SQLite3 -> Oban.Migrations.SQLite
201261
Ecto.Adapters.MyXQL -> Oban.Migrations.MyXQL

lib/oban/migrations/myxql.ex

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule Oban.Migrations.MyXQL do
55

66
use Ecto.Migration
77

8+
def current_version, do: 1
9+
810
@impl Oban.Migration
911
def up(_opts) do
1012
states =
@@ -65,5 +67,21 @@ defmodule Oban.Migrations.MyXQL do
6567
end
6668

6769
@impl Oban.Migration
68-
def migrated_version(_opts), do: 0
70+
def migrated_version(opts) do
71+
repo = Keyword.get_lazy(opts, :repo, fn -> repo() end)
72+
config = repo.config()
73+
database = Keyword.fetch!(config, :database)
74+
75+
query = """
76+
SELECT 1
77+
FROM information_schema.TABLES
78+
WHERE TABLE_SCHEMA = '#{database}'
79+
AND TABLE_NAME = 'oban_jobs'
80+
"""
81+
82+
case repo.query(query, [], log: false) do
83+
{:ok, %{rows: [[1]]}} -> 1
84+
_ -> 0
85+
end
86+
end
6987
end

lib/oban/migrations/sqlite.ex

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule Oban.Migrations.SQLite do
55

66
use Ecto.Migration
77

8+
def current_version, do: 1
9+
810
@impl Oban.Migration
911
def up(_opts) do
1012
create_if_not_exists table(:oban_jobs, primary_key: false) do
@@ -49,5 +51,16 @@ defmodule Oban.Migrations.SQLite do
4951
end
5052

5153
@impl Oban.Migration
52-
def migrated_version(_opts), do: 0
54+
def migrated_version(opts) do
55+
repo = Keyword.get_lazy(opts, :repo, fn -> repo() end)
56+
57+
query = """
58+
SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'oban_jobs'
59+
"""
60+
61+
case repo.query(query, [], log: false) do
62+
{:ok, %{rows: [[1]]}} -> 1
63+
_ -> 0
64+
end
65+
end
5366
end

test/oban/migrations/myxql_test.exs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ defmodule Oban.Migrations.MyXQLTest do
1313
end
1414
end
1515

16-
@moduletag :lite
16+
@moduletag :dolphin
1717

1818
defmodule Migration do
1919
use Ecto.Migration
@@ -27,9 +27,27 @@ defmodule Oban.Migrations.MyXQLTest do
2727
end
2828
end
2929

30-
test "migrating a mysql database" do
30+
defp storage_up(_conf) do
3131
MigrationRepo.__adapter__().storage_up(MigrationRepo.config())
32+
end
33+
34+
defp storage_down do
35+
MigrationRepo.__adapter__().storage_down(MigrationRepo.config())
36+
end
37+
38+
setup :storage_up
39+
40+
test "verifying that any migrations have ran" do
41+
start_supervised!(MigrationRepo)
3242

43+
assert_raise RuntimeError, ~r/migrations have not been run/, fn ->
44+
start_supervised_oban!(repo: MigrationRepo, testing: :manual)
45+
end
46+
after
47+
storage_down()
48+
end
49+
50+
test "migrating a mysql database" do
3351
start_supervised!(MigrationRepo)
3452

3553
assert :ok = Ecto.Migrator.up(MigrationRepo, 1, Migration)
@@ -40,7 +58,7 @@ defmodule Oban.Migrations.MyXQLTest do
4058
refute table_exists?("oban_jobs")
4159
refute table_exists?("oban_peers")
4260
after
43-
MigrationRepo.__adapter__().storage_down(MigrationRepo.config())
61+
storage_down()
4462
end
4563

4664
defp table_exists?(name) do

test/oban/migrations/postgres_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,24 @@ defmodule Oban.Migrations.PostgresTest do
5555

5656
@base_version 20_300_000_000_000
5757

58+
test "verifying that any migrations have ran" do
59+
assert_raise RuntimeError, ~r/migrations have not been run/, fn ->
60+
start_supervised_oban!(repo: Repo, testing: :manual, prefix: "does_not_exist")
61+
end
62+
end
63+
64+
test "verifying the database is migrated to the required version" do
65+
Application.put_env(:oban, :up_version, current_version() - 1)
66+
67+
assert :ok = Ecto.Migrator.up(UnboxedRepo, @base_version, StepMigration)
68+
69+
assert_raise RuntimeError, ~r/migrations are outdated/, fn ->
70+
start_supervised_oban!(repo: UnboxedRepo, testing: :manual, prefix: "migrating")
71+
end
72+
after
73+
clear_migrated()
74+
end
75+
5876
test "migrating up and down between specific versions" do
5977
for up <- initial_version()..current_version() do
6078
Application.put_env(:oban, :up_version, up)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ defmodule Oban.Migrations.SQLiteTest do
2727
end
2828
end
2929

30+
test "verifying that any migrations have ran" do
31+
start_supervised!(MigrationRepo)
32+
33+
assert_raise RuntimeError, ~r/migrations have not been run/, fn ->
34+
start_supervised_oban!(repo: MigrationRepo, testing: :manual)
35+
end
36+
end
37+
3038
test "migrating a sqlite database" do
3139
start_supervised!(MigrationRepo)
3240

0 commit comments

Comments
 (0)