Skip to content

Commit 2462c64

Browse files
author
Amish Patel
authored
Updates structure dump to support multiple prefixes on pg and myxql (#490)
1 parent e79677f commit 2462c64

File tree

6 files changed

+214
-26
lines changed

6 files changed

+214
-26
lines changed

integration_test/myxql/storage_test.exs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,53 @@ defmodule Ecto.Integration.StorageTest do
150150
:ok = Ecto.Migrator.up(PoolRepo, num, Migration, log: false)
151151
{:ok, path} = Ecto.Adapters.MyXQL.structure_dump(tmp_path(), TestRepo.config())
152152
contents = File.read!(path)
153-
assert contents =~ "INSERT INTO `schema_migrations` (version) VALUES ("
153+
assert contents =~ "INSERT INTO `ecto_test`.`schema_migrations` (version) VALUES (#{num})"
154+
end
155+
156+
test "dumps structure and schema_migration records from multiple prefixes" do
157+
# Create the test_schema schema
158+
create_database()
159+
prefix = params()[:database]
160+
161+
# Run migrations
162+
version = @base_migration + System.unique_integer([:positive])
163+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false)
164+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false, prefix: prefix)
165+
166+
config = Keyword.put(TestRepo.config(), :dump_prefixes, ["ecto_test", prefix])
167+
{:ok, path} = Ecto.Adapters.MyXQL.structure_dump(tmp_path(), config)
168+
contents = File.read!(path)
169+
170+
assert contents =~ "Current Database: `#{prefix}`"
171+
assert contents =~ "Current Database: `ecto_test`"
172+
assert contents =~ "CREATE TABLE `schema_migrations`"
173+
assert contents =~ ~s[INSERT INTO `#{prefix}`.`schema_migrations` (version) VALUES (#{version})]
174+
assert contents =~ ~s[INSERT INTO `ecto_test`.`schema_migrations` (version) VALUES (#{version})]
175+
after
176+
drop_database()
177+
end
178+
179+
test "dumps structure and schema_migration records only from queried prefix" do
180+
# Create the test_schema schema
181+
create_database()
182+
prefix = params()[:database]
183+
184+
# Run migrations
185+
version = @base_migration + System.unique_integer([:positive])
186+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false)
187+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false, prefix: prefix)
188+
189+
config = Keyword.put(TestRepo.config(), :dump_prefixes, ["ecto_test"])
190+
{:ok, path} = Ecto.Adapters.MyXQL.structure_dump(tmp_path(), config)
191+
contents = File.read!(path)
192+
193+
refute contents =~ "Current Database: `#{prefix}`"
194+
assert contents =~ "Current Database: `ecto_test`"
195+
assert contents =~ "CREATE TABLE `schema_migrations`"
196+
refute contents =~ ~s[INSERT INTO `#{prefix}`.`schema_migrations` (version) VALUES (#{version})]
197+
assert contents =~ ~s[INSERT INTO `ecto_test`.`schema_migrations` (version) VALUES (#{version})]
198+
after
199+
drop_database()
154200
end
155201

156202
defp strip_timestamp(dump) do

integration_test/pg/storage_test.exs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ defmodule Ecto.Integration.StorageTest do
5959
System.cmd("psql", args, env: env)
6060
end
6161

62+
def create_schema(database, schema) do
63+
run_psql(~s[CREATE SCHEMA #{schema}], [database])
64+
end
65+
66+
def drop_schema(database, schema) do
67+
run_psql(~s[DROP SCHEMA #{schema}], [database])
68+
end
69+
70+
def drop_schema_migrations_table(database, schema) do
71+
run_psql(~s[DROP TABLE #{schema}.schema_migrations], [database])
72+
end
73+
6274
test "storage up (twice in a row)" do
6375
assert Postgres.storage_up(params()) == :ok
6476
assert Postgres.storage_up(params()) == {:error, :already_up}
@@ -147,6 +159,72 @@ defmodule Ecto.Integration.StorageTest do
147159
assert contents =~ ~s[INSERT INTO public."schema_migrations" (version) VALUES]
148160
end
149161

162+
test "when :dump_prefixes is not provided, structure is dumped for all schemas but only public schema migration records are inserted" do
163+
# Create the test_schema schema
164+
create_schema(PoolRepo.config()[:database], "test_schema")
165+
166+
# Run migrations
167+
version = @base_migration + System.unique_integer([:positive])
168+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false)
169+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false, prefix: "test_schema")
170+
171+
{:ok, path} = Postgres.structure_dump(tmp_path(), TestRepo.config())
172+
contents = File.read!(path)
173+
174+
assert contents =~ "CREATE TABLE public.schema_migrations"
175+
assert contents =~ ~s[INSERT INTO public."schema_migrations" (version) VALUES (#{version})]
176+
assert contents =~ "CREATE TABLE test_schema.schema_migrations"
177+
refute contents =~ ~s[INSERT INTO test_schema."schema_migrations" (version) VALUES (#{version})]
178+
after
179+
drop_schema_migrations_table(PoolRepo.config()[:database], "test_schema")
180+
drop_schema(PoolRepo.config()[:database], "test_schema")
181+
end
182+
183+
test "dumps structure and schema_migration records from multiple schemas" do
184+
# Create the test_schema schema
185+
create_schema(PoolRepo.config()[:database], "test_schema")
186+
187+
# Run migrations
188+
version = @base_migration + System.unique_integer([:positive])
189+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false)
190+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false, prefix: "test_schema")
191+
192+
config = Keyword.put(TestRepo.config(), :dump_prefixes, ["public", "test_schema"])
193+
{:ok, path} = Postgres.structure_dump(tmp_path(), config)
194+
contents = File.read!(path)
195+
196+
assert contents =~ "CREATE TABLE public.schema_migrations"
197+
assert contents =~ ~s[INSERT INTO public."schema_migrations" (version) VALUES (#{version})]
198+
assert contents =~ "CREATE TABLE test_schema.schema_migrations"
199+
assert contents =~ ~s[INSERT INTO test_schema."schema_migrations" (version) VALUES (#{version})]
200+
after
201+
drop_schema_migrations_table(PoolRepo.config()[:database], "test_schema")
202+
drop_schema(PoolRepo.config()[:database], "test_schema")
203+
end
204+
205+
test "dumps structure and schema_migration records only from queried schema" do
206+
# Create the test_schema schema
207+
create_schema(PoolRepo.config()[:database], "test_schema")
208+
209+
# Run migrations
210+
version = @base_migration + System.unique_integer([:positive])
211+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false)
212+
:ok = Ecto.Migrator.up(PoolRepo, version, Migration, log: false, prefix: "test_schema")
213+
214+
config = Keyword.put(TestRepo.config(), :dump_prefixes, ["test_schema"])
215+
{:ok, path} = Postgres.structure_dump(tmp_path(), config)
216+
contents = File.read!(path)
217+
218+
refute contents =~ "CREATE TABLE public.schema_migrations"
219+
refute contents =~ ~s[INSERT INTO public."schema_migrations" (version) VALUES (#{version})]
220+
assert contents =~ "CREATE TABLE test_schema.schema_migrations"
221+
assert contents =~ ~s[INSERT INTO test_schema."schema_migrations" (version) VALUES (#{version})]
222+
after
223+
drop_schema_migrations_table(PoolRepo.config()[:database], "test_schema")
224+
drop_schema(PoolRepo.config()[:database], "test_schema")
225+
end
226+
227+
150228
test "storage status is up when database is created" do
151229
create_database()
152230
assert :up == Postgres.storage_status(params())

lib/ecto/adapters/myxql.ex

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ defmodule Ecto.Adapters.MyXQL do
5050
* `:charset` - the database encoding (default: "utf8mb4")
5151
* `:collation` - the collation order
5252
* `:dump_path` - where to place dumped structures
53+
* `:dump_prefixes` - list of prefixes that will be included in the
54+
structure dump. When specified, the prefixes will have their definitions
55+
dumped along with the data in their migration table. When it is not
56+
specified, only the configured database and its migration table are dumped.
5357
5458
### After connect callback
5559
@@ -312,27 +316,38 @@ defmodule Ecto.Adapters.MyXQL do
312316
def structure_dump(default, config) do
313317
table = config[:migration_source] || "schema_migrations"
314318
path = config[:dump_path] || Path.join(default, "structure.sql")
319+
prefixes = config[:dump_prefixes] || [config[:database]]
315320

316-
with {:ok, versions} <- select_versions(table, config),
317-
{:ok, contents} <- mysql_dump(config),
321+
with {:ok, versions} <- select_versions(prefixes, table, config),
322+
{:ok, contents} <- mysql_dump(prefixes, config),
318323
{:ok, contents} <- append_versions(table, versions, contents) do
319324
File.mkdir_p!(Path.dirname(path))
320325
File.write!(path, contents)
321326
{:ok, path}
322327
end
323328
end
324329

325-
defp select_versions(table, config) do
326-
case run_query(~s[SELECT version FROM `#{table}` ORDER BY version], config) do
327-
{:ok, %{rows: rows}} -> {:ok, Enum.map(rows, &hd/1)}
328-
{:error, %{mysql: %{name: :ER_NO_SUCH_TABLE}}} -> {:ok, []}
330+
defp select_versions(prefixes, table, config) do
331+
result =
332+
Enum.reduce_while(prefixes, [], fn prefix, versions ->
333+
case run_query(~s[SELECT version FROM `#{prefix}`.`#{table}` ORDER BY version], config) do
334+
{:ok, %{rows: rows}} -> {:cont, Enum.map(rows, &{prefix, hd(&1)}) ++ versions}
335+
{:error, %{mysql: %{name: :ER_NO_SUCH_TABLE}}} -> {:cont, versions}
336+
{:error, _} = error -> {:halt, error}
337+
{:exit, exit} -> {:halt, {:error, exit_to_exception(exit)}}
338+
end
339+
end)
340+
341+
case result do
329342
{:error, _} = error -> error
330-
{:exit, exit} -> {:error, exit_to_exception(exit)}
343+
versions -> {:ok, versions}
331344
end
332345
end
333346

334-
defp mysql_dump(config) do
335-
case run_with_cmd("mysqldump", config, ["--no-data", "--routines", config[:database]]) do
347+
defp mysql_dump(prefixes, config) do
348+
args = ["--no-data", "--routines", "--databases" | prefixes]
349+
350+
case run_with_cmd("mysqldump", config, args) do
336351
{output, 0} -> {:ok, output}
337352
{output, _} -> {:error, output}
338353
end
@@ -341,10 +356,14 @@ defmodule Ecto.Adapters.MyXQL do
341356
defp append_versions(_table, [], contents) do
342357
{:ok, contents}
343358
end
359+
344360
defp append_versions(table, versions, contents) do
345-
{:ok,
346-
contents <>
347-
Enum.map_join(versions, &~s[INSERT INTO `#{table}` (version) VALUES (#{&1});\n])}
361+
sql_statements =
362+
Enum.map_join(versions, fn {prefix, version} ->
363+
~s[INSERT INTO `#{prefix}`.`#{table}` (version) VALUES (#{version});\n]
364+
end)
365+
366+
{:ok, contents <> sql_statements}
348367
end
349368

350369
@impl true

lib/ecto/adapters/postgres.ex

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ defmodule Ecto.Adapters.Postgres do
9393
* `:lc_collate` - the collation order
9494
* `:lc_ctype` - the character classification
9595
* `:dump_path` - where to place dumped structures
96+
* `dump_prefixes` - list of prefixes that will be included in the structure dump.
97+
When specified, the prefixes will have their definitions dumped along with the
98+
data in their migration table. When it is not specified, the configured
99+
database has the definitions dumped from all of its schemas but only
100+
the data from the migration table from the `public` schema is included.
96101
* `:force_drop` - force the database to be dropped even
97102
if it has connections to it (requires PostgreSQL 13+)
98103
@@ -344,18 +349,36 @@ defmodule Ecto.Adapters.Postgres do
344349
end
345350

346351
defp select_versions(table, config) do
347-
case run_query(~s[SELECT version FROM public."#{table}" ORDER BY version], config) do
348-
{:ok, %{rows: rows}} -> {:ok, Enum.map(rows, &hd/1)}
349-
{:error, %{postgres: %{code: :undefined_table}}} -> {:ok, []}
352+
prefixes = config[:dump_prefixes] || ["public"]
353+
354+
result =
355+
Enum.reduce_while(prefixes, [], fn prefix, versions ->
356+
case run_query(~s[SELECT version FROM #{prefix}."#{table}" ORDER BY version], config) do
357+
{:ok, %{rows: rows}} -> {:cont, Enum.map(rows, &{prefix, hd(&1)}) ++ versions }
358+
{:error, %{postgres: %{code: :undefined_table}}} -> {:cont, versions}
359+
{:error, _} = error -> {:halt, error}
360+
end
361+
end)
362+
363+
case result do
350364
{:error, _} = error -> error
365+
versions -> {:ok, versions}
351366
end
352367
end
353368

354369
defp pg_dump(default, config) do
355370
path = config[:dump_path] || Path.join(default, "structure.sql")
371+
prefixes = config[:dump_prefixes] || []
372+
non_prefix_args = ["--file", path, "--schema-only", "--no-acl", "--no-owner"]
373+
374+
args =
375+
Enum.reduce(prefixes, non_prefix_args, fn prefix, acc ->
376+
["-n", prefix | acc]
377+
end)
378+
356379
File.mkdir_p!(Path.dirname(path))
357380

358-
case run_with_cmd("pg_dump", config, ["--file", path, "--schema-only", "--no-acl", "--no-owner"]) do
381+
case run_with_cmd("pg_dump", config, args) do
359382
{_output, 0} ->
360383
{:ok, path}
361384
{output, _} ->
@@ -368,7 +391,10 @@ defmodule Ecto.Adapters.Postgres do
368391
end
369392

370393
defp append_versions(table, versions, path) do
371-
sql = Enum.map_join(versions, &~s[INSERT INTO public."#{table}" (version) VALUES (#{&1});\n])
394+
sql =
395+
Enum.map_join(versions, fn {prefix, version} ->
396+
~s[INSERT INTO #{prefix}."#{table}" (version) VALUES (#{version});\n]
397+
end)
372398

373399
File.open!(path, [:append], fn file ->
374400
IO.write(file, sql)

lib/mix/tasks/ecto.dump.ex

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ defmodule Mix.Tasks.Ecto.Dump do
1717
quiet: :boolean,
1818
repo: [:string, :keep],
1919
no_compile: :boolean,
20-
no_deps_check: :boolean
20+
no_deps_check: :boolean,
21+
prefix: [:string, :keep]
2122
]
2223

2324
@moduledoc """
@@ -46,12 +47,30 @@ defmodule Mix.Tasks.Ecto.Dump do
4647
* `-q`, `--quiet` - run the command quietly
4748
* `--no-compile` - does not compile applications before dumping
4849
* `--no-deps-check` - does not check dependencies before dumping
50+
* `--prefix` - prefix that will be included in the structure dump.
51+
Can include multiple prefixes (ex. --prefix foo --prefix bar).
52+
When specified, the prefixes will have their definitions dumped along
53+
with the data in their migration table. The default behavior is
54+
dependent on the adapter for backwards compatibility reasons.
55+
For PostgreSQL, the configured database has the definitions dumped
56+
from all of its schemas but only the data from the migration table
57+
from the `public` schema is included. For MySQL, only the configured
58+
database and its migration table are dumped.
4959
"""
5060

5161
@impl true
5262
def run(args) do
5363
{opts, _} = OptionParser.parse! args, strict: @switches, aliases: @aliases
54-
opts = Keyword.merge(@default_opts, opts)
64+
65+
dump_prefixes =
66+
case Keyword.get_values(opts, :prefix) do
67+
[_ | _] = prefixes -> prefixes
68+
[] -> nil
69+
end
70+
71+
opts = @default_opts
72+
|> Keyword.merge(opts)
73+
|> Keyword.put(:dump_prefixes, dump_prefixes)
5574

5675
Enum.each parse_repo(args), fn repo ->
5776
ensure_repo(repo, args)

test/ecto/adapters/postgres_test.exs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,18 @@ defmodule Ecto.Adapters.PostgresTest do
5555
defp delete_all(query), do: query |> SQL.delete_all |> IO.iodata_to_binary()
5656
defp execute_ddl(query), do: query |> SQL.execute_ddl |> Enum.map(&IO.iodata_to_binary/1)
5757

58-
defp insert(prefx, table, header, rows, on_conflict, returning, placeholders \\ []) do
58+
defp insert(prefix, table, header, rows, on_conflict, returning, placeholders \\ []) do
5959
IO.iodata_to_binary(
60-
SQL.insert(prefx, table, header, rows, on_conflict, returning, placeholders)
60+
SQL.insert(prefix, table, header, rows, on_conflict, returning, placeholders)
6161
)
6262
end
6363

64-
defp update(prefx, table, fields, filter, returning) do
65-
IO.iodata_to_binary(SQL.update(prefx, table, fields, filter, returning))
64+
defp update(prefix, table, fields, filter, returning) do
65+
IO.iodata_to_binary(SQL.update(prefix, table, fields, filter, returning))
6666
end
6767

68-
defp delete(prefx, table, filter, returning) do
69-
IO.iodata_to_binary(SQL.delete(prefx, table, filter, returning))
68+
defp delete(prefix, table, filter, returning) do
69+
IO.iodata_to_binary(SQL.delete(prefix, table, filter, returning))
7070
end
7171

7272
test "from" do

0 commit comments

Comments
 (0)