-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 ).