From efc60cd93241a5e404bb3e240d0a8cedace19646 Mon Sep 17 00:00:00 2001 From: Ryan Lyman Date: Thu, 14 May 2026 11:58:59 -0600 Subject: [PATCH 1/3] fix: handle removal of :context multitenancy in migrations --- .../migration_generator.ex | 94 ++++++++++++++----- 1 file changed, 71 insertions(+), 23 deletions(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 1177760a..e75a875b 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -3109,32 +3109,22 @@ defmodule AshPostgres.MigrationGenerator do not is_nil(Map.get(attribute, :references)) and !(attribute.references.multitenancy && attribute.references.multitenancy.strategy == :context && - (is_nil(multitenancy) || multitenancy.strategy == :attribute)) + (is_nil(multitenancy) || multitenancy.strategy == :attribute)) and + !(multitenancy && multitenancy.strategy == :context && + attribute.references.multitenancy && + attribute.references.multitenancy.strategy == :context) end - def get_existing_snapshot(snapshot, opts) do - folder = get_snapshot_folder(snapshot, opts) - snapshot_path = get_snapshot_path(snapshot, folder) - - if File.exists?(snapshot_path) do - snapshot_path - |> File.ls!() - |> 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) - - snapshot_files -> - snapshot_path - |> Path.join(Enum.max(snapshot_files)) - |> File.read!() - |> load_snapshot() - end + defp get_alternate_snapshot_folder(snapshot, opts) do + if snapshot.multitenancy.strategy == :context do + opts + |> snapshot_path(snapshot.repo) + |> Path.join(repo_name(snapshot.repo)) else - get_old_snapshot(folder, snapshot) + opts + |> snapshot_path(snapshot.repo) + |> Path.join(repo_name(snapshot.repo)) + |> Path.join("tenants") end end @@ -3151,6 +3141,64 @@ defmodule AshPostgres.MigrationGenerator do end end + def get_existing_snapshot(snapshot, opts) do + folder = get_snapshot_folder(snapshot, opts) + snapshot_path = get_snapshot_path(snapshot, folder) + + result = + if File.exists?(snapshot_path) do + snapshot_path + |> File.ls!() + |> 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) + + snapshot_files -> + snapshot_path + |> Path.join(Enum.max(snapshot_files)) + |> File.read!() + |> load_snapshot() + end + else + get_old_snapshot(folder, snapshot) + end + + # If no snapshot was found in the primary folder, check the alternate folder. + # This handles the case where multitenancy strategy has changed (e.g. :context + # to nil/global), which causes the snapshot to be stored in a different folder. + if is_nil(result) do + alternate_folder = get_alternate_snapshot_folder(snapshot, opts) + alternate_path = get_snapshot_path(snapshot, alternate_folder) + + if File.exists?(alternate_path) do + alternate_path + |> File.ls!() + |> 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(alternate_folder, snapshot) + + snapshot_files -> + alternate_path + |> Path.join(Enum.max(snapshot_files)) + |> File.read!() + |> load_snapshot() + end + else + get_old_snapshot(alternate_folder, snapshot) + end + else + result + end + end + defp get_snapshot_path(snapshot, folder) do if snapshot.schema do schema_dir = Path.join(folder, "#{snapshot.schema}.#{snapshot.table}") From e63751e26a76298ab0753633c3c020dc24991841 Mon Sep 17 00:00:00 2001 From: Ryan Lyman Date: Thu, 14 May 2026 13:34:22 -0600 Subject: [PATCH 2/3] fix: handle removal of :context multitenancy in migrations --- .../migration_generator.ex | 95 +++++++------------ 1 file changed, 36 insertions(+), 59 deletions(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index e75a875b..84c07b47 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -3115,85 +3115,62 @@ defmodule AshPostgres.MigrationGenerator do attribute.references.multitenancy.strategy == :context) end - defp get_alternate_snapshot_folder(snapshot, opts) do - if snapshot.multitenancy.strategy == :context do + defp get_snapshot_folder(snapshot, opts) do + base = opts |> snapshot_path(snapshot.repo) |> Path.join(repo_name(snapshot.repo)) + + if snapshot.multitenancy.strategy == :context do + Path.join(base, "tenants") else - opts - |> snapshot_path(snapshot.repo) - |> Path.join(repo_name(snapshot.repo)) - |> Path.join("tenants") + base end end - defp get_snapshot_folder(snapshot, opts) do - if snapshot.multitenancy.strategy == :context do + defp get_alternate_snapshot_folder(snapshot, opts) do + base = opts |> snapshot_path(snapshot.repo) |> Path.join(repo_name(snapshot.repo)) - |> Path.join("tenants") + + if snapshot.multitenancy.strategy == :context do + base else - opts - |> snapshot_path(snapshot.repo) - |> Path.join(repo_name(snapshot.repo)) + Path.join(base, "tenants") end end - def get_existing_snapshot(snapshot, opts) do - folder = get_snapshot_folder(snapshot, opts) + defp load_snapshot_from_folder(folder, snapshot, opts) do snapshot_path = get_snapshot_path(snapshot, folder) - result = - if File.exists?(snapshot_path) do - snapshot_path - |> File.ls!() - |> 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) + if File.exists?(snapshot_path) do + snapshot_path + |> File.ls!() + |> 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) - snapshot_files -> - snapshot_path - |> Path.join(Enum.max(snapshot_files)) - |> File.read!() - |> load_snapshot() - end - else - get_old_snapshot(folder, snapshot) + snapshot_files -> + snapshot_path + |> Path.join(Enum.max(snapshot_files)) + |> File.read!() + |> load_snapshot() end + else + get_old_snapshot(folder, snapshot) + end + end - # If no snapshot was found in the primary folder, check the alternate folder. - # This handles the case where multitenancy strategy has changed (e.g. :context - # to nil/global), which causes the snapshot to be stored in a different folder. - if is_nil(result) do - alternate_folder = get_alternate_snapshot_folder(snapshot, opts) - alternate_path = get_snapshot_path(snapshot, alternate_folder) - - if File.exists?(alternate_path) do - alternate_path - |> File.ls!() - |> 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(alternate_folder, snapshot) + def get_existing_snapshot(snapshot, opts) do + result = load_snapshot_from_folder(get_snapshot_folder(snapshot, opts), snapshot, opts) - snapshot_files -> - alternate_path - |> Path.join(Enum.max(snapshot_files)) - |> File.read!() - |> load_snapshot() - end - else - get_old_snapshot(alternate_folder, snapshot) - end + if is_nil(result) do + load_snapshot_from_folder(get_alternate_snapshot_folder(snapshot, opts), snapshot, opts) else result end From df70b395758db3d5b371aa2b6ac618181ca18a6d Mon Sep 17 00:00:00 2001 From: Ryan Lyman Date: Tue, 19 May 2026 13:21:07 -0600 Subject: [PATCH 3/3] fix: handle removal of :context multitenancy in migrations test --- test/migration_generator_test.exs | 84 +++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index c1c939b6..7b40fb89 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -1264,6 +1264,90 @@ defmodule AshPostgres.MigrationGeneratorTest do end end + describe "removing :context multitenancy" do + setup %{snapshot_path: snapshot_path, migration_path: migration_path} do + defposts do + multitenancy do + strategy(:context) + end + + attributes do + uuid_primary_key(:id) + attribute(:title, :string, public?: true) + end + end + + defcomments do + multitenancy do + strategy(:context) + end + + attributes do + uuid_primary_key(:id) + attribute(:post_id, :uuid, public?: true) + end + + relationships do + belongs_to(:post, Post, public?: true) + end + end + + defdomain([Post, Comment]) + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: snapshot_path, + migration_path: migration_path, + quiet: true, + format: false, + auto_name: true + ) + + :ok + end + + test "when removing :context multitenancy, no invalid drop constraint is generated", %{ + snapshot_path: snapshot_path, + migration_path: migration_path + } do + defposts do + attributes do + uuid_primary_key(:id) + attribute(:title, :string, public?: true) + end + end + + defcomments do + attributes do + uuid_primary_key(:id) + attribute(:post_id, :uuid, public?: true) + end + + relationships do + belongs_to(:post, Post, public?: true) + end + end + + defdomain([Post, Comment]) + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: snapshot_path, + migration_path: migration_path, + quiet: true, + format: false, + auto_name: true + ) + + migration_files = Enum.sort(Path.wildcard("#{migration_path}/**/*_migrate_resources*.exs")) |> Enum.reject(&String.contains?(&1, "extensions")) + + all_contents = Enum.map_join(migration_files, "", &File.read!/1) + + [up_contents] =Regex.run(~r/def up do(.+)def down do/s, all_contents, capture: :all_but_first) + + + refute up_contents =~ ~S{drop constraint(:comments, "comments_post_id_fkey")} + end + end + describe "creating follow up migrations" do setup %{snapshot_path: snapshot_path, migration_path: migration_path} do :ok