Skip to content

Commit 89c913b

Browse files
committed
fix: tenant migrations should honor module attributes
such as @disable_ddl_transaction and @disable_migration_lock
1 parent 913f936 commit 89c913b

File tree

4 files changed

+235
-4
lines changed

4 files changed

+235
-4
lines changed

lib/migration_generator/migration_generator.ex

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,14 @@ defmodule AshPostgres.MigrationGenerator do
555555
version in versions
556556
end)
557557
|> Enum.each(fn {version, mod} ->
558+
runner_opts =
559+
[
560+
all: true,
561+
prefix: prefix
562+
]
563+
|> maybe_put_mod_attribute(mod, :disable_ddl_transaction)
564+
|> maybe_put_mod_attribute(mod, :disable_migration_lock)
565+
558566
Ecto.Migration.Runner.run(
559567
repo,
560568
[],
@@ -563,8 +571,7 @@ defmodule AshPostgres.MigrationGenerator do
563571
:forward,
564572
:down,
565573
:down,
566-
all: true,
567-
prefix: prefix
574+
runner_opts
568575
)
569576

570577
Ecto.Migration.SchemaMigration.down(repo, repo.config(), version, prefix: prefix)
@@ -3858,4 +3865,14 @@ defmodule AshPostgres.MigrationGenerator do
38583865

38593866
defp to_ordered_object(value) when is_list(value), do: Enum.map(value, &to_ordered_object/1)
38603867
defp to_ordered_object(value), do: value
3868+
3869+
defp maybe_put_mod_attribute(opts, mod, attribute) do
3870+
migration_config = mod.__migration__()
3871+
3872+
case migration_config[attribute] do
3873+
nil -> opts
3874+
false -> opts
3875+
value -> Keyword.put(opts, attribute, value)
3876+
end
3877+
end
38613878
end

lib/multitenancy.ex

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ defmodule AshPostgres.MultiTenancy do
4646
|> Enum.filter(& &1)
4747
|> Enum.map(&load_migration!/1)
4848
|> Enum.each(fn {version, mod} ->
49+
runner_opts =
50+
[
51+
all: true,
52+
prefix: tenant_name
53+
]
54+
|> maybe_put_mod_attribute(mod, :disable_ddl_transaction)
55+
|> maybe_put_mod_attribute(mod, :disable_migration_lock)
56+
4957
Ecto.Migration.Runner.run(
5058
repo,
5159
[],
@@ -54,8 +62,7 @@ defmodule AshPostgres.MultiTenancy do
5462
:forward,
5563
:up,
5664
:up,
57-
all: true,
58-
prefix: tenant_name
65+
runner_opts
5966
)
6067

6168
Ecto.Migration.SchemaMigration.up(repo, repo.config(), version, prefix: tenant_name)
@@ -121,4 +128,14 @@ defmodule AshPostgres.MultiTenancy do
121128
defp tenant_name_regex do
122129
~r/^[a-zA-Z0-9_-]+$/
123130
end
131+
132+
defp maybe_put_mod_attribute(opts, mod, attribute) do
133+
migration_config = mod.__migration__()
134+
135+
case migration_config[attribute] do
136+
nil -> opts
137+
false -> opts
138+
value -> Keyword.put(opts, attribute, value)
139+
end
140+
end
124141
end
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# SPDX-FileCopyrightText: 2019 ash_postgres contributors <https://github.com/ash-project/ash_postgres/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule AshPostgres.MigrationModuleAttributesNoSandboxTest do
6+
use AshPostgres.RepoNoSandboxCase, async: false
7+
@moduletag :migration
8+
9+
import ExUnit.CaptureLog
10+
11+
setup do
12+
timestamp = DateTime.utc_now() |> DateTime.to_unix(:microsecond)
13+
unique_id = System.unique_integer([:positive])
14+
tenant_name = "test_no_sandbox_tenant_#{timestamp}_#{unique_id}"
15+
16+
Ecto.Adapters.SQL.query!(
17+
AshPostgres.TestRepo,
18+
"CREATE SCHEMA IF NOT EXISTS \"#{tenant_name}\"",
19+
[]
20+
)
21+
22+
Ecto.Adapters.SQL.query!(
23+
AshPostgres.TestRepo,
24+
"CREATE TABLE \"#{tenant_name}\".posts (id serial PRIMARY KEY, title text)",
25+
[]
26+
)
27+
28+
on_exit(fn ->
29+
Ecto.Adapters.SQL.query!(AshPostgres.TestRepo, "DROP SCHEMA \"#{tenant_name}\" CASCADE", [])
30+
end)
31+
32+
%{tenant_name: tenant_name}
33+
end
34+
35+
describe "migration attributes without sandbox" do
36+
test "tenant migration with @disable_ddl_transaction can create concurrent index", %{
37+
tenant_name: tenant_name
38+
} do
39+
migration_content = """
40+
defmodule TestConcurrentIndexMigrationNoSandbox do
41+
use Ecto.Migration
42+
@disable_ddl_transaction true
43+
@disable_migration_lock true
44+
45+
def up do
46+
create index(:posts, [:title], concurrently: true)
47+
end
48+
49+
def down do
50+
drop index(:posts, [:title])
51+
end
52+
end
53+
"""
54+
55+
IO.puts(
56+
"You should not not see a warning in this test about missing @disable_ddl_transaction"
57+
)
58+
59+
migration_file =
60+
create_test_migration("test_concurrent_index_migration_no_sandbox.exs", migration_content)
61+
62+
result =
63+
capture_log(fn ->
64+
AshPostgres.MultiTenancy.migrate_tenant(
65+
tenant_name,
66+
AshPostgres.TestRepo,
67+
Path.dirname(migration_file)
68+
)
69+
end)
70+
71+
assert result =~ "== Migrated"
72+
73+
index_result =
74+
Ecto.Adapters.SQL.query!(
75+
AshPostgres.TestRepo,
76+
"""
77+
SELECT indexname FROM pg_indexes
78+
WHERE schemaname = '#{tenant_name}'
79+
AND tablename = 'posts'
80+
AND indexname LIKE '%title%'
81+
""",
82+
[]
83+
)
84+
85+
assert length(index_result.rows) > 0
86+
87+
cleanup_migration_files(migration_file)
88+
end
89+
90+
test "tenant migration without @disable_ddl_transaction gives warnings", %{
91+
tenant_name: tenant_name
92+
} do
93+
migration_content = """
94+
defmodule TestConcurrentIndexMigrationWithoutDisableNoSandbox do
95+
use Ecto.Migration
96+
97+
def up do
98+
create index(:posts, [:title], concurrently: true)
99+
end
100+
101+
def down do
102+
drop index(:posts, [:title])
103+
end
104+
end
105+
"""
106+
107+
IO.puts("You should see a warning in this test about missing @disable_ddl_transaction")
108+
109+
migration_file =
110+
create_test_migration(
111+
"test_concurrent_index_migration_without_disable_no_sandbox.exs",
112+
migration_content
113+
)
114+
115+
result =
116+
capture_log(fn ->
117+
AshPostgres.MultiTenancy.migrate_tenant(
118+
tenant_name,
119+
AshPostgres.TestRepo,
120+
Path.dirname(migration_file)
121+
)
122+
end)
123+
124+
# The warnings are printed to the console (visible in test output above)
125+
# We can see them in the test output, but they're not captured by capture_log
126+
assert result =~ "== Migrated"
127+
128+
index_result =
129+
Ecto.Adapters.SQL.query!(
130+
AshPostgres.TestRepo,
131+
"""
132+
SELECT indexname FROM pg_indexes
133+
WHERE schemaname = '#{tenant_name}'
134+
AND tablename = 'posts'
135+
AND indexname LIKE '%title%'
136+
""",
137+
[]
138+
)
139+
140+
assert length(index_result.rows) > 0
141+
142+
cleanup_migration_files(migration_file)
143+
end
144+
end
145+
146+
defp create_test_migration(filename, content) do
147+
timestamp = DateTime.utc_now() |> DateTime.to_unix(:microsecond) |> Integer.to_string()
148+
unique_id = System.unique_integer([:positive])
149+
150+
test_migrations_dir =
151+
Path.join(System.tmp_dir!(), "ash_postgres_test_migrations_#{timestamp}_#{unique_id}")
152+
153+
File.mkdir_p!(test_migrations_dir)
154+
155+
migration_filename = "#{timestamp}_#{filename}"
156+
migration_file = Path.join(test_migrations_dir, migration_filename)
157+
File.write!(migration_file, content)
158+
159+
migration_file
160+
end
161+
162+
defp cleanup_migration_files(migration_file) do
163+
migration_dir = Path.dirname(migration_file)
164+
File.rm_rf(migration_dir)
165+
end
166+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# SPDX-FileCopyrightText: 2019 ash_postgres contributors <https://github.com/ash-project/ash_postgres/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule AshPostgres.RepoNoSandboxCase do
6+
@moduledoc """
7+
Test case for testing database operations without sandbox transaction wrapping.
8+
9+
This is useful for testing operations that cannot run inside transactions,
10+
such as concurrent index creation with @disable_ddl_transaction.
11+
"""
12+
use ExUnit.CaseTemplate
13+
14+
using do
15+
quote do
16+
alias AshPostgres.TestRepo
17+
18+
import Ecto
19+
import Ecto.Query
20+
import AshPostgres.RepoNoSandboxCase
21+
22+
# and any other stuff
23+
end
24+
end
25+
26+
setup _tags do
27+
# No sandbox setup - just ensure the repo is available
28+
# This allows testing operations that cannot run in transactions
29+
:ok
30+
end
31+
end

0 commit comments

Comments
 (0)