Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 104 additions & 31 deletions lib/migration_generator/migration_generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ defmodule AshPostgres.MigrationGenerator do
|> group_into_phases()
|> comment_out_phases()
|> build_up_and_down()
|> write_migration(snapshots, repo, opts, tenant?)
|> write_migration!(snapshots, repo, opts, tenant?)
end
end)
end
Expand Down Expand Up @@ -302,30 +302,9 @@ defmodule AshPostgres.MigrationGenerator do
|> Enum.sort()
end

defp write_migration({up, down}, snapshots, repo, opts, tenant?) do
defp write_migration!({up, down}, snapshots, repo, opts, tenant?) do
repo_name = repo |> Module.split() |> List.last() |> Macro.underscore()

unless opts.dry_run do
Enum.each(snapshots, fn snapshot ->
snapshot_binary = snapshot_to_binary(snapshot)

snapshot_file =
if tenant? do
opts.snapshot_path
|> Path.join(repo_name)
|> Path.join("tenants")
|> Path.join(snapshot.table <> ".json")
else
opts.snapshot_path
|> Path.join(repo_name)
|> Path.join(snapshot.table <> ".json")
end

File.mkdir_p(Path.dirname(snapshot_file))
File.write!(snapshot_file, snapshot_binary, [])
end)
end

migration_path =
if tenant? do
if opts.tenant_migration_path do
Expand Down Expand Up @@ -421,10 +400,73 @@ defmodule AshPostgres.MigrationGenerator do
end
"""

if opts.dry_run do
Mix.shell().info(format(contents, opts))
else
create_file(migration_file, format(contents, opts))
try do
contents = format(contents, opts)

create_new_snapshot(snapshots, repo_name, opts, tenant?)

if opts.dry_run do
Mix.shell().info(contents)
else
create_file(migration_file, contents)
end
rescue
exception ->
reraise(
"""
Exception while formatting generated code:
#{Exception.format(:error, exception, __STACKTRACE__)}

Code:

#{add_line_numbers(contents)}

To generate it unformatted anyway, but manually fix it, use the `--no-format` option.
""",
__STACKTRACE__
)
end
end

defp add_line_numbers(contents) do
lines = String.split(contents, "\n")

digits = String.length(to_string(Enum.count(lines)))

lines
|> Enum.with_index()
|> Enum.map_join("\n", fn {line, index} ->
"#{String.pad_trailing(to_string(index), digits, " ")} | #{line}"
end)
end

defp create_new_snapshot(snapshots, repo_name, opts, tenant?) do
unless opts.dry_run do
Enum.each(snapshots, fn snapshot ->
snapshot_binary = snapshot_to_binary(snapshot)

snapshot_folder =
if tenant? do
opts.snapshot_path
|> Path.join(repo_name)
|> Path.join("tenants")
else
opts.snapshot_path
|> Path.join(repo_name)
end

snapshot_file = Path.join(snapshot_folder, "#{snapshot.table}/#{timestamp()}.json")

File.mkdir_p(Path.dirname(snapshot_file))
File.write!(snapshot_file, snapshot_binary, [])

old_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}.json")

if File.exists?(old_snapshot_folder) do
new_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}/initial.json")
File.rename(old_snapshot_folder, new_snapshot_folder)
end
end)
end
end

Expand Down Expand Up @@ -934,13 +976,42 @@ defmodule AshPostgres.MigrationGenerator do
|> Path.join(repo_name)
|> Path.join("tenants")
else
Path.join(opts.snapshot_path, repo_name)
opts.snapshot_path
|> Path.join(repo_name)
end

file = Path.join(folder, snapshot.table <> ".json")
snapshot_folder = Path.join(folder, snapshot.table)

if File.exists?(snapshot_folder) do
snapshot_folder
|> File.ls!()
|> Enum.filter(&String.ends_with?(&1, ".json"))
|> Enum.map(&String.trim_trailing(&1, ".json"))
|> Enum.map(&Integer.parse/1)
|> Enum.filter(fn {_int, remaining} -> remaining == "" end)
|> Enum.map(&elem(&1, 0))
|> case do
[] ->
get_old_snapshot(folder, snapshot)

timestamps ->
timestamp = Enum.max(timestamps)
snapshot_file = Path.join(snapshot_folder, "#{timestamp}.json")

snapshot_file
|> File.read!()
|> load_snapshot()
end
else
get_old_snapshot(folder, snapshot)
end
end

if File.exists?(file) do
file
defp get_old_snapshot(folder, snapshot) do
old_snapshot_file = Path.join(folder, "#{snapshot.table}.json")
# This is adapter code for the old version, where migrations were stored in a flat directory
if File.exists?(old_snapshot_file) do
old_snapshot_file
|> File.read!()
|> load_snapshot()
end
Expand Down Expand Up @@ -1175,6 +1246,8 @@ defmodule AshPostgres.MigrationGenerator do
attribute
|> Map.update!(:type, &String.to_atom/1)
|> Map.update!(:name, &String.to_atom/1)
|> Map.put_new(:default, "nil")
|> Map.update!(:default, &(&1 || "nil"))
|> Map.update!(:references, fn
nil ->
nil
Expand Down
7 changes: 7 additions & 0 deletions lib/mix/tasks/ash_postgres.generate_migrations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do
* `no_format` - files that are created will not be formatted with the code formatter
* `dry_run` - no files are created, instead the new migration is printed

#### Snapshots

Snapshots are stored in a folder for each table that migrations are generated for. Each snapshot is
stored in a file with a timestamp of when it was generated.
This is important because it allows for simultaneous work to be done on separate branches, and for rolling back
changes more easily, e.g removing a generated migration, and deleting the most recent snapshot, without having to redo
all of it

#### Dropping columns

Expand Down
6 changes: 3 additions & 3 deletions test/migration_generator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@ defmodule AshPostgres.MigrationGeneratorTest do
end

test "it creates a snapshot for each resource" do
assert File.exists?(Path.join(["test_snapshots_path", "test_repo", "posts.json"]))
assert File.exists?(Path.wildcard("test_snapshots_path/test_repo/posts/*.json"))
end

test "the snapshots can be loaded" do
assert File.exists?(Path.join(["test_snapshots_path", "test_repo", "posts.json"]))
assert File.exists?(Path.wildcard("test_snapshots_path/test_repo/posts/*.json"))
end

test "the snapshots contain valid json" do
assert File.read!(Path.join(["test_snapshots_path", "test_repo", "posts.json"]))
assert File.read!(Path.wildcard("test_snapshots_path/test_repo/posts/*.json"))
|> Jason.decode!(keys: :atoms!)
end

Expand Down