mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-20 13:33:50 +12:00
chore: open up some migration APIs for experimentation purposes
This commit is contained in:
parent
cf92fe14a5
commit
2d0245e368
2 changed files with 129 additions and 46 deletions
|
@ -14,6 +14,9 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
migration_path: nil,
|
migration_path: nil,
|
||||||
tenant_migration_path: nil,
|
tenant_migration_path: nil,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
|
current_snapshots: nil,
|
||||||
|
answers: [],
|
||||||
|
no_shell?: false,
|
||||||
format: true,
|
format: true,
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
check_generated: false,
|
check_generated: false,
|
||||||
|
@ -43,7 +46,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
tenant_snapshots_to_include_in_global =
|
tenant_snapshots_to_include_in_global =
|
||||||
tenant_snapshots
|
tenant_snapshots
|
||||||
|> Enum.filter(& &1.multitenancy.global)
|
|> Enum.filter(& &1.multitenancy.global)
|
||||||
|> Enum.map(&Map.put(&1, :multitenancy, %{strategy: nil, attribute: nil, global: false}))
|
|> Enum.map(&Map.put(&1, :multitenancy, %{strategy: nil, attribute: nil, global: nil}))
|
||||||
|
|
||||||
snapshots = snapshots ++ tenant_snapshots_to_include_in_global
|
snapshots = snapshots ++ tenant_snapshots_to_include_in_global
|
||||||
|
|
||||||
|
@ -57,6 +60,48 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
create_migrations(snapshots, opts, false)
|
create_migrations(snapshots, opts, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
A work in progress utility for getting snapshots.
|
||||||
|
|
||||||
|
Does not support everything supported by the migration generator.
|
||||||
|
"""
|
||||||
|
def take_snapshots(api, repo) do
|
||||||
|
all_resources = Ash.Api.resources(api)
|
||||||
|
|
||||||
|
all_resources
|
||||||
|
|> Enum.filter(&(Ash.DataLayer.data_layer(&1) == AshPostgres.DataLayer))
|
||||||
|
|> Enum.filter(&(AshPostgres.repo(&1) == repo))
|
||||||
|
|> Enum.flat_map(&get_snapshots(&1, all_resources))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
A work in progress utility for getting operations between snapshots.
|
||||||
|
|
||||||
|
Does not support everything supported by the migration generator.
|
||||||
|
"""
|
||||||
|
def get_operations_from_snapshots(old_snapshots, new_snapshots, opts \\ []) do
|
||||||
|
opts = %{opts(opts) | no_shell?: true}
|
||||||
|
|
||||||
|
old_snapshots = Enum.map(old_snapshots, &sanitize_snapshot/1)
|
||||||
|
|
||||||
|
new_snapshots
|
||||||
|
|> deduplicate_snapshots(opts, old_snapshots)
|
||||||
|
|> fetch_operations(opts)
|
||||||
|
|> Enum.flat_map(&elem(&1, 1))
|
||||||
|
|> Enum.uniq()
|
||||||
|
|> organize_operations()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp opts(opts) do
|
||||||
|
case struct(__MODULE__, opts) do
|
||||||
|
%{check_generated: true} = opts ->
|
||||||
|
%{opts | dry_run: true}
|
||||||
|
|
||||||
|
opts ->
|
||||||
|
opts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp create_extension_migrations(repos, opts) do
|
defp create_extension_migrations(repos, opts) do
|
||||||
for repo <- repos do
|
for repo <- repos do
|
||||||
snapshot_file = Path.join(opts.snapshot_path, "extensions.json")
|
snapshot_file = Path.join(opts.snapshot_path, "extensions.json")
|
||||||
|
@ -165,16 +210,23 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
if opts.check_generated, do: exit({:shutdown, 1})
|
if opts.check_generated, do: exit({:shutdown, 1})
|
||||||
|
|
||||||
operations
|
operations
|
||||||
|> sort_operations()
|
|> organize_operations
|
||||||
|> streamline()
|
|
||||||
|> group_into_phases()
|
|
||||||
|> comment_out_phases()
|
|
||||||
|> build_up_and_down()
|
|> build_up_and_down()
|
||||||
|> write_migration!(snapshots, repo, opts, tenant?)
|
|> write_migration!(snapshots, repo, opts, tenant?)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp organize_operations([]), do: []
|
||||||
|
|
||||||
|
defp organize_operations(operations) do
|
||||||
|
operations
|
||||||
|
|> sort_operations()
|
||||||
|
|> streamline()
|
||||||
|
|> group_into_phases()
|
||||||
|
|> comment_out_phases()
|
||||||
|
end
|
||||||
|
|
||||||
defp comment_out_phases(phases) do
|
defp comment_out_phases(phases) do
|
||||||
Enum.map(phases, fn
|
Enum.map(phases, fn
|
||||||
%{operations: operations} = phase ->
|
%{operations: operations} = phase ->
|
||||||
|
@ -189,14 +241,20 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp deduplicate_snapshots(snapshots, opts) do
|
defp deduplicate_snapshots(snapshots, opts, existing_snapshots \\ []) do
|
||||||
snapshots
|
snapshots
|
||||||
|> Enum.group_by(fn snapshot ->
|
|> Enum.group_by(fn snapshot ->
|
||||||
snapshot.table
|
snapshot.table
|
||||||
end)
|
end)
|
||||||
|> Enum.map(fn {_table, [snapshot | _] = snapshots} ->
|
|> Enum.map(fn {_table, [snapshot | _] = snapshots} ->
|
||||||
existing_snapshot = get_existing_snapshot(snapshot, opts)
|
existing_snapshot =
|
||||||
{primary_key, identities} = merge_primary_keys(existing_snapshot, snapshots)
|
if opts.no_shell? do
|
||||||
|
Enum.find(existing_snapshots, &(&1.table == snapshot.table))
|
||||||
|
else
|
||||||
|
get_existing_snapshot(snapshot, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
{primary_key, identities} = merge_primary_keys(existing_snapshot, snapshots, opts)
|
||||||
|
|
||||||
attributes = Enum.flat_map(snapshots, & &1.attributes)
|
attributes = Enum.flat_map(snapshots, & &1.attributes)
|
||||||
|
|
||||||
|
@ -336,7 +394,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp merge_primary_keys(nil, [snapshot | _] = snapshots) do
|
defp merge_primary_keys(nil, [snapshot | _] = snapshots, opts) do
|
||||||
snapshots
|
snapshots
|
||||||
|> Enum.map(&pkey_names(&1.attributes))
|
|> Enum.map(&pkey_names(&1.attributes))
|
||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
|
@ -352,16 +410,20 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
"#{index}: #{inspect(pkey)}"
|
"#{index}: #{inspect(pkey)}"
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
choice =
|
||||||
|
if opts.no_shell? do
|
||||||
|
raise "Unimplemented: cannot resolve primary key ambiguity without shell input"
|
||||||
|
else
|
||||||
message = """
|
message = """
|
||||||
Which primary key should be used for the table `#{snapshot.table}` (enter the number)?
|
Which primary key should be used for the table `#{snapshot.table}` (enter the number)?
|
||||||
|
|
||||||
#{unique_primary_key_names}
|
#{unique_primary_key_names}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
choice =
|
|
||||||
message
|
message
|
||||||
|> Mix.shell().prompt()
|
|> Mix.shell().prompt()
|
||||||
|> String.to_integer()
|
|> String.to_integer()
|
||||||
|
end
|
||||||
|
|
||||||
identities =
|
identities =
|
||||||
unique_primary_keys
|
unique_primary_keys
|
||||||
|
@ -387,7 +449,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp merge_primary_keys(existing_snapshot, snapshots) do
|
defp merge_primary_keys(existing_snapshot, snapshots, opts) do
|
||||||
pkey_names = pkey_names(existing_snapshot.attributes)
|
pkey_names = pkey_names(existing_snapshot.attributes)
|
||||||
|
|
||||||
one_pkey_exists? =
|
one_pkey_exists? =
|
||||||
|
@ -413,7 +475,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
|
|
||||||
{pkey_names, identities}
|
{pkey_names, identities}
|
||||||
else
|
else
|
||||||
merge_primary_keys(nil, snapshots)
|
merge_primary_keys(nil, snapshots, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -565,7 +627,8 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_up_and_down(phases) do
|
@doc false
|
||||||
|
def build_up_and_down(phases) do
|
||||||
up =
|
up =
|
||||||
Enum.map_join(phases, "\n", fn phase ->
|
Enum.map_join(phases, "\n", fn phase ->
|
||||||
phase
|
phase
|
||||||
|
@ -924,7 +987,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
multitenancy: %{
|
multitenancy: %{
|
||||||
attribute: nil,
|
attribute: nil,
|
||||||
strategy: nil,
|
strategy: nil,
|
||||||
global: false
|
global: nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -994,7 +1057,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{attributes_to_add, attributes_to_remove, attributes_to_rename} =
|
{attributes_to_add, attributes_to_remove, attributes_to_rename} =
|
||||||
resolve_renames(snapshot.table, attributes_to_add, attributes_to_remove)
|
resolve_renames(snapshot.table, attributes_to_add, attributes_to_remove, opts)
|
||||||
|
|
||||||
attributes_to_alter =
|
attributes_to_alter =
|
||||||
snapshot.attributes
|
snapshot.attributes
|
||||||
|
@ -1130,7 +1193,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
|
|
||||||
snapshot_file
|
snapshot_file
|
||||||
|> File.read!()
|
|> File.read!()
|
||||||
|> load_snapshot(snapshot.table)
|
|> load_snapshot()
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
get_old_snapshot(folder, snapshot)
|
get_old_snapshot(folder, snapshot)
|
||||||
|
@ -1145,37 +1208,60 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
if File.exists?(old_snapshot_file) do
|
if File.exists?(old_snapshot_file) do
|
||||||
old_snapshot_file
|
old_snapshot_file
|
||||||
|> File.read!()
|
|> File.read!()
|
||||||
|> load_snapshot(snapshot.table)
|
|> load_snapshot()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp resolve_renames(_table, adding, []), do: {adding, [], []}
|
defp resolve_renames(_table, adding, [], _opts), do: {adding, [], []}
|
||||||
|
|
||||||
defp resolve_renames(_table, [], removing), do: {[], removing, []}
|
defp resolve_renames(_table, [], removing, _opts), do: {[], removing, []}
|
||||||
|
|
||||||
defp resolve_renames(table, [adding], [removing]) do
|
defp resolve_renames(table, [adding], [removing], opts) do
|
||||||
if Mix.shell().yes?("Are you renaming #{table}.#{removing.name} to #{table}.#{adding.name}?") do
|
if renaming_to?(table, removing.name, adding.name, opts) do
|
||||||
{[], [], [{adding, removing}]}
|
{[], [], [{adding, removing}]}
|
||||||
else
|
else
|
||||||
{[adding], [removing], []}
|
{[adding], [removing], []}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp resolve_renames(table, adding, [removing | rest]) do
|
defp resolve_renames(table, adding, [removing | rest], opts) do
|
||||||
{new_adding, new_removing, new_renames} =
|
{new_adding, new_removing, new_renames} =
|
||||||
if Mix.shell().yes?("Are you renaming #{table}.#{removing.name}?") do
|
if renaming?(table, removing, opts) do
|
||||||
new_attribute = get_new_attribute(adding)
|
new_attribute =
|
||||||
|
if opts.no_shell? do
|
||||||
|
raise "Unimplemented: Cannot get new_attribute without the shell!"
|
||||||
|
else
|
||||||
|
get_new_attribute(adding)
|
||||||
|
end
|
||||||
|
|
||||||
{adding -- [new_attribute], [], [{new_attribute, removing}]}
|
{adding -- [new_attribute], [], [{new_attribute, removing}]}
|
||||||
else
|
else
|
||||||
{adding, [removing], []}
|
{adding, [removing], []}
|
||||||
end
|
end
|
||||||
|
|
||||||
{rest_adding, rest_removing, rest_renames} = resolve_renames(table, new_adding, rest)
|
{rest_adding, rest_removing, rest_renames} = resolve_renames(table, new_adding, rest, opts)
|
||||||
|
|
||||||
{new_adding ++ rest_adding, new_removing ++ rest_removing, rest_renames ++ new_renames}
|
{new_adding ++ rest_adding, new_removing ++ rest_removing, rest_renames ++ new_renames}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp renaming_to?(table, removing, adding, opts) do
|
||||||
|
if opts.no_shell? do
|
||||||
|
raise "Unimplemented: cannot determine: Are you renaming #{table}.#{removing} to #{table}.#{
|
||||||
|
adding
|
||||||
|
}? without shell input"
|
||||||
|
else
|
||||||
|
Mix.shell().yes?("Are you renaming #{table}.#{removing} to #{table}.#{adding}?")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp renaming?(table, removing, opts) do
|
||||||
|
if opts.no_shell? do
|
||||||
|
raise "Unimplemented: cannot determine: Are you renaming #{table}.#{removing.name}? without shell input"
|
||||||
|
else
|
||||||
|
Mix.shell().yes?("Are you renaming #{table}.#{removing.name}?")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp get_new_attribute(adding, tries \\ 3)
|
defp get_new_attribute(adding, tries \\ 3)
|
||||||
|
|
||||||
defp get_new_attribute(_adding, 0) do
|
defp get_new_attribute(_adding, 0) do
|
||||||
|
@ -1421,28 +1507,33 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sanitize_type({:array, type}) do
|
defp sanitize_type({:array, type}) do
|
||||||
["array", type]
|
["array", sanitize_type(type)]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sanitize_type(type) do
|
defp sanitize_type(type) do
|
||||||
type
|
type
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_snapshot(json, table) do
|
defp load_snapshot(json) do
|
||||||
json
|
json
|
||||||
|> Jason.decode!(keys: :atoms!)
|
|> Jason.decode!(keys: :atoms!)
|
||||||
|
|> sanitize_snapshot()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sanitize_snapshot(snapshot) do
|
||||||
|
snapshot
|
||||||
|> Map.put_new(:has_create_action, true)
|
|> Map.put_new(:has_create_action, true)
|
||||||
|> Map.update!(:identities, fn identities ->
|
|> Map.update!(:identities, fn identities ->
|
||||||
Enum.map(identities, &load_identity/1)
|
Enum.map(identities, &load_identity/1)
|
||||||
end)
|
end)
|
||||||
|> Map.update!(:attributes, fn attributes ->
|
|> Map.update!(:attributes, fn attributes ->
|
||||||
Enum.map(attributes, &load_attribute(&1, table))
|
Enum.map(attributes, &load_attribute(&1, snapshot.table))
|
||||||
end)
|
end)
|
||||||
|> Map.update!(:repo, &String.to_atom/1)
|
|> Map.update!(:repo, &String.to_atom/1)
|
||||||
|> Map.put_new(:multitenancy, %{
|
|> Map.put_new(:multitenancy, %{
|
||||||
attribute: nil,
|
attribute: nil,
|
||||||
strategy: nil,
|
strategy: nil,
|
||||||
global: false
|
global: nil
|
||||||
})
|
})
|
||||||
|> Map.update!(:multitenancy, &load_multitenancy/1)
|
|> Map.update!(:multitenancy, &load_multitenancy/1)
|
||||||
end
|
end
|
||||||
|
@ -1472,7 +1563,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
|> Map.put_new(:multitenancy, %{
|
|> Map.put_new(:multitenancy, %{
|
||||||
attribute: nil,
|
attribute: nil,
|
||||||
strategy: nil,
|
strategy: nil,
|
||||||
global: false
|
global: nil
|
||||||
})
|
})
|
||||||
|> Map.update!(:multitenancy, &load_multitenancy/1)
|
|> Map.update!(:multitenancy, &load_multitenancy/1)
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -114,21 +114,13 @@ defmodule AshPostgres.MigrationGenerator.Operation do
|
||||||
|
|
||||||
def up(%{
|
def up(%{
|
||||||
multitenancy: %{strategy: :attribute},
|
multitenancy: %{strategy: :attribute},
|
||||||
table: current_table,
|
|
||||||
attribute:
|
attribute:
|
||||||
%{
|
%{
|
||||||
references: %{
|
references: %{
|
||||||
table: table,
|
|
||||||
multitenancy: %{strategy: :context}
|
multitenancy: %{strategy: :context}
|
||||||
}
|
}
|
||||||
} = attribute
|
} = attribute
|
||||||
}) do
|
}) do
|
||||||
Mix.shell().info("""
|
|
||||||
table `#{current_table}` with attribute multitenancy refers to table `#{table}` with schema based multitenancy.
|
|
||||||
This means that it is not possible to use a foreign key. This is not necessarily a problem, just something
|
|
||||||
you should be aware of
|
|
||||||
""")
|
|
||||||
|
|
||||||
[
|
[
|
||||||
"add #{inspect(attribute.name)}",
|
"add #{inspect(attribute.name)}",
|
||||||
inspect(attribute.type),
|
inspect(attribute.type),
|
||||||
|
|
Loading…
Reference in a new issue