Skip to content

Commit 3073ca0

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

File tree

4 files changed

+238
-4
lines changed

4 files changed

+238
-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: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
# The important thing is that the migration succeeds despite the warnings
127+
assert result =~ "== Migrated"
128+
129+
index_result =
130+
Ecto.Adapters.SQL.query!(
131+
AshPostgres.TestRepo,
132+
"""
133+
SELECT indexname FROM pg_indexes
134+
WHERE schemaname = '#{tenant_name}'
135+
AND tablename = 'posts'
136+
AND indexname LIKE '%title%'
137+
""",
138+
[]
139+
)
140+
141+
assert length(index_result.rows) > 0
142+
143+
cleanup_migration_files(migration_file)
144+
end
145+
end
146+
147+
defp create_test_migration(filename, content) do
148+
# Create a unique directory for this specific test run
149+
timestamp = DateTime.utc_now() |> DateTime.to_unix(:microsecond) |> Integer.to_string()
150+
unique_id = System.unique_integer([:positive])
151+
152+
test_migrations_dir =
153+
Path.join(System.tmp_dir!(), "ash_postgres_test_migrations_#{timestamp}_#{unique_id}")
154+
155+
File.mkdir_p!(test_migrations_dir)
156+
157+
migration_filename = "#{timestamp}_#{filename}"
158+
migration_file = Path.join(test_migrations_dir, migration_filename)
159+
File.write!(migration_file, content)
160+
161+
# Don't compile the file here - let the migration system handle it
162+
migration_file
163+
end
164+
165+
defp cleanup_migration_files(migration_file) do
166+
migration_dir = Path.dirname(migration_file)
167+
File.rm_rf(migration_dir)
168+
end
169+
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)