Skip to content

Commit

Permalink
no supervisors (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
visciang authored Feb 17, 2022
1 parent b519bff commit ec7865b
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 260 deletions.
42 changes: 4 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ res = Postgrex.query!(@postgrex_conn, q, "select x, y from table")

## Exql.Migration

A is a minimalist executor for Postgres schema migration scripts.
A minimalist executor for Postgres schema migration scripts.

Define your ordered list of SQL migrations under `priv/migrations/*.sql` and add `Exql.Migration` to you app supervisor.
The migration task will execute the `*.sql` scripts not already applied to the target DB.
Expand All @@ -31,47 +31,13 @@ in a transaction and acquire a `'LOCK ... SHARE MODE'` ensuring that one and onl

### Usage

In your app supervisor, start `Postgrex` and then the `Exql.Migration`.
In your application you can call the `Exql.Migration.create_db` and `Exql.Migration.migrate` functions:

```elixir
migrations_dir = "priv/migrations"

postgres_conn = :db
postgres_conf = [
name: postgres_conn,
hostname: "localhost",
username: "postgres",
password: "postgres",
database: "postgres",
pool_size: 5
]

children = [
{Postgrex, postgres_conf},
{Exql.Migration, [
db_conn: postgres_conn,
migrations_dir: migrations_dir,
timeout: 5_000, # default :infinity
transactional: true # default true, if false You know What you are doing
]}
]

opts = [strategy: :one_for_one]
Supervisor.start_link(children, opts)
Exql.Migration.create_db(postgres_credentials, "db_name")
Exql.Migration.migrate(mydb_credentials, "priv/migrations/db_name")
```

if you have multiple DBs to setup:

```elixir
children = [
Supervisor.child_spec({Postgrex, db_A_conf}, id: :postgrex_A),
Supervisor.child_spec({Exql.Migration, [db_conn: db_A_conn, migrations_dir: db_A_migrations_dir]}, id: :exql_db_A),
Supervisor.child_spec({Postgrex, db_B_conf}, id: :postgrex_B),
Supervisor.child_spec({Exql.Migration, [db_conn: db_B_conn, migrations_dir: db_B_migrations_dir]}, id: :exql_db_B)
]
```

`Exql.CreateDB` can be included in the supervision tree to create (if not exists) a database.
Check the sample app under `./sample_app` for more details.

# Development
Expand Down
47 changes: 0 additions & 47 deletions lib/exql/create_db.ex

This file was deleted.

145 changes: 120 additions & 25 deletions lib/exql/migration.ex
Original file line number Diff line number Diff line change
@@ -1,43 +1,138 @@
# coveralls-ignore-start

defmodule Exql.Migration.Migration do
@moduledoc "Migration runner Supervisor"

use Supervisor
defmodule Exql.Migration do
@moduledoc false

require Logger
alias Exql.Migration.{Log, Schema}

@type opts :: [
{:conn, Postgrex.conn()}
| {:migrations_dir, Path.t()}
| {:timeout, timeout()}
| {:transactional, boolean()}
@type credentials :: [
hostname: String.t(),
username: String.t(),
password: String.t(),
database: String.t()
]

@spec start_link(opts()) :: Supervisor.on_start()
def start_link(opts) do
conn = Keyword.fetch!(opts, :conn)
migrations_dir = Keyword.fetch!(opts, :migrations_dir)
timeout = Keyword.get(opts, :timeout, :infinity)
transactional = Keyword.get(opts, :transactional, true)
@spec create_db(credentials(), String.t()) :: :ok
def create_db(credentials, db_name) do
start_options = Keyword.put(credentials, :pool_size, 1)
{:ok, conn} = Postgrex.start_link(start_options)

unless exists_db?(conn, db_name) do
case Postgrex.query(conn, "create database #{db_name}", []) do
{:ok, _} ->
Logger.info("Created DB #{inspect(db_name)}")

# coveralls-ignore-start
{:error, %Postgrex.Error{postgres: %{code: :duplicate_database}}} ->
# if two instances of the app runs concurrently, exists_db? + query!
# could raise since we do not acquire locks and execute these two
# operations transactionally (create database can't run in a transaction)
:ok

# coveralls-ignore-end
end
end

GenServer.stop(conn)
rescue
exc ->
# coveralls-ignore-start
Logger.emergency("Create DB failed")
Logger.emergency(Exception.message(exc))
Logger.emergency("#{inspect(exc)}")

Supervisor.start_link(__MODULE__, {conn, migrations_dir, timeout, transactional})
:init.stop()
# coveralls-ignore-end
end

@impl Supervisor
@spec init({Postgrex.conn(), Path.t(), timeout(), boolean()}) :: :ignore
def init({conn, migrations_dir, timeout, transactional}) do
Exql.Migration.migrate(conn, migrations_dir, timeout, transactional)
@spec migrate(credentials(), Path.t(), boolean(), timeout()) :: :ok
def migrate(credentials, migrations_dir, transactional \\ true, timeout \\ :infinity) do
start_options = Keyword.put(credentials, :pool_size, 1)
{:ok, conn} = Postgrex.start_link(start_options)

Schema.setup(conn)

:ignore
Logger.info("Migration started (dir: #{inspect(migrations_dir)})")

migrations_dir
|> migration_files()
|> filter_migrations(Log.last_migration(conn))
|> sort_migrations()
|> Enum.each(&run(conn, &1, File.read!(Path.join(migrations_dir, &1)), timeout, transactional))

Logger.info("Migration completed")

GenServer.stop(conn)
rescue
exc ->
# coveralls-ignore-start
Logger.emergency("Migration failed")
Logger.emergency(Exception.message(exc))
Logger.emergency("#{inspect(exc)}")

:init.stop()
# coveralls-ignore-end
end
end

# coveralls-ignore-stop
@spec run(Postgrex.conn(), Log.migration_id(), String.t(), timeout(), boolean()) :: :ok
defp run(conn, migration_id, statements, timeout, transactional) do
Logger.info("[#{migration_id}] Running")

statements = """
do $$
begin
#{statements}
end $$
"""

if transactional do
Postgrex.transaction(
conn,
fn conn ->
Log.lock(conn)
run_statements(conn, migration_id, statements, :infinity)
end,
timeout: timeout
)
else
run_statements(conn, migration_id, statements, timeout)
end

Logger.info("[#{migration_id}] Completed")

:ok
end

@spec run_statements(Postgrex.conn(), Log.migration_id(), String.t(), timeout()) :: :ok
defp run_statements(conn, migration_id, statements, timeout) do
Logger.debug(statements)
Postgrex.query!(conn, statements, [], timeout: timeout)
shasum = :crypto.hash(:sha256, statements) |> Base.encode16(case: :lower)
Log.insert(conn, migration_id, shasum)
:ok
end

@spec migration_files(Path.t()) :: [String.t()]
defp migration_files(migrations_dir) do
migrations_dir
|> File.ls!()
|> Enum.filter(&String.ends_with?(&1, ".sql"))
end

@spec filter_migrations([String.t()], Log.migration_id()) :: [String.t()]
defp filter_migrations(migrations, last_migration),
do: Enum.reject(migrations, &applied?(&1, last_migration))

@spec sort_migrations([String.t()]) :: [String.t()]
defp sort_migrations(migrations),
do: Enum.sort(migrations)

@spec applied?(Log.migration_id(), nil | Log.migration_id()) :: boolean()
defp applied?(migration, last_applied),
do: migration <= last_applied

@spec exists_db?(Postgrex.conn(), String.t()) :: boolean()
defp exists_db?(conn, name) do
res = Postgrex.query!(conn, "select true from pg_database where datname = $1", [name])
match?(%Postgrex.Result{num_rows: 1}, res)
end
end
File renamed without changes.
File renamed without changes.
File renamed without changes.
100 changes: 0 additions & 100 deletions lib/exql_migration.ex

This file was deleted.

11 changes: 5 additions & 6 deletions sample_app/lib/example.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ defmodule Example do
postgres_credentials = Application.fetch_env!(:example, :postgres_credentials)
mydb_conf = Application.fetch_env!(:example, :db_mydb)

Exql.Migration.create_db(postgres_credentials, mydb_conf[:database])
Exql.Migration.migrate(mydb_conf, mydb_conf[:migration_dir])

children = [
# create "mydb", connecting to "postgres" database
{Exql.Migration.CreateDB, [credentials: postgres_credentials, db_name: mydb_conf[:database]]},
# database "mydb" connection pool
Supervisor.child_spec({Postgrex, mydb_conf}, id: :db_mydb),
# database "mydb" schema migrations
{Exql.Migration.Migration, [conn: mydb_conf[:name], migrations_dir: mydb_conf[:migration_dir]]}
{Postgrex, mydb_conf}
# ...
]

opts = [strategy: :one_for_one]
Expand Down
Loading

0 comments on commit ec7865b

Please sign in to comment.