From 965c81ae52221427535003b6c0c6f2a4acd9576f Mon Sep 17 00:00:00 2001 From: ken-kost Date: Sun, 2 Mar 2025 16:07:48 +0100 Subject: [PATCH 01/18] Add dev opt to generate migrations task --- lib/mix/tasks/ash_postgres.generate_migrations.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/ash_postgres.generate_migrations.ex b/lib/mix/tasks/ash_postgres.generate_migrations.ex index 065942c8..d83fc4df 100644 --- a/lib/mix/tasks/ash_postgres.generate_migrations.ex +++ b/lib/mix/tasks/ash_postgres.generate_migrations.ex @@ -21,6 +21,7 @@ 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 * `check` - no files are created, returns an exit(1) code if the current snapshots and resources don't fit + * 'dev' - dev files are created * `snapshots-only` - no migrations are generated, only snapshots are stored * `concurrent-indexes` - new identities will be run concurrently and in a separate migration (like concurrent custom indexes) @@ -97,6 +98,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do no_format: :boolean, dry_run: :boolean, check: :boolean, + dev: :boolean, dont_drop_columns: :boolean, concurrent_indexes: :boolean ] @@ -110,7 +112,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do |> Keyword.delete(:no_format) |> Keyword.put_new(:name, name) - if !opts[:name] && !opts[:dry_run] && !opts[:check] && !opts[:snapshots_only] do + if !opts[:name] && !opts[:dry_run] && !opts[:check] && !opts[:snapshots_only] && !opts[:dev] do IO.warn(""" Name must be provided when generating migrations, unless `--dry-run` or `--check` is also provided. Using an autogenerated name will be deprecated in a future release. From 933188c75bbe74d07ed5ee08ee966564a1abc7fc Mon Sep 17 00:00:00 2001 From: ken-kost Date: Sun, 2 Mar 2025 16:08:23 +0100 Subject: [PATCH 02/18] Handle dev flag in migration generator --- .../migration_generator.ex | 79 +++++++++++++++++-- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index ea395da9..2d88ad39 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -19,6 +19,7 @@ defmodule AshPostgres.MigrationGenerator do format: true, dry_run: false, check: false, + dev: false, snapshots_only: false, dont_drop_columns: false @@ -452,6 +453,20 @@ defmodule AshPostgres.MigrationGenerator do :ok operations -> + if !opts.dev and any_dev_migrations?(opts, repo) do + if opts.check do + Mix.shell().error(""" + Generated migrations are from dev mode. + + Generate migrations without `--dev` flag. + """) + + exit({:shutdown, 1}) + else + remove_dev_migrations_and_snapshots(opts, tenant?, repo, snapshots) + end + end + if opts.check do Mix.shell().error(""" Migrations would have been generated, but the --check flag was provided. @@ -491,6 +506,50 @@ defmodule AshPostgres.MigrationGenerator do end) end + defp any_dev_migrations?(opts, repo) do + opts + |> migration_path(repo) + |> File.ls!() + |> Enum.any?(&String.contains?(&1, "_dev.exs")) + end + + defp remove_dev_migrations_and_snapshots(opts, tenant?, repo, snapshots) do + opts + |> migration_path(repo) + |> File.ls!() + |> Enum.filter(&String.contains?(&1, "_dev.exs")) + |> Enum.each(fn migration_name -> + opts + |> migration_path(repo) + |> Path.join(migration_name) + |> File.rm!() + end) + + Enum.each(snapshots, fn snapshot -> + snapshot_folder = + if tenant? do + opts + |> snapshot_path(snapshot.repo) + |> Path.join(repo_name(repo)) + |> Path.join("tenants") + |> Path.join(snapshot.table) + else + opts + |> snapshot_path(snapshot.repo) + |> Path.join(repo_name(snapshot.repo)) + |> Path.join(snapshot.table) + end + + File.ls!(snapshot_folder) + |> Enum.filter(&String.contains?(&1, "_dev.json")) + |> Enum.each(fn snapshot_name -> + snapshot_folder + |> Path.join(snapshot_name) + |> File.rm!() + end) + end) + end + defp split_into_migrations(operations) do operations |> Enum.split_with(fn @@ -962,7 +1021,7 @@ defmodule AshPostgres.MigrationGenerator do migration_file = migration_path - |> Path.join(migration_name <> ".exs") + |> Path.join(migration_name <> "#{if opts.dev, do: "_dev"}.exs") module_name = if tenant? do @@ -1056,20 +1115,25 @@ defmodule AshPostgres.MigrationGenerator do |> Path.join(repo_name) end + dev = if opts.dev, do: "_dev" + snapshot_file = if snapshot.schema do - Path.join(snapshot_folder, "#{snapshot.schema}.#{snapshot.table}/#{timestamp()}.json") + Path.join( + snapshot_folder, + "#{snapshot.schema}.#{snapshot.table}/#{timestamp()}#{dev}.json" + ) else - Path.join(snapshot_folder, "#{snapshot.table}/#{timestamp()}.json") + Path.join(snapshot_folder, "#{snapshot.table}/#{timestamp()}#{dev}.json") end File.mkdir_p(Path.dirname(snapshot_file)) create_file(snapshot_file, snapshot_binary, force: true) - old_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}.json") + old_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}#{dev}.json") if File.exists?(old_snapshot_folder) do - new_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}/initial.json") + new_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}/initial#{dev}.json") File.rename(old_snapshot_folder, new_snapshot_folder) end end) @@ -2683,7 +2747,10 @@ defmodule AshPostgres.MigrationGenerator do if File.exists?(snapshot_folder) do snapshot_folder |> File.ls!() - |> Enum.filter(&String.match?(&1, ~r/^\d{14}\.json$/)) + |> Enum.filter( + &(String.match?(&1, ~r/^\d{14}\.json$/) or + (opts.dev and String.match?(&1, ~r/^\d{14}\_dev.json$/))) + ) |> case do [] -> get_old_snapshot(folder, snapshot) From 5ebbb16b474eb64ec3e210b56ee26c17f2d82746 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Mon, 3 Mar 2025 18:53:44 +0100 Subject: [PATCH 03/18] Expand warn message with dev option --- lib/mix/tasks/ash_postgres.generate_migrations.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/ash_postgres.generate_migrations.ex b/lib/mix/tasks/ash_postgres.generate_migrations.ex index d83fc4df..846c0333 100644 --- a/lib/mix/tasks/ash_postgres.generate_migrations.ex +++ b/lib/mix/tasks/ash_postgres.generate_migrations.ex @@ -114,7 +114,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do if !opts[:name] && !opts[:dry_run] && !opts[:check] && !opts[:snapshots_only] && !opts[:dev] do IO.warn(""" - Name must be provided when generating migrations, unless `--dry-run` or `--check` is also provided. + Name must be provided when generating migrations, unless `--dry-run` or `--check` or `--dev` is also provided. Using an autogenerated name will be deprecated in a future release. Please provide a name. for example: From 5036c4f6cb015555926a60778427f4111cfbf307 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Mon, 3 Mar 2025 18:54:05 +0100 Subject: [PATCH 04/18] Add test for dev option --- test/migration_generator_test.exs | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index a1168ccb..ebac37c0 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -1403,6 +1403,53 @@ defmodule AshPostgres.MigrationGeneratorTest do end end + describe "--dev option" do + setup do + on_exit(fn -> + File.rm_rf!("test_snapshots_path") + File.rm_rf!("test_migration_path") + end) + end + + test "generates dev migration" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:title, :string, public?: true) + end + end + + defdomain([Post]) + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + snapshots_only: false, + migration_path: "test_migration_path", + dev: true + ) + + assert [dev_file] = + Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + |> Enum.reject(&String.contains?(&1, "extensions")) + + assert String.contains?(dev_file, "_dev.exs") + contents = File.read!(dev_file) + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path" + ) + + assert [file] = + Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + |> Enum.reject(&String.contains?(&1, "extensions")) + + refute String.contains?(file, "_dev.exs") + + assert contents == File.read!(file) + end + end + describe "references" do setup do on_exit(fn -> From f117c160fc89be090bcc693d14bf5cbfd4b9eb70 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Fri, 7 Mar 2025 08:21:16 +0100 Subject: [PATCH 05/18] Rollback migrations before file removal --- lib/migration_generator/migration_generator.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 2d88ad39..8229f959 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -518,6 +518,12 @@ defmodule AshPostgres.MigrationGenerator do |> migration_path(repo) |> File.ls!() |> Enum.filter(&String.contains?(&1, "_dev.exs")) + |> then(fn dev_migrations -> + version = dev_migrations |> Enum.min() |> String.split("_") |> hd() + System.cmd("mix", ["ash_postgres.rollback", "--to", version]) + System.cmd("mix", ["ash_postgres.rollback", "--to", version], env: [{"MIX_ENV", "test"}]) + dev_migrations + end) |> Enum.each(fn migration_name -> opts |> migration_path(repo) From e2f0e2a32416eb0cfa8ef78e10a297f01a7c805f Mon Sep 17 00:00:00 2001 From: ken-kost Date: Fri, 7 Mar 2025 09:07:15 +0100 Subject: [PATCH 06/18] Rollback tenant migrations --- lib/migration_generator/migration_generator.ex | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 8229f959..185c712b 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -520,8 +520,17 @@ defmodule AshPostgres.MigrationGenerator do |> Enum.filter(&String.contains?(&1, "_dev.exs")) |> then(fn dev_migrations -> version = dev_migrations |> Enum.min() |> String.split("_") |> hd() + opts = [env: [{"MIX_ENV", "test"}]] System.cmd("mix", ["ash_postgres.rollback", "--to", version]) - System.cmd("mix", ["ash_postgres.rollback", "--to", version], env: [{"MIX_ENV", "test"}]) + System.cmd("mix", ["ash_postgres.rollback", "--to", version], opts) + + if tenant? do + for tenant <- AshPostgres.Mix.Helpers.tenants(repo, opts) do + System.cmd("mix", ["ash_postgres.rollback", "--to", version, "--prefix", tenant]) + System.cmd("mix", ["ash_postgres.rollback", "--to", version, "--prefix", tenant], opts) + end + end + dev_migrations end) |> Enum.each(fn migration_name -> From 32ffcca16b8cab66e01c8faf5d39bcece6f201a0 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Fri, 7 Mar 2025 13:22:47 +0100 Subject: [PATCH 07/18] Check dev for tenants, use helper functions to split up logic --- .../migration_generator.ex | 136 +++++++++--------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 185c712b..7e4e2442 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -453,7 +453,9 @@ defmodule AshPostgres.MigrationGenerator do :ok operations -> - if !opts.dev and any_dev_migrations?(opts, repo) do + dev_migrations = get_dev_migrations(opts, tenant?, repo) + + if !opts.dev and dev_migrations != [] do if opts.check do Mix.shell().error(""" Generated migrations are from dev mode. @@ -463,7 +465,8 @@ defmodule AshPostgres.MigrationGenerator do exit({:shutdown, 1}) else - remove_dev_migrations_and_snapshots(opts, tenant?, repo, snapshots) + remove_dev_migrations(dev_migrations, tenant?, repo, opts) + remove_dev_snapshots(snapshots, opts) end end @@ -506,59 +509,53 @@ defmodule AshPostgres.MigrationGenerator do end) end - defp any_dev_migrations?(opts, repo) do + defp get_dev_migrations(opts, tenant?, repo) do opts - |> migration_path(repo) - |> File.ls!() - |> Enum.any?(&String.contains?(&1, "_dev.exs")) + |> migration_path(repo, tenant?) + |> File.ls() + |> case do + {:error, _error} -> [] + {:ok, migrations} -> Enum.filter(migrations, &String.contains?(&1, "_dev.exs")) + end end - defp remove_dev_migrations_and_snapshots(opts, tenant?, repo, snapshots) do - opts - |> migration_path(repo) - |> File.ls!() - |> Enum.filter(&String.contains?(&1, "_dev.exs")) - |> then(fn dev_migrations -> - version = dev_migrations |> Enum.min() |> String.split("_") |> hd() - opts = [env: [{"MIX_ENV", "test"}]] - System.cmd("mix", ["ash_postgres.rollback", "--to", version]) - System.cmd("mix", ["ash_postgres.rollback", "--to", version], opts) + defp remove_dev_migrations(dev_migrations, tenant?, repo, opts) do + version = dev_migrations |> Enum.min() |> String.split("_") |> hd() + test_env = [env: [{"MIX_ENV", "test"}]] + System.cmd("mix", ["ash_postgres.rollback", "--to", version]) + System.cmd("mix", ["ash_postgres.rollback", "--to", version], test_env) - if tenant? do - for tenant <- AshPostgres.Mix.Helpers.tenants(repo, opts) do - System.cmd("mix", ["ash_postgres.rollback", "--to", version, "--prefix", tenant]) - System.cmd("mix", ["ash_postgres.rollback", "--to", version, "--prefix", tenant], opts) - end + if tenant? do + for tenant <- repo.all_tenants() do + System.cmd("mix", ["ash_postgres.rollback", "--to", version, "--prefix", tenant]) + + System.cmd( + "mix", + ["ash_postgres.rollback", "--to", version, "--prefix", tenant], + opts + ) end + end - dev_migrations - end) + dev_migrations |> Enum.each(fn migration_name -> opts - |> migration_path(repo) + |> migration_path(repo, tenant?) |> Path.join(migration_name) |> File.rm!() end) + end + def remove_dev_snapshots(snapshots, opts) do Enum.each(snapshots, fn snapshot -> - snapshot_folder = - if tenant? do - opts - |> snapshot_path(snapshot.repo) - |> Path.join(repo_name(repo)) - |> Path.join("tenants") - |> Path.join(snapshot.table) - else - opts - |> snapshot_path(snapshot.repo) - |> Path.join(repo_name(snapshot.repo)) - |> Path.join(snapshot.table) - end + folder = get_snapshot_folder(snapshot, opts) + snapshot_path = get_snapshot_path(snapshot, folder) - File.ls!(snapshot_folder) + snapshot_path + |> File.ls!() |> Enum.filter(&String.contains?(&1, "_dev.json")) |> Enum.each(fn snapshot_name -> - snapshot_folder + snapshot_path |> Path.join(snapshot_name) |> File.rm!() end) @@ -2732,35 +2729,11 @@ defmodule AshPostgres.MigrationGenerator do end def get_existing_snapshot(snapshot, opts) do - repo_name = snapshot.repo |> Module.split() |> List.last() |> Macro.underscore() - - folder = - if snapshot.multitenancy.strategy == :context do - opts - |> snapshot_path(snapshot.repo) - |> Path.join(repo_name) - |> Path.join("tenants") - else - opts - |> snapshot_path(snapshot.repo) - |> Path.join(repo_name) - end - - snapshot_folder = - if snapshot.schema do - schema_dir = Path.join(folder, "#{snapshot.schema}.#{snapshot.table}") - - if File.dir?(schema_dir) do - schema_dir - else - Path.join(folder, snapshot.table) - end - else - Path.join(folder, snapshot.table) - end + folder = get_snapshot_folder(snapshot, opts) + snapshot_path = get_snapshot_path(snapshot, folder) - if File.exists?(snapshot_folder) do - snapshot_folder + if File.exists?(snapshot_path) do + snapshot_path |> File.ls!() |> Enum.filter( &(String.match?(&1, ~r/^\d{14}\.json$/) or @@ -2771,7 +2744,7 @@ defmodule AshPostgres.MigrationGenerator do get_old_snapshot(folder, snapshot) snapshot_files -> - snapshot_folder + snapshot_path |> Path.join(Enum.max(snapshot_files)) |> File.read!() |> load_snapshot() @@ -2781,6 +2754,33 @@ defmodule AshPostgres.MigrationGenerator do end end + defp get_snapshot_folder(snapshot, opts) do + if snapshot.multitenancy.strategy == :context do + opts + |> snapshot_path(snapshot.repo) + |> Path.join(repo_name(snapshot.repo)) + |> Path.join("tenants") + else + opts + |> snapshot_path(snapshot.repo) + |> Path.join(repo_name(snapshot.repo)) + end + end + + defp get_snapshot_path(snapshot, folder) do + if snapshot.schema do + schema_dir = Path.join(folder, "#{snapshot.schema}.#{snapshot.table}") + + if File.dir?(schema_dir) do + schema_dir + else + Path.join(folder, snapshot.table) + end + else + Path.join(folder, snapshot.table) + end + end + defp get_old_snapshot(folder, snapshot) do schema_file = if snapshot.schema do From 3ba2116d677f5df05aae677d2c84905222112604 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Fri, 7 Mar 2025 13:23:10 +0100 Subject: [PATCH 08/18] Add dev option test for tenant --- test/migration_generator_test.exs | 51 ++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index ebac37c0..f5b2e11c 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -1408,6 +1408,7 @@ defmodule AshPostgres.MigrationGeneratorTest do on_exit(fn -> File.rm_rf!("test_snapshots_path") File.rm_rf!("test_migration_path") + File.rm_rf!("test_tenant_migration_path") end) end @@ -1423,7 +1424,6 @@ defmodule AshPostgres.MigrationGeneratorTest do AshPostgres.MigrationGenerator.generate(Domain, snapshot_path: "test_snapshots_path", - snapshots_only: false, migration_path: "test_migration_path", dev: true ) @@ -1448,6 +1448,55 @@ defmodule AshPostgres.MigrationGeneratorTest do assert contents == File.read!(file) end + + test "generates dev migration for tenant" do + defposts do + postgres do + schema("example") + end + + attributes do + uuid_primary_key(:id) + attribute(:title, :string, public?: true, primary_key?: true, allow_nil?: false) + end + + multitenancy do + strategy(:context) + end + end + + defdomain([Post]) + + send(self(), {:mix_shell_input, :yes?, true}) + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + tenant_migration_path: "test_tenant_migration_path", + dev: true + ) + + assert [dev_file] = + Enum.sort(Path.wildcard("test_tenant_migration_path/**/*_migrate_resources*.exs")) + |> Enum.reject(&String.contains?(&1, "extensions")) + + assert String.contains?(dev_file, "_dev.exs") + contents = File.read!(dev_file) + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + tenant_migration_path: "test_tenant_migration_path" + ) + + assert [file] = + Path.wildcard("test_tenant_migration_path/**/*_migrate_resources*.exs") + |> Enum.reject(&String.contains?(&1, "extensions")) + + refute String.contains?(file, "_dev.exs") + + assert contents == File.read!(file) + end end describe "references" do From bbcf674fdeb81a8b48c73d0e0c739a9ea66de9fc Mon Sep 17 00:00:00 2001 From: ken-kost Date: Sun, 9 Mar 2025 18:02:28 +0100 Subject: [PATCH 09/18] Fix wrong opts pass in tenant rollback --- lib/migration_generator/migration_generator.ex | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 7e4e2442..82c5f834 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -526,15 +526,11 @@ defmodule AshPostgres.MigrationGenerator do System.cmd("mix", ["ash_postgres.rollback", "--to", version], test_env) if tenant? do - for tenant <- repo.all_tenants() do - System.cmd("mix", ["ash_postgres.rollback", "--to", version, "--prefix", tenant]) - - System.cmd( - "mix", - ["ash_postgres.rollback", "--to", version, "--prefix", tenant], - opts - ) - end + Enum.each(repo.all_tenants(), fn tenant -> + args = ["ash_postgres.rollback", "--to", version, "--prefix", tenant] + System.cmd("mix", args) + System.cmd("mix", args, test_env) + end) end dev_migrations From b30f56f5cd63f9943a6137a23c61f297200f9e48 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Sun, 30 Mar 2025 11:10:51 +0200 Subject: [PATCH 10/18] Fix docs flag wrap --- lib/mix/tasks/ash_postgres.generate_migrations.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/ash_postgres.generate_migrations.ex b/lib/mix/tasks/ash_postgres.generate_migrations.ex index 846c0333..791f0703 100644 --- a/lib/mix/tasks/ash_postgres.generate_migrations.ex +++ b/lib/mix/tasks/ash_postgres.generate_migrations.ex @@ -21,7 +21,7 @@ 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 * `check` - no files are created, returns an exit(1) code if the current snapshots and resources don't fit - * 'dev' - dev files are created + * `dev` - dev files are created * `snapshots-only` - no migrations are generated, only snapshots are stored * `concurrent-indexes` - new identities will be run concurrently and in a separate migration (like concurrent custom indexes) From b7b2dddfff26f61708ebcfd9cfea0669b3fed1b3 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Sun, 30 Mar 2025 11:38:26 +0200 Subject: [PATCH 11/18] Use mix task run with repo option instead of system cmd --- lib/migration_generator/migration_generator.ex | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 82c5f834..79b42dce 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -521,17 +521,8 @@ defmodule AshPostgres.MigrationGenerator do defp remove_dev_migrations(dev_migrations, tenant?, repo, opts) do version = dev_migrations |> Enum.min() |> String.split("_") |> hd() - test_env = [env: [{"MIX_ENV", "test"}]] - System.cmd("mix", ["ash_postgres.rollback", "--to", version]) - System.cmd("mix", ["ash_postgres.rollback", "--to", version], test_env) - - if tenant? do - Enum.each(repo.all_tenants(), fn tenant -> - args = ["ash_postgres.rollback", "--to", version, "--prefix", tenant] - System.cmd("mix", args) - System.cmd("mix", args, test_env) - end) - end + Mix.Task.reenable("ash_postgres.rollback") + Mix.Task.run("ash_postgres.rollback", ["--repo", inspect(repo), "--to", version]) dev_migrations |> Enum.each(fn migration_name -> From 44d9a96cf51cd451cf60bd8e21c6a1009d7ec555 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Sun, 30 Mar 2025 16:21:09 +0200 Subject: [PATCH 12/18] Setup dev test repo --- config/config.exs | 10 ++++++++- test/support/dev_test_repo.ex | 39 +++++++++++++++++++++++++++++++++++ test/test_helper.exs | 2 ++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 test/support/dev_test_repo.ex diff --git a/config/config.exs b/config/config.exs index ee88e028..0041c0ac 100644 --- a/config/config.exs +++ b/config/config.exs @@ -37,6 +37,14 @@ if Mix.env() == :test do hostname: "localhost", pool: Ecto.Adapters.SQL.Sandbox + config :ash_postgres, AshPostgres.DevTestRepo, + username: "postgres", + password: "postgres", + database: "ash_postgres_dev_test", + hostname: "localhost", + migration_primary_key: [name: :id, type: :binary_id], + pool: Ecto.Adapters.SQL.Sandbox + # sobelow_skip ["Config.Secrets"] config :ash_postgres, AshPostgres.TestRepo, password: "postgres" @@ -54,7 +62,7 @@ if Mix.env() == :test do migration_primary_key: [name: :id, type: :binary_id] config :ash_postgres, - ecto_repos: [AshPostgres.TestRepo, AshPostgres.TestNoSandboxRepo], + ecto_repos: [AshPostgres.TestRepo, AshPostgres.DevTestRepo, AshPostgres.TestNoSandboxRepo], ash_domains: [ AshPostgres.Test.Domain, AshPostgres.MultitenancyTest.Domain, diff --git a/test/support/dev_test_repo.ex b/test/support/dev_test_repo.ex new file mode 100644 index 00000000..67819081 --- /dev/null +++ b/test/support/dev_test_repo.ex @@ -0,0 +1,39 @@ +defmodule AshPostgres.DevTestRepo do + @moduledoc false + use AshPostgres.Repo, + otp_app: :ash_postgres + + def on_transaction_begin(data) do + send(self(), data) + end + + def prefer_transaction?, do: false + + def prefer_transaction_for_atomic_updates?, do: false + + def installed_extensions do + ["ash-functions", "uuid-ossp", "pg_trgm", "citext", AshPostgres.TestCustomExtension, "ltree"] -- + Application.get_env(:ash_postgres, :no_extensions, []) + end + + def min_pg_version do + case System.get_env("PG_VERSION") do + nil -> + %Version{major: 16, minor: 0, patch: 0} + + version -> + case Integer.parse(version) do + {major, ""} -> %Version{major: major, minor: 0, patch: 0} + _ -> Version.parse!(version) + end + end + end + + def all_tenants do + Code.ensure_compiled(AshPostgres.MultitenancyTest.Org) + + AshPostgres.MultitenancyTest.Org + |> Ash.read!() + |> Enum.map(&"org_#{&1.id}") + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 056ebefd..6745f6eb 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -20,6 +20,7 @@ exclude_tags = ExUnit.configure(stacktrace_depth: 100, exclude: exclude_tags) AshPostgres.TestRepo.start_link() +AshPostgres.DevTestRepo.start_link() AshPostgres.TestNoSandboxRepo.start_link() format_sql_query = @@ -49,4 +50,5 @@ format_sql_query = end Ecto.DevLogger.install(AshPostgres.TestRepo, before_inline_callback: format_sql_query) +Ecto.DevLogger.install(AshPostgres.DevTestRepo, before_inline_callback: format_sql_query) Ecto.DevLogger.install(AshPostgres.TestNoSandboxRepo, before_inline_callback: format_sql_query) From c0ac88dbaf537661c702076820fef3643c54c3b2 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Sun, 30 Mar 2025 16:21:24 +0200 Subject: [PATCH 13/18] Add dev migrations test --- test/dev_migrations_test.exs | 136 +++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 test/dev_migrations_test.exs diff --git a/test/dev_migrations_test.exs b/test/dev_migrations_test.exs new file mode 100644 index 00000000..da92fc1b --- /dev/null +++ b/test/dev_migrations_test.exs @@ -0,0 +1,136 @@ +defmodule AshPostgres.DevMigrationsTest do + use AshPostgres.RepoCase, async: false + @moduletag :migration + + import ExUnit.CaptureLog + + alias Ecto.Adapters.SQL.Sandbox + + setup do + current_shell = Mix.shell() + + :ok = Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(current_shell) + end) + + Sandbox.checkout(AshPostgres.DevTestRepo) + end + + defmacrop defresource(mod, do: body) do + quote do + Code.compiler_options(ignore_module_conflict: true) + + defmodule unquote(mod) do + use Ash.Resource, + domain: nil, + data_layer: AshPostgres.DataLayer + + unquote(body) + end + + Code.compiler_options(ignore_module_conflict: false) + end + end + + defmacrop defposts(mod \\ Post, do: body) do + quote do + defresource unquote(mod) do + postgres do + table "posts" + repo(AshPostgres.DevTestRepo) + + custom_indexes do + # need one without any opts + index(["id"]) + index(["id"], unique: true, name: "test_unique_index") + end + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + + unquote(body) + end + end + end + + defmacrop defdomain(resources) do + quote do + Code.compiler_options(ignore_module_conflict: true) + + defmodule Domain do + use Ash.Domain + + resources do + for resource <- unquote(resources) do + resource(resource) + end + end + end + + Code.compiler_options(ignore_module_conflict: false) + end + end + + describe "--dev option" do + setup do + on_exit(fn -> + resource_dev_path = "priv/resource_snapshots/dev_test_repo" + resource_files = File.ls!(resource_dev_path) + Enum.each(resource_files, &File.rm_rf!(Path.join(resource_dev_path, &1))) + migrations_dev_path = "priv/dev_test_repo/migrations" + migration_files = File.ls!(migrations_dev_path) + Enum.each(migration_files, &File.rm!(Path.join(migrations_dev_path, &1))) + tenant_migrations_dev_path = "priv/dev_test_repo/tenant_migrations" + tenant_migration_files = File.ls!(tenant_migrations_dev_path) + Enum.each(tenant_migration_files, &File.rm!(Path.join(tenant_migrations_dev_path, &1))) + AshPostgres.DevTestRepo.query!("DROP TABLE posts") + end) + end + + test "rolls back dev migrations before deleting" do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:title, :string, public?: true) + end + end + + defdomain([Post]) + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: "priv/resource_snapshots", + migration_path: "priv/dev_test_repo/migrations", + dev: true + ) + + assert [_migration] = + Enum.sort( + Path.wildcard("priv/dev_test_repo/migrations/**/*_migrate_resources*.exs") + ) + |> Enum.reject(&String.contains?(&1, "extensions")) + + capture_log(fn -> migrate() end) =~ "create table posts" + capture_log(fn -> migrate() end) =~ "create table posts" + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: "priv/resource_snapshots", + migration_path: "priv/dev_test_repo/migrations" + ) + + capture_log(fn -> migrate() end) =~ "create table posts" + end + end + + defp migrate do + Mix.Tasks.AshPostgres.Migrate.run([ + "--migrations-path", + "priv/dev_test_repo/migrations", + "--repo", + "AshPostgres.DevTestRepo" + ]) + end +end From d9bf13ad3a039ac588ac328e30bf934931512ce9 Mon Sep 17 00:00:00 2001 From: ken-kost Date: Sun, 30 Mar 2025 16:22:01 +0200 Subject: [PATCH 14/18] WIP --- test/dev_migrations_test.exs | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/test/dev_migrations_test.exs b/test/dev_migrations_test.exs index da92fc1b..74f8112c 100644 --- a/test/dev_migrations_test.exs +++ b/test/dev_migrations_test.exs @@ -125,6 +125,72 @@ defmodule AshPostgres.DevMigrationsTest do end end + describe "--dev option tenant" do + setup do + on_exit(fn -> + resource_dev_path = "priv/resource_snapshots/dev_test_repo" + resource_files = File.ls!(resource_dev_path) + Enum.each(resource_files, &File.rm_rf!(Path.join(resource_dev_path, &1))) + migrations_dev_path = "priv/dev_test_repo/migrations" + migration_files = File.ls!(migrations_dev_path) + Enum.each(migration_files, &File.rm!(Path.join(migrations_dev_path, &1))) + tenant_migrations_dev_path = "priv/dev_test_repo/tenant_migrations" + tenant_migration_files = File.ls!(tenant_migrations_dev_path) + Enum.each(tenant_migration_files, &File.rm!(Path.join(tenant_migrations_dev_path, &1))) + end) + end + + test "rolls back dev migrations before deleting" do + defposts do + postgres do + schema("example") + end + + attributes do + uuid_primary_key(:id) + attribute(:title, :string, public?: true, primary_key?: true, allow_nil?: false) + end + + multitenancy do + strategy(:context) + end + end + + defdomain([Post]) + capture_log(fn -> tenant_migrate() end) |> dbg() + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: "priv/resource_snapshots", + migration_path: "priv/dev_test_repo/migrations", + tenant_migration_path: "priv/dev_test_repo/tenant_migrations", + dev: true + ) + + assert [] = + Enum.sort( + Path.wildcard("priv/dev_test_repo/migrations/**/*_migrate_resources*.exs") + ) + |> Enum.reject(&String.contains?(&1, "extensions")) + + assert [_tenant_migration] = + Enum.sort( + Path.wildcard("priv/dev_test_repo/tenant_migrations/**/*_migrate_resources*.exs") + ) + |> Enum.reject(&String.contains?(&1, "extensions")) + + assert capture_log(fn -> tenant_migrate() end) =~ "create table posts" + assert capture_log(fn -> tenant_migrate() end) =~ "create table posts" + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: "priv/resource_snapshots", + migration_path: "priv/dev_test_repo/migrations", + tenant_migration_path: "priv/dev_test_repo/tenant_migrations" + ) + + assert capture_log(fn -> tenant_migrate() end) =~ "create table posts" + end + end + defp migrate do Mix.Tasks.AshPostgres.Migrate.run([ "--migrations-path", @@ -133,4 +199,14 @@ defmodule AshPostgres.DevMigrationsTest do "AshPostgres.DevTestRepo" ]) end + + defp tenant_migrate do + Mix.Tasks.AshPostgres.Migrate.run([ + "--migrations-path", + "priv/dev_test_repo/tenant_migrations", + "--repo", + "AshPostgres.DevTestRepo", + "--tenants" + ]) + end end From 20262c4017475ae109d8a0c6f0db18f8c5d6358f Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 26 May 2025 18:41:35 -0400 Subject: [PATCH 15/18] chore: fixes & testing updates --- .../migration_generator.ex | 104 ++++++++++- lib/mix/tasks/ash_postgres.migrate.ex | 11 +- lib/mix/tasks/ash_postgres.rollback.ex | 25 ++- lib/multitenancy.ex | 14 +- ...6214825_migrate_resources_extensions_1.exs | 172 ++++++++++++++++++ .../20250526214827_migrate_resources1.exs | 29 +++ .../dev_test_repo/extensions.json | 11 ++ .../multitenant_orgs/20250526214827.json | 70 +++++++ test/dev_migrations_test.exs | 142 ++++++++------- test/migration_generator_test.exs | 96 ---------- test/support/dev_test_repo.ex | 2 +- test/support/multitenancy/domain.ex | 1 + .../resources/dev_migrations_org.ex | 91 +++++++++ 13 files changed, 584 insertions(+), 184 deletions(-) create mode 100644 priv/dev_test_repo/migrations/20250526214825_migrate_resources_extensions_1.exs create mode 100644 priv/dev_test_repo/migrations/20250526214827_migrate_resources1.exs create mode 100644 priv/resource_snapshots/dev_test_repo/extensions.json create mode 100644 priv/resource_snapshots/dev_test_repo/multitenant_orgs/20250526214827.json create mode 100644 test/support/multitenancy/resources/dev_migrations_org.ex diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 79b42dce..ba725d64 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -519,18 +519,102 @@ defmodule AshPostgres.MigrationGenerator do end end + if Mix.env() == :test do + defp with_repo_not_in_test(repo, fun) do + fun.(repo) + end + else + defp with_repo_not_in_test(repo, fun) do + Ecto.Migrator.with_repo(repo, fun) + end + end + defp remove_dev_migrations(dev_migrations, tenant?, repo, opts) do - version = dev_migrations |> Enum.min() |> String.split("_") |> hd() - Mix.Task.reenable("ash_postgres.rollback") - Mix.Task.run("ash_postgres.rollback", ["--repo", inspect(repo), "--to", version]) + dev_migrations = + Enum.map(dev_migrations, fn migration -> + opts + |> migration_path(repo, tenant?) + |> Path.join(migration) + end) - dev_migrations - |> Enum.each(fn migration_name -> - opts - |> migration_path(repo, tenant?) - |> Path.join(migration_name) - |> File.rm!() - end) + if tenant? do + with_repo_not_in_test(repo, fn repo -> + for prefix <- repo.all_tenants() do + dev_migrations + |> Enum.map(&extract_migration_info/1) + |> Enum.filter(& &1) + |> Enum.map(&load_migration!/1) + |> Enum.each(fn {version, mod} -> + Ecto.Migration.Runner.run( + repo, + [], + version, + mod, + :forward, + :down, + :down, + all: true, + prefix: prefix + ) + + Ecto.Migration.SchemaMigration.down(repo, repo.config(), version, prefix: prefix) + end) + end + end) + else + with_repo_not_in_test(repo, fn repo -> + dev_migrations + |> Enum.map(&extract_migration_info/1) + |> Enum.filter(& &1) + |> Enum.map(&load_migration!/1) + |> Enum.sort() + |> Enum.each(fn {version, mod} -> + Ecto.Migration.Runner.run( + repo, + [], + version, + mod, + :forward, + :down, + :down, + all: true + ) + + Ecto.Migration.SchemaMigration.down(repo, repo.config(), version, []) + end) + end) + end + + Enum.each(dev_migrations, &File.rm!/1) + end + + defp extract_migration_info(file) do + base = Path.basename(file) + + case Integer.parse(Path.rootname(base)) do + {integer, "_" <> name} -> {integer, name, file} + _ -> nil + end + end + + defp load_migration!({version, _, file}) when is_binary(file) do + loaded_modules = file |> compile_file() |> Enum.map(&elem(&1, 0)) + + if mod = Enum.find(loaded_modules, &migration?/1) do + {version, mod} + else + raise Ecto.MigrationError, + "file #{Path.relative_to_cwd(file)} does not define an Ecto.Migration" + end + end + + defp compile_file(file) do + AshPostgres.MigrationCompileCache.start_link() + AshPostgres.MigrationCompileCache.compile_file(file) + end + + defp migration?(mod) do + function_exported?(mod, :__migration__, 0) end def remove_dev_snapshots(snapshots, opts) do diff --git a/lib/mix/tasks/ash_postgres.migrate.ex b/lib/mix/tasks/ash_postgres.migrate.ex index 36b9187d..3299fc5b 100644 --- a/lib/mix/tasks/ash_postgres.migrate.ex +++ b/lib/mix/tasks/ash_postgres.migrate.ex @@ -119,6 +119,8 @@ defmodule Mix.Tasks.AshPostgres.Migrate do |> AshPostgres.Mix.Helpers.delete_flag("--tenants") |> AshPostgres.Mix.Helpers.delete_arg("--only-tenants") |> AshPostgres.Mix.Helpers.delete_arg("--except-tenants") + |> AshPostgres.Mix.Helpers.delete_arg("--repo") + |> AshPostgres.Mix.Helpers.delete_arg("-r") Mix.Task.reenable("ecto.migrate") @@ -128,9 +130,16 @@ defmodule Mix.Tasks.AshPostgres.Migrate do for tenant <- tenants(repo, opts) do rest_opts = AshPostgres.Mix.Helpers.delete_arg(rest_opts, "--prefix") + repo = + if is_atom(repo) do + inspect(repo) + else + repo + end + Mix.Task.run( "ecto.migrate", - ["-r", to_string(repo)] ++ + ["-r", repo] ++ rest_opts ++ ["--prefix", tenant, "--migrations-path", tenant_migrations_path(opts, repo)] ) diff --git a/lib/mix/tasks/ash_postgres.rollback.ex b/lib/mix/tasks/ash_postgres.rollback.ex index b9a8354a..add81d59 100644 --- a/lib/mix/tasks/ash_postgres.rollback.ex +++ b/lib/mix/tasks/ash_postgres.rollback.ex @@ -61,6 +61,7 @@ defmodule Mix.Tasks.AshPostgres.Rollback do log_migrator_sql: :boolean, only_tenants: :string, except_tenants: :string, + already_started: :boolean, repo: :string ], aliases: [n: :step, v: :to, r: :repo] @@ -76,25 +77,23 @@ defmodule Mix.Tasks.AshPostgres.Rollback do |> AshPostgres.Mix.Helpers.delete_flag("--only-tenants") |> AshPostgres.Mix.Helpers.delete_flag("--except-tenants") |> AshPostgres.Mix.Helpers.delete_arg("-r") + |> AshPostgres.Mix.Helpers.delete_arg("--already-started") Mix.Task.reenable("ecto.rollback") if opts[:tenants] do for repo <- repos do - Ecto.Migrator.with_repo(repo, fn repo -> - for tenant <- tenants(repo, opts) do - rest_opts = AshPostgres.Mix.Helpers.delete_arg(rest_opts, "--prefix") + for tenant <- tenants(repo, opts) do + Mix.Task.reenable("ecto.rollback") + rest_opts = AshPostgres.Mix.Helpers.delete_arg(rest_opts, "--prefix") - Mix.Task.run( - "ecto.rollback", - ["-r", to_string(repo)] ++ - rest_opts ++ - ["--prefix", tenant, "--migrations-path", tenant_migrations_path(opts, repo)] - ) - - Mix.Task.reenable("ecto.rollback") - end - end) + Mix.Task.run( + "ecto.rollback", + ["-r", to_string(repo)] ++ + rest_opts ++ + ["--prefix", tenant, "--migrations-path", tenant_migrations_path(opts, repo)] + ) + end end else for repo <- repos do diff --git a/lib/multitenancy.ex b/lib/multitenancy.ex index 96cddafc..eac6d15a 100644 --- a/lib/multitenancy.ex +++ b/lib/multitenancy.ex @@ -3,6 +3,7 @@ defmodule AshPostgres.MultiTenancy do @dialyzer {:nowarn_function, load_migration!: 1} + # sobelow_skip ["SQL.Query"] def create_tenant!(tenant_name, repo) do validate_tenant_name!(tenant_name) Ecto.Adapters.SQL.query!(repo, "CREATE SCHEMA IF NOT EXISTS \"#{tenant_name}\"", []) @@ -10,7 +11,7 @@ defmodule AshPostgres.MultiTenancy do migrate_tenant(tenant_name, repo) end - def migrate_tenant(tenant_name, repo, migrations_path \\ nil) do + def migrate_tenant(tenant_name, repo, migrations_path \\ nil, after_file \\ nil) do tenant_migrations_path = migrations_path || repo.config()[:tenant_migrations_path] || default_tenant_migration_path(repo) @@ -24,6 +25,17 @@ defmodule AshPostgres.MultiTenancy do [tenant_migrations_path, "**", "*.exs"] |> Path.join() |> Path.wildcard() + |> then(fn files -> + if after_file do + files + |> Enum.drop_while(fn file -> + file != after_file + end) + |> Enum.drop(1) + else + files + end + end) |> Enum.map(&extract_migration_info/1) |> Enum.filter(& &1) |> Enum.map(&load_migration!/1) diff --git a/priv/dev_test_repo/migrations/20250526214825_migrate_resources_extensions_1.exs b/priv/dev_test_repo/migrations/20250526214825_migrate_resources_extensions_1.exs new file mode 100644 index 00000000..389b0b27 --- /dev/null +++ b/priv/dev_test_repo/migrations/20250526214825_migrate_resources_extensions_1.exs @@ -0,0 +1,172 @@ +defmodule AshPostgres.DevTestRepo.Migrations.MigrateResourcesExtensions1 do + @moduledoc """ + Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + execute(""" + CREATE OR REPLACE FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) + AS $$ SELECT COALESCE(NULLIF($1, FALSE), $2) $$ + LANGUAGE SQL + SET search_path = '' + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) + AS $$ SELECT COALESCE($1, $2) $$ + LANGUAGE SQL + SET search_path = '' + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ + SELECT CASE + WHEN $1 IS TRUE THEN $2 + ELSE $1 + END $$ + LANGUAGE SQL + SET search_path = '' + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ + SELECT CASE + WHEN $1 IS NOT NULL THEN $2 + ELSE $1 + END $$ + LANGUAGE SQL + SET search_path = '' + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[]) + RETURNS text[] AS $$ + DECLARE + start_index INT = 1; + end_index INT = array_length(arr, 1); + BEGIN + WHILE start_index <= end_index AND arr[start_index] = '' LOOP + start_index := start_index + 1; + END LOOP; + + WHILE end_index >= start_index AND arr[end_index] = '' LOOP + end_index := end_index - 1; + END LOOP; + + IF start_index > end_index THEN + RETURN ARRAY[]::text[]; + ELSE + RETURN arr[start_index : end_index]; + END IF; + END; $$ + LANGUAGE plpgsql + SET search_path = '' + IMMUTABLE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb) + RETURNS BOOLEAN AS $$ + BEGIN + -- Raise an error with the provided JSON data. + -- The JSON object is converted to text for inclusion in the error message. + RAISE EXCEPTION 'ash_error: %', json_data::text; + RETURN NULL; + END; + $$ LANGUAGE plpgsql + STABLE + SET search_path = ''; + """) + + execute(""" + CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb, type_signal ANYCOMPATIBLE) + RETURNS ANYCOMPATIBLE AS $$ + BEGIN + -- Raise an error with the provided JSON data. + -- The JSON object is converted to text for inclusion in the error message. + RAISE EXCEPTION 'ash_error: %', json_data::text; + RETURN NULL; + END; + $$ LANGUAGE plpgsql + STABLE + SET search_path = ''; + """) + + execute(""" + CREATE OR REPLACE FUNCTION uuid_generate_v7() + RETURNS UUID + AS $$ + DECLARE + timestamp TIMESTAMPTZ; + microseconds INT; + BEGIN + timestamp = clock_timestamp(); + microseconds = (cast(extract(microseconds FROM timestamp)::INT - (floor(extract(milliseconds FROM timestamp))::INT * 1000) AS DOUBLE PRECISION) * 4.096)::INT; + + RETURN encode( + set_byte( + set_byte( + overlay(uuid_send(gen_random_uuid()) placing substring(int8send(floor(extract(epoch FROM timestamp) * 1000)::BIGINT) FROM 3) FROM 1 FOR 6 + ), + 6, (b'0111' || (microseconds >> 8)::bit(4))::bit(8)::int + ), + 7, microseconds::bit(8)::int + ), + 'hex')::UUID; + END + $$ + LANGUAGE PLPGSQL + SET search_path = '' + VOLATILE; + """) + + execute(""" + CREATE OR REPLACE FUNCTION timestamp_from_uuid_v7(_uuid uuid) + RETURNS TIMESTAMP WITHOUT TIME ZONE + AS $$ + SELECT to_timestamp(('x0000' || substr(_uuid::TEXT, 1, 8) || substr(_uuid::TEXT, 10, 4))::BIT(64)::BIGINT::NUMERIC / 1000); + $$ + LANGUAGE SQL + SET search_path = '' + IMMUTABLE PARALLEL SAFE STRICT; + """) + + execute("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"") + execute("CREATE EXTENSION IF NOT EXISTS \"pg_trgm\"") + execute("CREATE EXTENSION IF NOT EXISTS \"citext\"") + + execute(""" + CREATE OR REPLACE FUNCTION ash_demo_functions() + RETURNS boolean AS $$ SELECT TRUE $$ + LANGUAGE SQL + IMMUTABLE; + """) + + execute("CREATE EXTENSION IF NOT EXISTS \"ltree\"") + end + + def down do + # Uncomment this if you actually want to uninstall the extensions + # when this migration is rolled back: + execute( + "DROP FUNCTION IF EXISTS uuid_generate_v7(), timestamp_from_uuid_v7(uuid), ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_elixir_and(BOOLEAN, ANYCOMPATIBLE), ash_elixir_and(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(BOOLEAN, ANYCOMPATIBLE), ash_trim_whitespace(text[])" + ) + + # execute("DROP EXTENSION IF EXISTS \"uuid-ossp\"") + # execute("DROP EXTENSION IF EXISTS \"pg_trgm\"") + # execute("DROP EXTENSION IF EXISTS \"citext\"") + execute(""" + DROP FUNCTION IF EXISTS ash_demo_functions() + """) + + # execute("DROP EXTENSION IF EXISTS \"ltree\"") + end +end diff --git a/priv/dev_test_repo/migrations/20250526214827_migrate_resources1.exs b/priv/dev_test_repo/migrations/20250526214827_migrate_resources1.exs new file mode 100644 index 00000000..3a56462c --- /dev/null +++ b/priv/dev_test_repo/migrations/20250526214827_migrate_resources1.exs @@ -0,0 +1,29 @@ +defmodule AshPostgres.DevTestRepo.Migrations.MigrateResources1 do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + create table(:multitenant_orgs, primary_key: false) do + add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true) + add(:name, :text) + add(:owner_id, :uuid) + end + + create( + unique_index(:multitenant_orgs, [:id, :name], name: "multitenant_orgs_unique_by_name_index") + ) + end + + def down do + drop_if_exists( + unique_index(:multitenant_orgs, [:id, :name], name: "multitenant_orgs_unique_by_name_index") + ) + + drop(table(:multitenant_orgs)) + end +end diff --git a/priv/resource_snapshots/dev_test_repo/extensions.json b/priv/resource_snapshots/dev_test_repo/extensions.json new file mode 100644 index 00000000..d1c5a122 --- /dev/null +++ b/priv/resource_snapshots/dev_test_repo/extensions.json @@ -0,0 +1,11 @@ +{ + "ash_functions_version": 5, + "installed": [ + "ash-functions", + "uuid-ossp", + "pg_trgm", + "citext", + "demo-functions_v1", + "ltree" + ] +} \ No newline at end of file diff --git a/priv/resource_snapshots/dev_test_repo/multitenant_orgs/20250526214827.json b/priv/resource_snapshots/dev_test_repo/multitenant_orgs/20250526214827.json new file mode 100644 index 00000000..072cfe57 --- /dev/null +++ b/priv/resource_snapshots/dev_test_repo/multitenant_orgs/20250526214827.json @@ -0,0 +1,70 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "owner_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "979D7BEB5939EAD2706978B7914C561B5F6854EBBCFD1F2748AB4C98668304EB", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "multitenant_orgs_unique_by_name_index", + "keys": [ + { + "type": "atom", + "value": "name" + } + ], + "name": "unique_by_name", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": "id", + "global": true, + "strategy": "attribute" + }, + "repo": "Elixir.AshPostgres.DevTestRepo", + "schema": null, + "table": "multitenant_orgs" +} \ No newline at end of file diff --git a/test/dev_migrations_test.exs b/test/dev_migrations_test.exs index 74f8112c..f5d7b2f2 100644 --- a/test/dev_migrations_test.exs +++ b/test/dev_migrations_test.exs @@ -3,6 +3,7 @@ defmodule AshPostgres.DevMigrationsTest do @moduletag :migration import ExUnit.CaptureLog + require Logger alias Ecto.Adapters.SQL.Sandbox @@ -34,9 +35,9 @@ defmodule AshPostgres.DevMigrationsTest do end end - defmacrop defposts(mod \\ Post, do: body) do + defmacrop defposts(do: body) do quote do - defresource unquote(mod) do + defresource Post do postgres do table "posts" repo(AshPostgres.DevTestRepo) @@ -75,22 +76,54 @@ defmodule AshPostgres.DevMigrationsTest do end end - describe "--dev option" do - setup do - on_exit(fn -> - resource_dev_path = "priv/resource_snapshots/dev_test_repo" - resource_files = File.ls!(resource_dev_path) - Enum.each(resource_files, &File.rm_rf!(Path.join(resource_dev_path, &1))) - migrations_dev_path = "priv/dev_test_repo/migrations" - migration_files = File.ls!(migrations_dev_path) - Enum.each(migration_files, &File.rm!(Path.join(migrations_dev_path, &1))) - tenant_migrations_dev_path = "priv/dev_test_repo/tenant_migrations" - tenant_migration_files = File.ls!(tenant_migrations_dev_path) - Enum.each(tenant_migration_files, &File.rm!(Path.join(tenant_migrations_dev_path, &1))) - AshPostgres.DevTestRepo.query!("DROP TABLE posts") - end) - end + setup_all do + resource_dev_path = "priv/resource_snapshots/dev_test_repo" + + initial_resource_files = + if File.exists?(resource_dev_path), do: File.ls!(resource_dev_path), else: [] + + migrations_dev_path = "priv/dev_test_repo/migrations" + + initial_migration_files = + if File.exists?(migrations_dev_path), do: File.ls!(migrations_dev_path), else: [] + + tenant_migrations_dev_path = "priv/dev_test_repo/tenant_migrations" + + initial_tenant_migration_files = + if File.exists?(tenant_migrations_dev_path), + do: File.ls!(tenant_migrations_dev_path), + else: [] + + on_exit(fn -> + if File.exists?(resource_dev_path) do + current_resource_files = File.ls!(resource_dev_path) + new_resource_files = current_resource_files -- initial_resource_files + Enum.each(new_resource_files, &File.rm_rf!(Path.join(resource_dev_path, &1))) + end + if File.exists?(migrations_dev_path) do + current_migration_files = File.ls!(migrations_dev_path) + new_migration_files = current_migration_files -- initial_migration_files + Enum.each(new_migration_files, &File.rm!(Path.join(migrations_dev_path, &1))) + end + + if File.exists?(tenant_migrations_dev_path) do + current_tenant_migration_files = File.ls!(tenant_migrations_dev_path) + + new_tenant_migration_files = + current_tenant_migration_files -- initial_tenant_migration_files + + Enum.each( + new_tenant_migration_files, + &File.rm!(Path.join(tenant_migrations_dev_path, &1)) + ) + end + + AshPostgres.DevTestRepo.query!("DROP TABLE IF EXISTS posts") + end) + end + + describe "--dev option" do test "rolls back dev migrations before deleting" do defposts do attributes do @@ -107,45 +140,23 @@ defmodule AshPostgres.DevMigrationsTest do dev: true ) - assert [_migration] = - Enum.sort( - Path.wildcard("priv/dev_test_repo/migrations/**/*_migrate_resources*.exs") - ) - |> Enum.reject(&String.contains?(&1, "extensions")) + assert [_extensions, migration, _migration] = + Path.wildcard("priv/dev_test_repo/migrations/**/*_migrate_resources*.exs") - capture_log(fn -> migrate() end) =~ "create table posts" - capture_log(fn -> migrate() end) =~ "create table posts" + assert capture_log(fn -> migrate(migration) end) =~ "create table posts" AshPostgres.MigrationGenerator.generate(Domain, snapshot_path: "priv/resource_snapshots", migration_path: "priv/dev_test_repo/migrations" ) - capture_log(fn -> migrate() end) =~ "create table posts" + assert capture_log(fn -> migrate(migration) end) =~ "create table posts" end end describe "--dev option tenant" do - setup do - on_exit(fn -> - resource_dev_path = "priv/resource_snapshots/dev_test_repo" - resource_files = File.ls!(resource_dev_path) - Enum.each(resource_files, &File.rm_rf!(Path.join(resource_dev_path, &1))) - migrations_dev_path = "priv/dev_test_repo/migrations" - migration_files = File.ls!(migrations_dev_path) - Enum.each(migration_files, &File.rm!(Path.join(migrations_dev_path, &1))) - tenant_migrations_dev_path = "priv/dev_test_repo/tenant_migrations" - tenant_migration_files = File.ls!(tenant_migrations_dev_path) - Enum.each(tenant_migration_files, &File.rm!(Path.join(tenant_migrations_dev_path, &1))) - end) - end - test "rolls back dev migrations before deleting" do defposts do - postgres do - schema("example") - end - attributes do uuid_primary_key(:id) attribute(:title, :string, public?: true, primary_key?: true, allow_nil?: false) @@ -157,7 +168,6 @@ defmodule AshPostgres.DevMigrationsTest do end defdomain([Post]) - capture_log(fn -> tenant_migrate() end) |> dbg() AshPostgres.MigrationGenerator.generate(Domain, snapshot_path: "priv/resource_snapshots", @@ -166,7 +176,12 @@ defmodule AshPostgres.DevMigrationsTest do dev: true ) - assert [] = + org = + AshPostgres.MultitenancyTest.DevMigrationsOrg + |> Ash.Changeset.for_create(:create, %{name: "test1"}, authorize?: false) + |> Ash.create!() + + assert [_] = Enum.sort( Path.wildcard("priv/dev_test_repo/migrations/**/*_migrate_resources*.exs") ) @@ -178,35 +193,38 @@ defmodule AshPostgres.DevMigrationsTest do ) |> Enum.reject(&String.contains?(&1, "extensions")) - assert capture_log(fn -> tenant_migrate() end) =~ "create table posts" - assert capture_log(fn -> tenant_migrate() end) =~ "create table posts" - AshPostgres.MigrationGenerator.generate(Domain, snapshot_path: "priv/resource_snapshots", migration_path: "priv/dev_test_repo/migrations", tenant_migration_path: "priv/dev_test_repo/tenant_migrations" ) - assert capture_log(fn -> tenant_migrate() end) =~ "create table posts" + assert [_tenant_migration] = + Enum.sort( + Path.wildcard("priv/dev_test_repo/tenant_migrations/**/*_migrate_resources*.exs") + ) + |> Enum.reject(&String.contains?(&1, "extensions")) + + assert capture_log(fn -> tenant_migrate() end) =~ "create table org_#{org.id}.posts" end end - defp migrate do - Mix.Tasks.AshPostgres.Migrate.run([ - "--migrations-path", + defp migrate(after_file) do + AshPostgres.MultiTenancy.migrate_tenant( + nil, + AshPostgres.DevTestRepo, "priv/dev_test_repo/migrations", - "--repo", - "AshPostgres.DevTestRepo" - ]) + after_file + ) end defp tenant_migrate do - Mix.Tasks.AshPostgres.Migrate.run([ - "--migrations-path", - "priv/dev_test_repo/tenant_migrations", - "--repo", - "AshPostgres.DevTestRepo", - "--tenants" - ]) + for tenant <- AshPostgres.DevTestRepo.all_tenants() do + AshPostgres.MultiTenancy.migrate_tenant( + tenant, + AshPostgres.DevTestRepo, + "priv/dev_test_repo/tenant_migrations" + ) + end end end diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index f5b2e11c..a1168ccb 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -1403,102 +1403,6 @@ defmodule AshPostgres.MigrationGeneratorTest do end end - describe "--dev option" do - setup do - on_exit(fn -> - File.rm_rf!("test_snapshots_path") - File.rm_rf!("test_migration_path") - File.rm_rf!("test_tenant_migration_path") - end) - end - - test "generates dev migration" do - defposts do - attributes do - uuid_primary_key(:id) - attribute(:title, :string, public?: true) - end - end - - defdomain([Post]) - - AshPostgres.MigrationGenerator.generate(Domain, - snapshot_path: "test_snapshots_path", - migration_path: "test_migration_path", - dev: true - ) - - assert [dev_file] = - Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") - |> Enum.reject(&String.contains?(&1, "extensions")) - - assert String.contains?(dev_file, "_dev.exs") - contents = File.read!(dev_file) - - AshPostgres.MigrationGenerator.generate(Domain, - snapshot_path: "test_snapshots_path", - migration_path: "test_migration_path" - ) - - assert [file] = - Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") - |> Enum.reject(&String.contains?(&1, "extensions")) - - refute String.contains?(file, "_dev.exs") - - assert contents == File.read!(file) - end - - test "generates dev migration for tenant" do - defposts do - postgres do - schema("example") - end - - attributes do - uuid_primary_key(:id) - attribute(:title, :string, public?: true, primary_key?: true, allow_nil?: false) - end - - multitenancy do - strategy(:context) - end - end - - defdomain([Post]) - - send(self(), {:mix_shell_input, :yes?, true}) - - AshPostgres.MigrationGenerator.generate(Domain, - snapshot_path: "test_snapshots_path", - migration_path: "test_migration_path", - tenant_migration_path: "test_tenant_migration_path", - dev: true - ) - - assert [dev_file] = - Enum.sort(Path.wildcard("test_tenant_migration_path/**/*_migrate_resources*.exs")) - |> Enum.reject(&String.contains?(&1, "extensions")) - - assert String.contains?(dev_file, "_dev.exs") - contents = File.read!(dev_file) - - AshPostgres.MigrationGenerator.generate(Domain, - snapshot_path: "test_snapshots_path", - migration_path: "test_migration_path", - tenant_migration_path: "test_tenant_migration_path" - ) - - assert [file] = - Path.wildcard("test_tenant_migration_path/**/*_migrate_resources*.exs") - |> Enum.reject(&String.contains?(&1, "extensions")) - - refute String.contains?(file, "_dev.exs") - - assert contents == File.read!(file) - end - end - describe "references" do setup do on_exit(fn -> diff --git a/test/support/dev_test_repo.ex b/test/support/dev_test_repo.ex index 67819081..2b1fe929 100644 --- a/test/support/dev_test_repo.ex +++ b/test/support/dev_test_repo.ex @@ -32,7 +32,7 @@ defmodule AshPostgres.DevTestRepo do def all_tenants do Code.ensure_compiled(AshPostgres.MultitenancyTest.Org) - AshPostgres.MultitenancyTest.Org + AshPostgres.MultitenancyTest.DevMigrationsOrg |> Ash.read!() |> Enum.map(&"org_#{&1.id}") end diff --git a/test/support/multitenancy/domain.ex b/test/support/multitenancy/domain.ex index 9ad6f881..52ffbc63 100644 --- a/test/support/multitenancy/domain.ex +++ b/test/support/multitenancy/domain.ex @@ -4,6 +4,7 @@ defmodule AshPostgres.MultitenancyTest.Domain do resources do resource(AshPostgres.MultitenancyTest.Org) + resource(AshPostgres.MultitenancyTest.DevMigrationsOrg) resource(AshPostgres.MultitenancyTest.NamedOrg) resource(AshPostgres.MultitenancyTest.User) resource(AshPostgres.MultitenancyTest.Post) diff --git a/test/support/multitenancy/resources/dev_migrations_org.ex b/test/support/multitenancy/resources/dev_migrations_org.ex new file mode 100644 index 00000000..6ebd3394 --- /dev/null +++ b/test/support/multitenancy/resources/dev_migrations_org.ex @@ -0,0 +1,91 @@ +defmodule AshPostgres.MultitenancyTest.DevMigrationsOrg do + @moduledoc false + use Ash.Resource, + domain: AshPostgres.MultitenancyTest.Domain, + data_layer: AshPostgres.DataLayer, + authorizers: [Ash.Policy.Authorizer] + + defimpl Ash.ToTenant do + def to_tenant(%{id: id}, resource) do + if Ash.Resource.Info.data_layer(resource) == AshPostgres.DataLayer && + Ash.Resource.Info.multitenancy_strategy(resource) == :context do + "org_#{id}" + else + id + end + end + end + + policies do + policy action(:has_policies) do + authorize_if(relates_to_actor_via(:owner)) + end + + # policy always() do + # authorize_if(always()) + # end + end + + identities do + identity(:unique_by_name, [:name]) + end + + attributes do + uuid_primary_key(:id, writable?: true) + attribute(:name, :string, public?: true) + end + + actions do + default_accept(:*) + + defaults([:create, :read, :update, :destroy]) + + read(:has_policies) + end + + postgres do + table "multitenant_orgs" + repo(AshPostgres.DevTestRepo) + + manage_tenant do + template(["org_", :id]) + end + end + + multitenancy do + strategy(:attribute) + attribute(:id) + global?(true) + parse_attribute({__MODULE__, :tenant, []}) + end + + aggregates do + count(:total_users_posts, [:users, :posts]) + count(:total_posts, :posts) + end + + relationships do + belongs_to :owner, AshPostgres.MultitenancyTest.User do + attribute_public?(false) + public?(false) + end + + has_many(:posts, AshPostgres.MultitenancyTest.Post, + destination_attribute: :org_id, + public?: true + ) + + has_many(:users, AshPostgres.MultitenancyTest.User, + destination_attribute: :org_id, + public?: true + ) + end + + def tenant("org_" <> tenant) do + tenant + end + + def tenant(tenant) do + tenant + end +end From 94bf17971a29ff96b6e497f9620c5d75c5e21974 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 26 May 2025 19:27:34 -0400 Subject: [PATCH 16/18] chore: update docs --- .../development/migrations-and-tasks.md | 10 +++++++++- .../migration_generator.ex | 16 ++++++++++++++++ .../tasks/ash_postgres.generate_migrations.ex | 5 +++-- mix.exs | 2 +- usage-rules.md | 19 +++++++++++++++++-- 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/documentation/topics/development/migrations-and-tasks.md b/documentation/topics/development/migrations-and-tasks.md index a9d6b951..d3bf453a 100644 --- a/documentation/topics/development/migrations-and-tasks.md +++ b/documentation/topics/development/migrations-and-tasks.md @@ -7,9 +7,17 @@ Ash comes with its own tasks, and AshPostgres exposes lower level tasks that you ## Basic Workflow - Make resource changes -- Run `mix ash.codegen --name add_a_combobulator` to generate migrations and resource snapshots +- Run `mix ash.codegen --dev` to generate a migration tagged as a `dev` migration. This will **run** the migrations. +- Make some more resource changes +- Run `mix ash.codegen add_a_combobulator` (use a good name for your feature) once you are all done, + to generate migrations and resource snapshots. This will **rollback** the dev migrations, and squash them into a + the new named migration (or sometimes migrations). - Run `mix ash.migrate` to run those migrations +The `--dev` workflow enables you to avoid having to think of a name for migrations while developing, and also enables some +upcoming workflows that will detect when code generation needs to be run on page load and will show you a button to generate +dev migrations and run them. + For more information on generating migrations, run `mix help ash_postgres.generate_migrations` (the underlying task that is called by `mix ash.migrate`) > ### list_tenants/0 {: .info} diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index ba725d64..0f825e2b 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -57,6 +57,22 @@ defmodule AshPostgres.MigrationGenerator do create_migrations(tenant_snapshots, opts, true, snapshots) Mix.shell().info("\nGenerating Migrations:") create_migrations(snapshots, opts, false) + + if opts[:dev] do + migrate_unless_test(repos) + end + end + + if Mix.env() == :test do + defp migrate_unless_test(_repos) do + :ok + end + else + defp migrate_unless_test(repos) do + Enum.each(repos, fn repo -> + Mix.Task.run("ash_postgres.migrate", ["--repo", repo]) + end) + end end defp find_repos do diff --git a/lib/mix/tasks/ash_postgres.generate_migrations.ex b/lib/mix/tasks/ash_postgres.generate_migrations.ex index 791f0703..e2cec94e 100644 --- a/lib/mix/tasks/ash_postgres.generate_migrations.ex +++ b/lib/mix/tasks/ash_postgres.generate_migrations.ex @@ -94,6 +94,7 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do tenant_migration_path: :string, quiet: :boolean, snapshots_only: :boolean, + auto_name: :boolean, name: :string, no_format: :boolean, dry_run: :boolean, @@ -112,10 +113,10 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do |> Keyword.delete(:no_format) |> Keyword.put_new(:name, name) - if !opts[:name] && !opts[:dry_run] && !opts[:check] && !opts[:snapshots_only] && !opts[:dev] do + if !opts[:name] && !opts[:dry_run] && !opts[:check] && !opts[:snapshots_only] && !opts[:dev] && + !opts[:auto_name] do IO.warn(""" Name must be provided when generating migrations, unless `--dry-run` or `--check` or `--dev` is also provided. - Using an autogenerated name will be deprecated in a future release. Please provide a name. for example: diff --git a/mix.exs b/mix.exs index 31424e80..fb84295b 100644 --- a/mix.exs +++ b/mix.exs @@ -240,7 +240,7 @@ defmodule AshPostgres.MixProject do format: "format --migrate", "spark.formatter": "spark.formatter --extensions AshPostgres.DataLayer", "spark.cheat_sheets": "spark.cheat_sheets --extensions AshPostgres.DataLayer", - "test.generate_migrations": "ash_postgres.generate_migrations", + "test.generate_migrations": "ash_postgres.generate_migrations --auto-name", "test.check_migrations": "ash_postgres.generate_migrations --check", "test.migrate_tenants": "ash_postgres.migrate --tenants", "test.migrate": "ash_postgres.migrate", diff --git a/usage-rules.md b/usage-rules.md index 5c8ee524..64ea4d57 100644 --- a/usage-rules.md +++ b/usage-rules.md @@ -149,14 +149,29 @@ end ## Migrations and Codegen -### Generating Migrations +### Development Migration Workflow (Recommended) -After creating or modifying Ash resources: +For development iterations, use the dev workflow to avoid naming migrations prematurely: + +1. Make resource changes +2. Run `mix ash.codegen --dev` to generate and run dev migrations +3. Continue making changes and running `mix ash.codegen --dev` as needed +4. When your feature is complete, run `mix ash.codegen add_feature_name` to generate final named migrations (this will rollback dev migrations and squash them) +5. Review the migrations +6. Run `mix ash.migrate` to apply the final migrations + +### Traditional Migration Generation + +For single-step changes or when you know the final feature name: 1. Run `mix ash.codegen add_feature_name` to generate migrations 2. Review the generated migrations in `priv/repo/migrations` 3. Run `mix ash.migrate` to apply the migrations +> **Tip**: The dev workflow (`--dev` flag) is preferred during development as it allows you to iterate without thinking of migration names and provides better development ergonomics. + +> **Warning**: Always review migrations before applying them to ensure they are correct and safe. + ## Multitenancy AshPostgres supports schema-based multitenancy: From 14e1a27af2c9a517658e40797eee727f0dfdb2c1 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 26 May 2025 19:43:22 -0400 Subject: [PATCH 17/18] chore: don't run migrations --- lib/migration_generator/migration_generator.ex | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 0f825e2b..ba725d64 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -57,22 +57,6 @@ defmodule AshPostgres.MigrationGenerator do create_migrations(tenant_snapshots, opts, true, snapshots) Mix.shell().info("\nGenerating Migrations:") create_migrations(snapshots, opts, false) - - if opts[:dev] do - migrate_unless_test(repos) - end - end - - if Mix.env() == :test do - defp migrate_unless_test(_repos) do - :ok - end - else - defp migrate_unless_test(repos) do - Enum.each(repos, fn repo -> - Mix.Task.run("ash_postgres.migrate", ["--repo", repo]) - end) - end end defp find_repos do From a7574d0c8ebd74b3302177ed82253bacc52de5ee Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 26 May 2025 20:34:20 -0400 Subject: [PATCH 18/18] fix: various fixes --- .../development/migrations-and-tasks.md | 9 +- .../migration_generator.ex | 66 ++++-- .../tasks/ash_postgres.generate_migrations.ex | 11 - test/dev_migrations_test.exs | 14 +- test/migration_generator_test.exs | 189 ++++++++++++------ test/mix_squash_snapshots_test.exs | 9 +- usage-rules.md | 8 +- 7 files changed, 198 insertions(+), 108 deletions(-) diff --git a/documentation/topics/development/migrations-and-tasks.md b/documentation/topics/development/migrations-and-tasks.md index d3bf453a..04fefc93 100644 --- a/documentation/topics/development/migrations-and-tasks.md +++ b/documentation/topics/development/migrations-and-tasks.md @@ -7,11 +7,10 @@ Ash comes with its own tasks, and AshPostgres exposes lower level tasks that you ## Basic Workflow - Make resource changes -- Run `mix ash.codegen --dev` to generate a migration tagged as a `dev` migration. This will **run** the migrations. -- Make some more resource changes -- Run `mix ash.codegen add_a_combobulator` (use a good name for your feature) once you are all done, - to generate migrations and resource snapshots. This will **rollback** the dev migrations, and squash them into a - the new named migration (or sometimes migrations). +- Run `mix ash.codegen --dev` to generate a migration tagged as a `dev` migration, which will later be squashed and does not require a name. +- Run `mix ash.migrate` to run the migrations. +- Make some more resource changes. +- Once you're all done, run `mix ash.codegen add_a_combobulator`, using a good name for your changes to generate migrations and resource snapshots. This will **rollback** the dev migrations, and squash them into a the new named migration (or sometimes migrations). - Run `mix ash.migrate` to run those migrations The `--dev` workflow enables you to avoid having to think of a name for migrations while developing, and also enables some diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index ba725d64..2479d067 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -21,6 +21,7 @@ defmodule AshPostgres.MigrationGenerator do check: false, dev: false, snapshots_only: false, + auto_name: false, dont_drop_columns: false def generate(domains, opts \\ []) do @@ -227,11 +228,13 @@ defmodule AshPostgres.MigrationGenerator do Mix.shell().info("No extensions to install") :ok else + dev = if opts.dev, do: "_dev" + {module, migration_name} = case to_install do [{ext_name, version, _up_fn, _down_fn}] -> {"install_#{ext_name}_v#{version}_#{timestamp(true)}", - "#{timestamp(true)}_install_#{ext_name}_v#{version}_extension"} + "#{timestamp(true)}_install_#{ext_name}_v#{version}_extension#{dev}"} ["ash_functions"] -> {"install_ash_functions_extension_#{AshPostgres.MigrationGenerator.AshFunctions.latest_version()}_#{timestamp(true)}", @@ -240,6 +243,8 @@ defmodule AshPostgres.MigrationGenerator do _multiple -> migration_path = migration_path(opts, repo, false) + require_name!(opts) + if opts.name do count = migration_path @@ -263,7 +268,7 @@ defmodule AshPostgres.MigrationGenerator do |> Kernel.+(1) {"#{opts.name}_extensions_#{count}", - "#{timestamp(true)}_#{opts.name}_extensions_#{count}"} + "#{timestamp(true)}_#{opts.name}_extensions_#{count}#{dev}"} else count = migration_path @@ -287,7 +292,7 @@ defmodule AshPostgres.MigrationGenerator do |> Kernel.+(1) {"migrate_resources_extensions_#{count}", - "#{timestamp(true)}_migrate_resources_extensions_#{count}"} + "#{timestamp(true)}_migrate_resources_extensions_#{count}#{dev}"} end end @@ -529,6 +534,21 @@ defmodule AshPostgres.MigrationGenerator do end end + defp require_name!(opts) do + if !opts.name && !opts.dry_run && !opts.check && !opts.snapshots_only && !opts.dev && + !opts.auto_name do + raise """ + Name must be provided when generating migrations, unless `--dry-run` or `--check` or `--dev` is also provided. + + Please provide a name. for example: + + mix ash_postgres.generate_migrations ...args + """ + end + + :ok + end + defp remove_dev_migrations(dev_migrations, tenant?, repo, opts) do dev_migrations = Enum.map(dev_migrations, fn migration -> @@ -540,10 +560,17 @@ defmodule AshPostgres.MigrationGenerator do if tenant? do with_repo_not_in_test(repo, fn repo -> for prefix <- repo.all_tenants() do + {repo, query, opts} = Ecto.Migration.SchemaMigration.versions(repo, [], prefix) + + versions = repo.all(query, opts) + dev_migrations |> Enum.map(&extract_migration_info/1) |> Enum.filter(& &1) |> Enum.map(&load_migration!/1) + |> Enum.filter(fn {version, _} -> + version in versions + end) |> Enum.each(fn {version, mod} -> Ecto.Migration.Runner.run( repo, @@ -563,11 +590,18 @@ defmodule AshPostgres.MigrationGenerator do end) else with_repo_not_in_test(repo, fn repo -> + {repo, query, opts} = Ecto.Migration.SchemaMigration.versions(repo, [], nil) + + versions = repo.all(query, opts) + dev_migrations |> Enum.map(&extract_migration_info/1) |> Enum.filter(& &1) |> Enum.map(&load_migration!/1) |> Enum.sort() + |> Enum.filter(fn {version, _} -> + version in versions + end) |> Enum.each(fn {version, mod} -> Ecto.Migration.Runner.run( repo, @@ -1074,6 +1108,8 @@ defmodule AshPostgres.MigrationGenerator do defp write_migration!({up, down}, repo, opts, tenant?, run_without_transaction?) do migration_path = migration_path(opts, repo, tenant?) + require_name!(opts) + {migration_name, last_part} = if opts.name do {"#{timestamp(true)}_#{opts.name}", "#{opts.name}"} @@ -3335,11 +3371,7 @@ defmodule AshPostgres.MigrationGenerator do scale -> scale end - cond do - precision && scale -> {:decimal, precision, scale} - precision -> {:decimal, precision} - true -> :decimal - end + {:decimal, precision, scale} end defp migration_type(other, constraints) do @@ -3558,7 +3590,7 @@ defmodule AshPostgres.MigrationGenerator do end defp sanitize_type(:decimal, _size, scale, precision) do - ["decimal", scale, precision] |> Enum.reject(&is_nil/1) + ["decimal", precision, scale] end defp sanitize_type(type, size, precision, decimal) when is_atom(type) and is_integer(size) do @@ -3665,11 +3697,11 @@ defmodule AshPostgres.MigrationGenerator do {other, size} when is_atom(other) and is_integer(size) -> {other, size, nil, nil} - {:decimal, scale} -> - {:decimal, scale, nil, nil} + {:decimal, precision} -> + {:decimal, nil, nil, precision} - {:decimal, scale, precision} -> - {:decimal, scale, precision, nil} + {:decimal, precision, scale} -> + {:decimal, nil, precision, scale} other -> {other, nil, nil, nil} @@ -3773,12 +3805,12 @@ defmodule AshPostgres.MigrationGenerator do {:binary, size} end - defp load_type(["decimal", scale]) do - {:decimal, scale} + defp load_type(["decimal", precision]) do + {:decimal, precision} end - defp load_type(["decimal", scale, precision]) do - {:decimal, scale, precision} + defp load_type(["decimal", precision, scale]) do + {:decimal, precision, scale} end defp load_type([string, size]) when is_binary(string) and is_integer(size) do diff --git a/lib/mix/tasks/ash_postgres.generate_migrations.ex b/lib/mix/tasks/ash_postgres.generate_migrations.ex index e2cec94e..8431572c 100644 --- a/lib/mix/tasks/ash_postgres.generate_migrations.ex +++ b/lib/mix/tasks/ash_postgres.generate_migrations.ex @@ -113,17 +113,6 @@ defmodule Mix.Tasks.AshPostgres.GenerateMigrations do |> Keyword.delete(:no_format) |> Keyword.put_new(:name, name) - if !opts[:name] && !opts[:dry_run] && !opts[:check] && !opts[:snapshots_only] && !opts[:dev] && - !opts[:auto_name] do - IO.warn(""" - Name must be provided when generating migrations, unless `--dry-run` or `--check` or `--dev` is also provided. - - Please provide a name. for example: - - mix ash_postgres.generate_migrations #{Enum.join(args, " ")} - """) - end - AshPostgres.MigrationGenerator.generate(domains, opts) end end diff --git a/test/dev_migrations_test.exs b/test/dev_migrations_test.exs index f5d7b2f2..475aa909 100644 --- a/test/dev_migrations_test.exs +++ b/test/dev_migrations_test.exs @@ -76,7 +76,7 @@ defmodule AshPostgres.DevMigrationsTest do end end - setup_all do + setup do resource_dev_path = "priv/resource_snapshots/dev_test_repo" initial_resource_files = @@ -137,7 +137,8 @@ defmodule AshPostgres.DevMigrationsTest do AshPostgres.MigrationGenerator.generate(Domain, snapshot_path: "priv/resource_snapshots", migration_path: "priv/dev_test_repo/migrations", - dev: true + dev: true, + auto_name: true ) assert [_extensions, migration, _migration] = @@ -147,7 +148,8 @@ defmodule AshPostgres.DevMigrationsTest do AshPostgres.MigrationGenerator.generate(Domain, snapshot_path: "priv/resource_snapshots", - migration_path: "priv/dev_test_repo/migrations" + migration_path: "priv/dev_test_repo/migrations", + auto_name: true ) assert capture_log(fn -> migrate(migration) end) =~ "create table posts" @@ -173,7 +175,8 @@ defmodule AshPostgres.DevMigrationsTest do snapshot_path: "priv/resource_snapshots", migration_path: "priv/dev_test_repo/migrations", tenant_migration_path: "priv/dev_test_repo/tenant_migrations", - dev: true + dev: true, + auto_name: true ) org = @@ -196,7 +199,8 @@ defmodule AshPostgres.DevMigrationsTest do AshPostgres.MigrationGenerator.generate(Domain, snapshot_path: "priv/resource_snapshots", migration_path: "priv/dev_test_repo/migrations", - tenant_migration_path: "priv/dev_test_repo/tenant_migrations" + tenant_migration_path: "priv/dev_test_repo/tenant_migrations", + auto_name: true ) assert [_tenant_migration] = diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index a1168ccb..6a4d0ef5 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -147,7 +147,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok @@ -247,7 +248,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok @@ -326,7 +328,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) end @@ -371,7 +374,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) end @@ -414,7 +418,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) end @@ -440,7 +445,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -493,7 +499,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok @@ -518,7 +525,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -552,7 +560,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, _file2, file3] = @@ -605,7 +614,8 @@ defmodule AshPostgres.MigrationGeneratorTest do migration_path: "test_migration_path", tenant_migration_path: "test_tenant_migration_path", quiet: false, - format: false + format: false, + auto_name: true ) defposts do @@ -632,7 +642,8 @@ defmodule AshPostgres.MigrationGeneratorTest do migration_path: "test_migration_path", tenant_migration_path: "test_tenant_migration_path", quiet: false, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -675,7 +686,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok @@ -701,7 +713,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -731,7 +744,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -774,7 +788,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok @@ -805,7 +820,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1] = @@ -838,7 +854,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok @@ -864,7 +881,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -897,7 +915,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -930,7 +949,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -966,7 +986,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -993,7 +1014,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -1019,7 +1041,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -1048,7 +1071,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -1083,7 +1107,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -1156,7 +1181,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -1189,7 +1215,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -1232,7 +1259,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file1, file2] = @@ -1277,7 +1305,8 @@ defmodule AshPostgres.MigrationGeneratorTest do migration_path: "test_migration_path", quiet: true, concurrent_indexes: true, - format: false + format: false, + auto_name: true ) assert [_file1, _file2, file3] = @@ -1316,7 +1345,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -1356,7 +1386,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok @@ -1394,7 +1425,8 @@ defmodule AshPostgres.MigrationGeneratorTest do AshPostgres.MigrationGenerator.generate(domain, snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", - check: true + check: true, + auto_name: true ) ) == {:shutdown, 1} @@ -1437,7 +1469,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -1474,7 +1507,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -1520,7 +1554,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -1566,7 +1601,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -1617,7 +1653,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) defposts Post2 do @@ -1646,7 +1683,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert file = @@ -1727,7 +1765,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -1781,7 +1820,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert file = @@ -1863,7 +1903,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -1918,7 +1959,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -1957,7 +1999,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) defposts Post2 do @@ -1983,7 +2026,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert file = @@ -2089,7 +2133,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -2169,7 +2214,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -2209,7 +2255,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert file = @@ -2242,7 +2289,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert file = @@ -2288,7 +2336,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert file = @@ -2323,7 +2372,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) defposts do @@ -2337,7 +2387,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert file = @@ -2414,7 +2465,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) [domain: Domain] @@ -2471,7 +2523,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file1] = @@ -2527,7 +2580,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) end) @@ -2576,7 +2630,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok @@ -2614,7 +2669,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [_file1, file2] = @@ -2679,7 +2735,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -2722,7 +2779,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) # Now update the precision and scale @@ -2747,7 +2805,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) migration_files = @@ -2791,7 +2850,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = @@ -2835,7 +2895,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) # Now change to arbitrary precision and scale @@ -2865,7 +2926,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) migration_files = @@ -2943,7 +3005,8 @@ defmodule AshPostgres.MigrationGeneratorTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) assert [file] = diff --git a/test/mix_squash_snapshots_test.exs b/test/mix_squash_snapshots_test.exs index f2827228..dcdce819 100644 --- a/test/mix_squash_snapshots_test.exs +++ b/test/mix_squash_snapshots_test.exs @@ -94,7 +94,8 @@ defmodule AshPostgres.MixSquashSnapshotsTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) defposts do @@ -113,7 +114,8 @@ defmodule AshPostgres.MixSquashSnapshotsTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok @@ -179,7 +181,8 @@ defmodule AshPostgres.MixSquashSnapshotsTest do snapshot_path: "test_snapshots_path", migration_path: "test_migration_path", quiet: true, - format: false + format: false, + auto_name: true ) :ok diff --git a/usage-rules.md b/usage-rules.md index 64ea4d57..632fc52d 100644 --- a/usage-rules.md +++ b/usage-rules.md @@ -155,10 +155,10 @@ For development iterations, use the dev workflow to avoid naming migrations prem 1. Make resource changes 2. Run `mix ash.codegen --dev` to generate and run dev migrations -3. Continue making changes and running `mix ash.codegen --dev` as needed -4. When your feature is complete, run `mix ash.codegen add_feature_name` to generate final named migrations (this will rollback dev migrations and squash them) -5. Review the migrations -6. Run `mix ash.migrate` to apply the final migrations +3. Review the migrations and run `mix ash.migrate` to run them +4. Continue making changes and running `mix ash.codegen --dev` as needed +5. When your feature is complete, run `mix ash.codegen add_feature_name` to generate final named migrations (this will rollback dev migrations and squash them) +3. Review the migrations and run `mix ash.migrate` to run them ### Traditional Migration Generation