Current section
Files
Jump to
Current section
Files
lib/botanist.ex
defmodule Botanist do
@moduledoc """
Botanist is a seeding library which uses [Ecto](https://github.com/elixir-ecto/ecto). Its intended purpose
is for seeding a database in a safe and atomic manner.
"""
@doc """
Macro for seeding the database. No seed can be run more than once. If extra data is to be added or
removed, a new seed must be generated with `mix ecto.gen.seed` or use `perennial_seed/1`.
A seed must be encased in a function named `planter` to be run. Only a single seed can fit into a
planter.
### Example
```elixir
import Botanist
alias MyApp.Repo
alias MyApp.User
def planter do
seed do
Repo.insert(%User{email: "email@gmail.com", name: "John Smith"})
end
end
```
If at any point an error is thrown or raised or the `seed` returns `{:error, error_msg}`, the seed
will be listed as failed. In order for the seed to take root, it will need to be corrected and reran.
As the seed takes place in a transaction, it is not possible for a seed to run partially.
"""
defmacro seed(do: block) do
quote do
alias unquote(Mix.Botanist.repo())
alias Botanist.Seed
alias Ecto.Changeset
alias Botanist.Migrations.CreateSeedTable
import Ecto.Query
require Logger
Seed.ensure_seed_table!(Repo)
seed_name = Path.basename(__ENV__.file, ".exs")
seeds =
from(
s in Seed,
where: s.name == ^seed_name
)
|> Repo.all()
cond do
length(seeds) > 0 ->
{:repeat, "The seed #{seed_name} has already run."}
true ->
case Repo.transaction(fn ->
case unquote(block) do
{:error, error} -> Repo.rollback(error)
_ = output -> output
end
end) do
{:ok, out} ->
Repo.insert(%Seed{name: seed_name, inserted_at: NaiveDateTime.utc_now()})
{:ok, out}
{:error, error} ->
Logger.error("Error occurred #{error}")
{:error, error}
other ->
other
end
end
end
end
@doc """
Macro for seeds which can be run recurrently. Functionally equivilant to `seed/1` in terms of atomicity and robustness
but will run every time `mix ecto.seed` is called rather than once.
Unlike `seed/1`, multiple `perennial_seed` can be planted in a single `planter` function.
### Example
```elixir
import Botanist
def planter do
perennial_seed do
# Recurrent work here
end
end
```
"""
defmacro perennial_seed(do: block) do
quote do
alias unquote(Mix.Botanist.repo())
require Logger
seed_name = Path.basename(__ENV__.file, ".exs")
case Repo.transaction(
fn ->
case unquote(block) do
{:error, error} -> Repo.rollback(error)
_ = output -> output
end
end,
timeout: :infinity,
pool_timeout: :infinity
) do
{:ok, out} ->
out
{:error, error} ->
Logger.error("Error occurred #{error}")
{:error, error}
other ->
other
end
end
end
end