Ah ok, that makes sense.
So if I’m understanding correctly, it’s not necessarily that you need a particular repo module to be able to swap adapters at the drop of a hat, or for different use-cases running in the same VM, but that you need the ability to avoid the maintenance headache of never being able to just use MyApp.Repo
, and not even put_dynamic_repo
because if they’re different adapters, they’re likely different modules. And even if you did ship different releases for different DB adapters, you might be dealing with constant confusion.
If shipping a my_app_pg.tar
and my_app_sqlite3.tar
would be too much of a pain, then I’d look at the Ecto.Repo
behaviour itself and just write a thing shim implementation that immediately calls out persistent_term
to get the “true” repo module, and delegate to that.
Then in your MyApp.Application.start/2
, or in runtime.exs
the very first thing it should do is determine which adapter is actually going to get started, and start that one.
I have no idea if that will work though.
Here’s some example code that adapted from the a branch of double
I had been working on that builds shims.
This is really hacked together and could be vastly simplified/condensed, I just copy-pasted and changed some bits.
A word of caution though: This is a BAD idea to do it this way. You will constantly be debugging things, especially since Ecto.Repos expect that they can use their own module name to find the current dynamic_repo
.
I originally built these macros to facilitate debugging badly behaving macro-laden/auto-generated libraries, or overgrown domain modules by allowing the ability to inject a spy module that you could configure and introspect as part of a test, and was only meant to exist in the codebase long enough for a person to guide them towards proper abstractions.
I REALLY suggest you just have multiple tarballs called my_app_minimal.tar
and my_app_full.tar
. You’ll pay more in CI/CD from duplicate test runs, but it’s a level of mental load the devs won’t have to constantly be aware of.
defmodule MyApp.Application do
def start(_type, _args) do
# determine which repo needs to be started
repo_mod = MyApp.RepoPicker.starting_repo!()
:persistent_term.put({MyApp.ShimRepo, :repo}, repo_mod)
children = [
...,
repo_mod,
# other stuff, but NOT MyApp.ShimRepo
]
end
end
defmodule MyApp.RepoPicker do
@default_impl_repo_mod Application.compile_env(:my_app, [__MODULE__, :default_impl], MyApp.SqliteRepo)
@other_repo_mods Application.compile_env(:my_app, [__MODULE__, :others], [MyApp.PgRepo])
import RepoShimmer
defshim(MyApp.ShimRepo, for: [default_impl_repo_mod | @other_repo_mods])
# set in runtime.exs, pulled from a ENV VAR or something, idk
def starting_repo!, do: Application.fetch_env(__MODULE__, :starting_repo)
end
defmodule RepoShimmer do
defmacro defshim(alias, opts \\ []) do
sources = Keyword.fetch!(opts, :for)
env = __CALLER__
expanded = Macro.expand(alias, env)
sources = for s <- sources, do: Macro.expand(s, env)
default_source = #... idk, the first one, or you could grab it from opts? idk, doesn't matter
func_defs = generate_function_defs(expanded, default_source)
Module.create(expanded, func_defs, Macro.Env.location(__ENV__))
end
def repo_from_persistent_term(shim_mod), do: :persistent_term.get({shim_mod, :repo})
defp generate_function_defs(mod_name, source, other_funcs \\ []) do
funcs = Enum.uniq(nonprotected_functions(source) ++ other_funcs)
for {func_name, arity} <- funcs do
generate_function_def(mod_name, func_name, arity)
end
end
defp generate_function_def(mod, func_name, arity) do
args = Macro.generate_arguments(arity, mod)
quote do
def unquote(func_name)(unquote_splicing(args)) do
repo_mod = RepoShimmer.repo_from_persistent_term(unquote(mod))
apply(repo_mod, unquote(func_name), [unquote_splicing(args)])
end
end
end
defp nonprotected_functions(mod) do
mod.module_info(:functions)
|> Enum.reject(fn {k, _} ->
[:__info__, :module_info] |> Enum.member?(k) ||
String.starts_with?("#{k}", "_") ||
String.starts_with?("#{k}", "-")
end)
end
end