Skip to content

Commit

Permalink
Merge 648c22c into 790cfea
Browse files Browse the repository at this point in the history
  • Loading branch information
bearmug committed Feb 4, 2019
2 parents 790cfea + 648c22c commit ff32ee3
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 171 deletions.
154 changes: 0 additions & 154 deletions README-hidden.md

This file was deleted.

156 changes: 156 additions & 0 deletions README.md
@@ -0,0 +1,156 @@
# Erlang ❤ pure database migrations
> Database migrations engine. Effects-free.
[![Build Status](https://travis-ci.org/bearmug/erlang-pure-migrations.svg?branch=master)](https://travis-ci.org/bearmug/erlang-pure-migrations) [![Coverage Status](https://coveralls.io/repos/github/bearmug/erlang-pure-migrations/badge.svg?branch=master)](https://coveralls.io/github/bearmug/erlang-pure-migrations?branch=master)

Migrate your Erlang application database with no effort.
This amazing toolkit has [one and only](https://en.wikipedia.org/wiki/Unix_philosophy)
purpose - consistently upgrade database schema, using Erlang stack.
As an extra - do this in "no side-effects" mode.

# Table of content
- [Current limitations](#current-limitations)
- [Quick start](#quick-start)
* [Compatibility table](#compatibility-table)
* [Live code samples](#live-code-samples)
+ [Postgres and epgsql/epgsql sample](#postgres-and--epgsql-epgsql--https---githubcom-epgsql-epgsql--sample)
- [No-effects approach and tools used to achieve it](#no-effects-approach-and-tools-used-to-achieve-it)
* [Tool #1: effects externalization](#tool--1--effects-externalization)
* [Tool #2: make effects explicit](#tool--2--make-effects-explicit)
- [Functional programming abstractions used](#functional-programming-abstractions-used)
* [Functions composition](#functions-composition)
* [Functor applications](#functor-applications)
* [Partial function applications](#partial-function-applications)

# Current limitations
* **up** transactional migration available only. No **downgrade**
or **rollback** possible. Either whole **up** migration completes OK
or failed and rolled back to the state before migration.
* migrations engine **deliberately isolated from any specific
database library**. This way engine user is free to choose from variety
of frameworks (see tested combinations [here](#compatibility-table)) and so on.

# Quick start
Just call `engine:migrate/3` (see specification [here](src/engine.erl#L9)), providing:
* `Path` to migration scripts folder (strictly and incrementally enumerated).
* `FTx` transaction handler
* `FQuery` database queries execution handler

## Compatibility table
| Database dialect | Library | Example |
| -------------- | ------ | ------- |
| postgres | [epgsql/epgsql:4.2.1](https://github.com/epgsql/epgsql/releases/tag/4.2.1) | [epgsql test](test/epgsql_migrations_SUITE.erl)

## Live code samples
### Postgres and [epgsql/epgsql](https://github.com/epgsql/epgsql) sample
<details>
<summary>Click to expand</summary>

```erlang
Conn = ?config(conn, Opts),
MigrationCall =
engine:migrate(
"scripts/folder/path",
fun(F) -> epgsql:with_transaction(Conn, fun(_) -> F() end) end,
fun(Q) ->
case epgsql:squery(Conn, Q) of
{ok, [
{column, <<"version">>, _, _, _, _, _},
{column, <<"filename">>, _, _, _, _, _}], Data} ->
[{list_to_integer(binary_to_list(BinV)), binary_to_list(BinF)} || {BinV, BinF} <- Data];
{ok, [{column, <<"max">>, _, _, _, _, _}], [{null}]} -> -1;
{ok, [{column, <<"max">>, _, _, _, _, _}], [{N}]} ->
list_to_integer(binary_to_list(N));
[{ok, _, _}, {ok, _}] -> ok;
{ok, _, _} -> ok;
{ok, _} -> ok;
Default -> Default
end
end),
...
%% more preparation steps
...
%% migration call
ok = MigrationCall(),

```
Also see examples from live epgsql integration tests
[here](test/epgsql_migrations_SUITE.erl)
</details>

# No-effects approach and tools used to achieve it
Oh, **there is more!** Library implemented in the [way](https://en.wikipedia.org/wiki/Pure_function),
that all side-effects either externalized or deferred explicitly. Reasons
are quite common:
* bring side-effects as close to program edges as possible. Which may
mean enhanced code reasoning, better bugs reproduceability, etc...
* simplify module contracts testing
* library users empowered to re-run idempotent code safely. Well, if
tx/query handlers are real ones - execution is still idempotent (at
application level) and formally pure. But purity maintained inside
library code only. One call is to be issued anyway - migrations
table creation, if this one does not exists.

## Tool #1: effects externalization
There are 2 externalized kind of effects:
* transaction management handler
* database queries handler
Although, those two can`t be pure in real application, it is fairly
simple to replace them with their pure versions if we would like to
(for debug purposes, or testing, or something else).

## Tool #2: make effects explicit
Other effects (like file operations) are deferred in bulk with outcome
like:
* pure referentially-transparent program actions composed only. Impact
or any communication with external world postponed until later stages
* library users decide when they ready to apply migration changes.
Maybe for some reason they would like to prepare execution ->
prepare migrations folder content -> run migrations.

# Functional programming abstractions used
## Functions composition
This trick is quite useful if someone would like to compose two functions
without their actual execution (or without their application,
alternatively speaking). This pretty standard routine may look like below
(Scala or Kotlin+Arrow):
```scala
val divideByTwo = (number : Int) => number / 2;
val addThree = (number: Int) => number + 3;
val composed = addThree compose divideByTwo
```
Simplistic Erlang version:
```erlang
compose(F1, F2) -> fun() -> F2(F1()) end.
```

## Functor applications
There area few places in library with clear need to compose function **A**
and another function **B** inside deferred execution context. Specifics is
that **A** supplies list of objects, and **B** should be applied to each of
them. Sounds like some functor **B** to be applied to **A** output, when
this output is being wrapped into future execution context. Two cases
of this appeared in library:
* have functor running and produce nested list of contexts:
```erlang
%% Map/1 call here produces new context (defferred function call)
map(Generate, Map) -> fun() -> [Map(R) || R <- Generate()] end.
```
* flatten (or fold) contexts (or function calls) list to a single one:
```erlang
flatten(Generate) -> fun() -> [ok = R() || R <- Generate()], ok end.
```
## Partial function applications
Partial application is very useful, in case if not all function arguments
known yet. Or maybe there is deliberate decision to pass some of arguments
later on. Again, in Scala it may look like:
```scala
val add = (a: Int, b: Int) => a + b
val partiallyApplied = add(3, _)
```
Library code has very simplistic partial application, done for exact
arguments number (although it is easy to generalize it for arguments,
represented as list):
```erlang
Partial = fun(V_F) -> do_migration(Path, FQuery, V_F) end,
```
4 changes: 2 additions & 2 deletions src/dialect_postgres.erl
@@ -1,6 +1,6 @@
-module(dialect_postgres).

-export([init/0, migrations_names/0, save_migration/2, latest_existing_version/0]).
-export([init/0, migrations_done/0, save_migration/2, latest_existing_version/0]).

init() ->
"CREATE TABLE IF NOT EXISTS database_migrations_history (
Expand All @@ -9,7 +9,7 @@ init() ->
creation_timestamp TIMESTAMP NOT NULL DEFAULT NOW()
)".

migrations_names() ->
migrations_done() ->
"SELECT version, filename FROM database_migrations_history".

save_migration(Version, Filename) ->
Expand Down
8 changes: 5 additions & 3 deletions src/engine.erl
Expand Up @@ -2,13 +2,15 @@

-export([migrate/3]).

-type version() :: non_neg_integer().
-type migration() :: {version(), Filename :: nonempty_string()}.
-type error() :: {error, Type :: atom(), Details :: any()}.

-spec migrate(
Path :: nonempty_string(),
FTx :: fun((F) -> ok | error()),
FQuery :: fun((nonempty_string()) -> ok | list(string()) | integer() | error())) -> R
when
FQuery :: fun((nonempty_string()) -> ok | [migration()] | version() | error())) -> R
when
F :: fun(() -> ok | error()),
R :: fun(() -> ok | error()).
migrate(Path, FTx, FQuery) ->
Expand Down Expand Up @@ -43,7 +45,7 @@ find_migrations(ScriptsLocation, FQuery) ->
fun(Files) -> filter_script_files(Files, ScriptsLocation, FQuery) end).

filter_script_files({ok, Files}, _Folder, FQuery) ->
MigrationsDone = sets:from_list(FQuery(dialect_postgres:migrations_names())),
MigrationsDone = sets:from_list(FQuery(dialect_postgres:migrations_done())),
lists:filter(
fun(N) -> not sets:is_element(N, MigrationsDone) end,
lists:keysort(1, [version_and_filename(F) || F <- Files]));
Expand Down
20 changes: 10 additions & 10 deletions test/config/test.config
@@ -1,11 +1,11 @@
[{db, [{
config, [
{host, "localhost"},
{port, 5432},
{database, "migration"},
{username, "migration"},
{secret, "migration"},
{timeout, 10000}
]
}]
[{postgres, [{
config, [
{host, "localhost"},
{port, 5432},
{database, "migration"},
{username, "migration"},
{secret, "migration"},
{timeout, 10000}
]
}]
}].
3 changes: 1 addition & 2 deletions test/epgsql_migrations_SUITE.erl
Expand Up @@ -150,7 +150,6 @@ transactional_migration_test(Opts) ->
epgsql_query_fun(Conn) ->
fun(Q) ->
case epgsql:squery(Conn, Q) of
ok -> ok;
{ok, [
{column, <<"version">>, _, _, _, _, _},
{column, <<"filename">>, _, _, _, _, _}], Data} ->
Expand All @@ -171,7 +170,7 @@ init_per_testcase(_TestCase, Opts) ->
{database, Database},
{username, Username},
{secret, Secret},
{timeout, Timeout}]} = application:get_env(db, config),
{timeout, Timeout}]} = application:get_env(postgres, config),
{ok, C} = epgsql:connect(Host, Username, Secret,
#{
database => Database,
Expand Down

0 comments on commit ff32ee3

Please sign in to comment.