Skip to content

Commit

Permalink
Merge pull request #1 from visciang/exql
Browse files Browse the repository at this point in the history
Extensions
  • Loading branch information
visciang authored Nov 24, 2021
2 parents dd5a18f + 0f7a338 commit ce06138
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 50 deletions.
40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
# ExqlMigration
# Exql

![CI](https://github.com/visciang/exql_migration/workflows/CI/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/visciang/exql_migration/badge.svg?branch=master)](https://coveralls.io/github/visciang/exql_migration?branch=master)
![CI](https://github.com/visciang/exql/workflows/CI/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/visciang/exql/badge.svg?branch=master)](https://coveralls.io/github/visciang/exql?branch=master)

`exql_migration` is a minimalist executor for Postgres schema migration scripts.
Few little things to work directly with `Postgrex`.

Good for those who:
## Exql.Query

> No Ecto, just Postgrex please!
> No down script, we go only up!
### Results as a list of maps

Define your ordered list of SQL migrations under `priv/migrations/*.sql` and add `ExqlMigration.Migration` to you app supervisor.
```elixir
res = Postgrex.query!(@postgrex_conn, q, "select x, y from table")
[%{"x" => x, "y" => y}, ...] = Query.result(res)
```

## Exql.Migration

A is 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.
The execution order follows the scripts filename alphabetic order.

If a migration script fails, the `ExqlMigration.Migration` executor stops the application.
If a migration script fails, the `Exql.Migration` executor stops the application.

## Multi instance deployment safety
### Multi instance deployment safety

If you have n instances of your app deployed, each of them can safely run the migration task since every migration runs
in a transaction and acquire a `'LOCK ... SHARE MODE'` ensuring that one and only migration execution can run at a time.

## Usage
### Usage

In your app supervisor, start `Postgrex` and then the `ExqlMigration.Migration`.
In your app supervisor, start `Postgrex` and then the `Exql.Migration`.

```elixir
migrations_dir = "priv/migrations"
Expand All @@ -40,7 +48,7 @@ postgres_conf = [

children = [
{Postgrex, postgres_conf},
{ExqlMigration.Migration, [
{Exql.Migration, [
db_conn: postgres_conn,
migrations_dir: migrations_dir,
timeout: 5_000, # default :infinity
Expand All @@ -57,13 +65,13 @@ if you have multiple DBs to setup:
```elixir
children = [
Supervisor.child_spec({Postgrex, db_A_conf}, id: :postgrex_A),
Supervisor.child_spec({ExqlMigration.Migration, [db_conn: db_A_conn, migrations_dir: db_A_migrations_dir]}, id: :exql_db_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({ExqlMigration.Migration, [db_conn: db_B_conn, migrations_dir: db_B_migrations_dir]}, id: :exql_db_B)
Supervisor.child_spec({Exql.Migration, [db_conn: db_B_conn, migrations_dir: db_B_migrations_dir]}, id: :exql_db_B)
]
```

`ExqlMigration.CreateDB` can be included in the supervision tree to create (if not exists) a database.
`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
6 changes: 3 additions & 3 deletions lib/exql_migration.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
defmodule ExqlMigration do
@moduledoc File.read!("README.md")
defmodule Exql.Migration do
@moduledoc false

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

@spec create_db(Postgrex.conn(), String.t()) :: :ok
def create_db(conn, name) do
Expand Down
4 changes: 2 additions & 2 deletions lib/exql_migration/create_db.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# coveralls-ignore-start

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

use Supervisor
Expand All @@ -20,7 +20,7 @@ defmodule ExqlMigration.CreateDB do
@impl Supervisor
@spec init({Postgrex.conn(), String.t()}) :: :ignore
def init({db_conn, db_name}) do
ExqlMigration.create_db(db_conn, db_name)
Exql.Migration.create_db(db_conn, db_name)

:ignore
rescue
Expand Down
2 changes: 1 addition & 1 deletion lib/exql_migration/log.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule ExqlMigration.Log do
defmodule Exql.Migration.Log do
@moduledoc false

@type migration_id :: String.t()
Expand Down
4 changes: 2 additions & 2 deletions lib/exql_migration/migration.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# coveralls-ignore-start

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

use Supervisor
Expand All @@ -27,7 +27,7 @@ defmodule ExqlMigration.Migration do
@impl Supervisor
@spec init({Postgrex.conn(), Path.t(), timeout(), boolean()}) :: :ignore
def init({db_conn, migrations_dir, timeout, transactional}) do
ExqlMigration.migrate(db_conn, migrations_dir, timeout, transactional)
Exql.Migration.migrate(db_conn, migrations_dir, timeout, transactional)

:ignore
rescue
Expand Down
2 changes: 1 addition & 1 deletion lib/exql_migration/schema.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule ExqlMigration.Schema do
defmodule Exql.Migration.Schema do
@moduledoc false

@create_schema_stmt """
Expand Down
10 changes: 10 additions & 0 deletions lib/exql_query.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Exql.Query do
@moduledoc false

@spec result(Postgrex.Result.t()) :: [map()]
def result(%Postgrex.Result{columns: columns, rows: rows}) do
Enum.map(rows, fn row ->
Enum.zip(columns, row) |> Map.new()
end)
end
end
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule ExqlMigration.MixProject do
defmodule Exql.MixProject do
use Mix.Project

def project do
[
app: :exql_migration,
app: :exql,
version: "0.1.0",
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
Expand Down
12 changes: 6 additions & 6 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
"certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"},
"db_connection": {:hex, :db_connection, "2.4.0", "d04b1b73795dae60cead94189f1b8a51cc9e1f911c234cc23074017c43c031e5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad416c21ad9f61b3103d254a71b63696ecadb6a917b36f563921e0de00d7d7c8"},
"credo": {:hex, :credo, "1.6.1", "7dc76dcdb764a4316c1596804c48eada9fff44bd4b733a91ccbf0c0f368be61e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "698607fb5993720c7e93d2d8e76f2175bba024de964e160e2f7151ef3ab82ac5"},
"db_connection": {:hex, :db_connection, "2.4.1", "6411f6e23f1a8b68a82fa3a36366d4881f21f47fc79a9efb8c615e62050219da", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea36d226ec5999781a9a8ad64e5d8c4454ecedc7a4d643e4832bf08efca01f00"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"dep_from_hexpm": {:hex, :dep_from_hexpm, "0.3.0", "e820a753364b715e84457836317107c340d3bdcaa21b469272da79f29ef5f5cb", [:mix], [], "hexpm", "55b0c9db6c5666a4358e1d8e799f43f3fa091ef036dc0d09bf5ee9f091f07b6d"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"excoveralls": {:hex, :excoveralls, "0.14.2", "f9f5fd0004d7bbeaa28ea9606251bb643c313c3d60710bad1f5809c845b748f0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ca6fd358621cb4d29311b29d4732c4d47dac70e622850979bc54ed9a3e50f3e1"},
"excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
"hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"postgrex": {:hex, :postgrex, "0.15.10", "2809dee1b1d76f7cbabe570b2a9285c2e7b41be60cf792f5f2804a54b838a067", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1560ca427542f6b213f8e281633ae1a3b31cdbcd84ebd7f50628765b8f6132be"},
"postgrex": {:hex, :postgrex, "0.15.13", "7794e697481799aee8982688c261901de493eb64451feee6ea58207d7266d54a", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3ffb76e1a97cfefe5c6a95632a27ffb67f28871c9741fb585f9d1c3cd2af70f1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
Expand Down
35 changes: 18 additions & 17 deletions test/exql_migration_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule ExqlMigrationTest do
defmodule Test.Exql.Migration do
use ExUnit.Case
alias Exql.Migration

@postgrex_conn :test
@timeout 1_000
Expand Down Expand Up @@ -27,41 +28,41 @@ defmodule ExqlMigrationTest do
end

test "empty migrations set" do
ExqlMigration.migrate(@postgrex_conn, "test/no_migrations", @timeout, true)
assert ExqlMigration.Log.migrations(@postgrex_conn) == []
Migration.migrate(@postgrex_conn, "test/no_migrations", @timeout, true)
assert Migration.Log.migrations(@postgrex_conn) == []
end

test "add a new migration" do
ExqlMigration.migrate(@postgrex_conn, "test/partial_migrations", @timeout, true)
assert [%{id: "001.sql"}] = ExqlMigration.Log.migrations(@postgrex_conn)
Migration.migrate(@postgrex_conn, "test/partial_migrations", @timeout, true)
assert [%{id: "001.sql"}] = Migration.Log.migrations(@postgrex_conn)

ExqlMigration.migrate(@postgrex_conn, "test/all_migrations", @timeout, true)
assert [%{id: "001.sql"}, %{id: "002.sql"}] = ExqlMigration.Log.migrations(@postgrex_conn)
Migration.migrate(@postgrex_conn, "test/all_migrations", @timeout, true)
assert [%{id: "001.sql"}, %{id: "002.sql"}] = Migration.Log.migrations(@postgrex_conn)
end

test "add a new migration (non transactional)" do
ExqlMigration.migrate(@postgrex_conn, "test/partial_migrations", @timeout, false)
assert [%{id: "001.sql"}] = ExqlMigration.Log.migrations(@postgrex_conn)
Migration.migrate(@postgrex_conn, "test/partial_migrations", @timeout, false)
assert [%{id: "001.sql"}] = Migration.Log.migrations(@postgrex_conn)

ExqlMigration.migrate(@postgrex_conn, "test/all_migrations", @timeout, false)
assert [%{id: "001.sql"}, %{id: "002.sql"}] = ExqlMigration.Log.migrations(@postgrex_conn)
Migration.migrate(@postgrex_conn, "test/all_migrations", @timeout, false)
assert [%{id: "001.sql"}, %{id: "002.sql"}] = Migration.Log.migrations(@postgrex_conn)
end

test "idempotent" do
ExqlMigration.migrate(@postgrex_conn, "test/all_migrations", @timeout, true)
assert all = [%{id: "001.sql"}, %{id: "002.sql"}] = ExqlMigration.Log.migrations(@postgrex_conn)
Migration.migrate(@postgrex_conn, "test/all_migrations", @timeout, true)
assert all = [%{id: "001.sql"}, %{id: "002.sql"}] = Migration.Log.migrations(@postgrex_conn)

ExqlMigration.migrate(@postgrex_conn, "test/all_migrations", @timeout, true)
assert ^all = ExqlMigration.Log.migrations(@postgrex_conn)
Migration.migrate(@postgrex_conn, "test/all_migrations", @timeout, true)
assert ^all = Migration.Log.migrations(@postgrex_conn)
end

test "create DB" do
on_exit(fn ->
Postgrex.query!(@postgrex_conn, "drop database test_db", [])
end)

ExqlMigration.create_db(@postgrex_conn, "test_db")
ExqlMigration.create_db(@postgrex_conn, "test_db")
Migration.create_db(@postgrex_conn, "test_db")
Migration.create_db(@postgrex_conn, "test_db")

res = Postgrex.query!(@postgrex_conn, "select true from pg_database where datname = $1", ["test_db"])
assert %Postgrex.Result{num_rows: 1} = res
Expand Down
38 changes: 38 additions & 0 deletions test/exql_query_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
defmodule Test.Exql.Query do
use ExUnit.Case
alias Exql.Query

@postgrex_conn :test

@postgrex_conf [
hostname: System.get_env("POSTGRES_HOST", "localhost"),
username: "postgres",
password: "postgres",
database: "postgres",
name: @postgrex_conn
]

@test_table "query_test_table"

setup_all do
start_supervised!({Postgrex, @postgrex_conf})

Postgrex.query!(@postgrex_conn, "drop table if exists #{@test_table}", [])
Postgrex.query!(@postgrex_conn, "create table #{@test_table} (x text, y text)", [])

:ok
end

setup do
on_exit(fn ->
Postgrex.query!(@postgrex_conn, "truncate table #{@test_table}", [])
end)
end

test "results zip" do
Postgrex.query!(@postgrex_conn, "insert into #{@test_table} (x, y) values ('a', 'b')", [])

res = Postgrex.query!(@postgrex_conn, "select * from #{@test_table}", [])
assert [%{"x" => "a", "y" => "b"}] = Query.result(res)
end
end

0 comments on commit ce06138

Please sign in to comment.