Packages

Simple pure gleam database agnostic migrations library for gleam

Current section

Files

Jump to
gleager src gleager.erl
Raw

src/gleager.erl

-module(gleager).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gleager.gleam").
-export([up/2, down/2]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Gleager is a database migration tool for Gleam.\n"
"\n"
" It applies and rolls back versioned SQL migrations using a driver\n"
" abstraction. Any database library that can execute SQL statements and run\n"
" parameterised queries can be used by implementing the `Driver` type — see\n"
" the `gleager/types` module.\n"
"\n"
" Migration files live in a directory and follow this format:\n"
"\n"
" ```\n"
" 001_description.sql\n"
" 002_another.sql\n"
" ```\n"
"\n"
" Each file contains annotated up and down sections:\n"
"\n"
" ```sql\n"
" -- migrate:up\n"
" CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);\n"
" -- migrate:down\n"
" DROP TABLE users;\n"
" ```\n"
"\n"
" The version is extracted from the filename prefix (e.g. `001` from\n"
" `001_description.sql`). Migrations are applied in version order and\n"
" rolled back in reverse.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import gleager\n"
" import gleager/types.{Driver}\n"
" import sqlight\n"
"\n"
" pub fn main() {\n"
" sqlight.with_connection(\"dev.db\", fn(conn) {\n"
" let driver = Driver(\n"
" migration_dir: \"migrations\",\n"
" exec: fn(sql, args) {\n"
" case args {\n"
" [] -> sqlight.exec(sql, on: conn)\n"
" _ -> {\n"
" let values = list.map(args, sqlight.text)\n"
" case sqlight.query(sql, on: conn, with: values, expecting: decode.int) {\n"
" Ok(_) -> Ok(Nil)\n"
" Error(e) -> Error(e)\n"
" }\n"
" }\n"
" }\n"
" },\n"
" query: fn(sql, args, decoder) {\n"
" let values = list.map(args, sqlight.text)\n"
" sqlight.query(sql, on: conn, with: values, expecting: decoder)\n"
" },\n"
" )\n"
" let assert Ok(Nil) = gleager.up(driver, steps: None)\n"
" })\n"
" }\n"
" ```\n"
).
-file("src/gleager.gleam", 95).
?DOC(
" Applies pending migrations from the migration directory in version order.\n"
"\n"
" Migrations that have already been applied (tracked in a `schema_migrations`\n"
" table created automatically) are skipped. Each new migration runs inside a\n"
" transaction: either all its statements succeed and the migration is recorded,\n"
" or any failure rolls back the entire migration.\n"
"\n"
" The `steps` parameter limits how many migrations are applied. Pass `None`\n"
" to apply all pending migrations, or `Some(n)` to apply at most `n`.\n"
"\n"
" Returns `Ok(Nil)` when all requested migrations have been applied (or if\n"
" there are none). Returns `Error(MigrationError)` if any migration fails or\n"
" the migration directory cannot be read.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Apply all pending migrations\n"
" let assert Ok(Nil) = gleager.up(driver, steps: None)\n"
" ```\n"
"\n"
" ```gleam\n"
" // Apply only the next 2 migrations\n"
" let assert Ok(Nil) = gleager.up(driver, steps: Some(2))\n"
" ```\n"
).
-spec up(gleager@types:driver(any()), gleam@option:option(integer())) -> {ok,
nil} |
{error, gleager@types:migration_error()}.
up(Driver, Steps) ->
gleam@result:'try'(
gleager@internal@database:create_schema_table(
fun(_capture) -> (erlang:element(3, Driver))(_capture, []) end
),
fun(_) ->
gleam@result:'try'(
gleager@internal@database:select_migrations(
fun(Sql, Decoder) ->
(erlang:element(4, Driver))(Sql, [], Decoder)
end
),
fun(Applied_migrations) ->
gleam@result:'try'(
begin
_pipe = gleager@internal@filesystem:find_migrations(
erlang:element(2, Driver)
),
_pipe@1 = gleam@result:map(
_pipe,
fun gleager@internal@filesystem:make_migrations/1
),
gleam@result:flatten(_pipe@1)
end,
fun(Migration_files) ->
Pending = begin
gleam@bool:guard(
gleam@list:is_empty(Applied_migrations),
Migration_files,
fun() ->
gleam@list:filter(
Migration_files,
fun(Migration) ->
_pipe@2 = gleam@list:map(
Applied_migrations,
fun(A) ->
erlang:element(2, A)
end
),
_pipe@3 = gleam@list:contains(
_pipe@2,
erlang:element(2, Migration)
),
gleam@bool:negate(_pipe@3)
end
)
end
)
end,
To_apply = case Steps of
{some, N} ->
gleam@list:take(Pending, N);
none ->
Pending
end,
gleam@bool:guard(
gleam@list:is_empty(To_apply),
{ok, nil},
fun() -> _pipe@4 = To_apply,
gleam@list:try_each(
_pipe@4,
fun(_capture@1) ->
gleager@internal@database:up(
_capture@1,
erlang:element(3, Driver)
)
end
) end
)
end
)
end
)
end
).
-file("src/gleager.gleam", 158).
?DOC(
" Rolls back applied migrations in reverse version order.\n"
"\n"
" Each migration runs its `down` section inside a transaction: either all\n"
" statements succeed and the migration is removed from the tracking table,\n"
" or any failure rolls back the entire migration.\n"
"\n"
" The `steps` parameter limits how many migrations are rolled back. Pass `None`\n"
" to roll back all applied migrations, or `Some(n)` to roll back at most `n`.\n"
"\n"
" Returns `Ok(Nil)` when all requested migrations have been rolled back (or\n"
" if none are applied). Returns `Error(MigrationError)` if any migration\n"
" fails or the migration directory cannot be read.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Roll back all applied migrations\n"
" let assert Ok(Nil) = gleager.down(driver, steps: None)\n"
" ```\n"
"\n"
" ```gleam\n"
" // Roll back only the last migration\n"
" let assert Ok(Nil) = gleager.down(driver, steps: Some(1))\n"
" ```\n"
).
-spec down(gleager@types:driver(any()), gleam@option:option(integer())) -> {ok,
nil} |
{error, gleager@types:migration_error()}.
down(Driver, Steps) ->
gleam@result:'try'(
gleager@internal@database:create_schema_table(
fun(_capture) -> (erlang:element(3, Driver))(_capture, []) end
),
fun(_) ->
gleam@result:'try'(
gleager@internal@database:select_migrations(
fun(Sql, Decoder) ->
(erlang:element(4, Driver))(Sql, [], Decoder)
end
),
fun(Applied_migrations) ->
gleam@result:'try'(
begin
_pipe = gleager@internal@filesystem:find_migrations(
erlang:element(2, Driver)
),
_pipe@1 = gleam@result:map(
_pipe,
fun gleager@internal@filesystem:make_migrations/1
),
gleam@result:flatten(_pipe@1)
end,
fun(Migration_files) ->
Pending = begin
gleam@bool:guard(
gleam@list:is_empty(Applied_migrations),
[],
fun() ->
_pipe@3 = gleam@list:filter(
Migration_files,
fun(Migration) ->
_pipe@2 = gleam@list:map(
Applied_migrations,
fun(A) ->
erlang:element(2, A)
end
),
gleam@list:contains(
_pipe@2,
erlang:element(2, Migration)
)
end
),
lists:reverse(_pipe@3)
end
)
end,
To_remove = case Steps of
{some, N} ->
gleam@list:take(Pending, N);
none ->
Pending
end,
gleam@bool:guard(
gleam@list:is_empty(To_remove),
{ok, nil},
fun() -> _pipe@4 = To_remove,
gleam@list:try_each(
_pipe@4,
fun(_capture@1) ->
gleager@internal@database:down(
_capture@1,
erlang:element(3, Driver)
)
end
) end
)
end
)
end
)
end
).