Multi tenant signup testing with Ecto 3

Hello fellow alchemists,

I’m attempting an upgrade of a multi-tenant application from ecto 2.2.11 to ecto 3.
I’m facing an issue with the Ecto.Migrator.run function:

https://hexdocs.pm/ecto_sql/Ecto.Migrator.html#run/4-execution-model

[…] migrations cannot run dynamically during test under the Ecto.Adapters.SQL.Sandbox , as the sandbox has to share a single connection across processes to guarantee the changes can be reverted.

I was previously able to run migrations during a test and this allowed me to test from end to end a signup registration.
What would be a good alternative to using Ecto.Migrator from the tests ?

Hello again!

For whoever has a similar issue, the solution for me was to create a second repo dedicated to running the migrations on test:

I struggled a lot with the settings queue_target and queue_interval as reported by my stacktraces, but in the end, I was mislead as I simply forgot to start the new repo.

In my test_helper, I now have:

defmodule Datastore.MigrationRepo do
  use Ecto.Repo, [
    adapter: Ecto.Adapters.Postgres,
    otp_app: :datastore,
    migration_lock: nil
  ]
end
Datastore.MigrationRepo.start_link

At the beginning, I used this repo only for testing, then saw benefits in having it in production as well, in my codebase.

With Ecto3, my migrator module has lost lots of LoC and now looks like this:

defmodule Datastore.Migrator do
  @moduledoc false

  def run(prefix, repo) do
    migrations_path = tenant_migrations_path()

    opts =
      add_prefix_option( [all: true, log: :info, log_sql: :info ], prefix)

    pool = repo.config[:pool]

    _migrated =
      if function_exported?(pool, :unboxed_run, 2) do
        pool.unboxed_run(repo, fn ->
          Ecto.Migrator.run(repo, migrations_path, :up, opts)
        end)
      else
        Ecto.Migrator.run(repo, migrations_path, :up, opts)
      end
  end

  defp add_prefix_option(opts, "public"), do: opts

  defp add_prefix_option(opts, prefix) do
    Keyword.merge(opts, prefix: prefix)
  end

  # My migrations live in Repo, but could be moved to
  # MigrationRepo.
  def tenant_migrations_path() do
    Path.join(build_repo_priv(Datastore.Repo), "migrations")
  end

  def build_repo_priv(repo) do
    Application.app_dir(
      Keyword.fetch!(repo.config(), :otp_app),
      source_repo_priv(repo)
    )
  end

  def source_repo_priv(repo) do
    repo.config()[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}"
  end
end

Cheers!

6 Likes