Current section
Files
Jump to
Current section
Files
src/cigogne.erl
-module(cigogne).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-define(FILEPATH, "src/cigogne.gleam").
-export([create_migration_engine/1, new_migration/1, get_migrations/0, get_schema/1, update_schema_file/1, update_schema/1, print_error/1, create_zero_migration/3, is_zero_migration/1, verify_applied_migration_hashes/1, apply_migration/2, apply_next_migration/1, migrate_up/0, execute_migrations_to_last/1, migrate_to_last/0, apply_migration_if_not_applied/2, roll_back_migration/2, roll_back_previous_migration/1, migrate_down/0, execute_n_migrations/2, migrate_n/1, main/0]).
-export_type([database_config/0, migration_engine/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type database_config() :: env_var_config |
{url_config, binary()} |
{pog_config, pog:config()} |
{connection_config, pog:connection()}.
-type migration_engine() :: {migration_engine,
gleam@option:option(binary()),
pog:connection(),
list(cigogne@types:migration()),
list(cigogne@types:migration()),
boolean()}.
-file("src/cigogne.gleam", 59).
-spec show_usage() -> {ok, nil} | {error, cigogne@types:migrate_error()}.
show_usage() ->
gleam_stdlib:println(<<"======================================="/utf8>>),
gleam_stdlib:println(<<"= CIGOGNE ="/utf8>>),
gleam_stdlib:println(<<"======================================="/utf8>>),
gleam_stdlib:println(<<""/utf8>>),
gleam_stdlib:println(<<"Usage: gleam run -m cigogne [command]"/utf8>>),
gleam_stdlib:println(<<""/utf8>>),
gleam_stdlib:println(<<"List of commands:"/utf8>>),
gleam_stdlib:println(
<<" - show: Show the last currently applied migration"/utf8>>
),
gleam_stdlib:println(
<<" - up: Migrate up one version / Apply one migration"/utf8>>
),
gleam_stdlib:println(
<<" - down: Migrate down one version / Rollback one migration"/utf8>>
),
gleam_stdlib:println(
<<" - last: Apply all migrations until the last one defined"/utf8>>
),
gleam_stdlib:println(
<<" - apply N: Apply or roll back N migrations (N can be positive or negative)"/utf8>>
),
gleam_stdlib:println(
<<" - new NAME: Create a new migration file in priv/migrations with the specified name"/utf8>>
),
{ok, nil}.
-file("src/cigogne.gleam", 156).
-spec get_applied_if_migration_zero(pog:connection()) -> {ok,
{list(cigogne@types:migration()), boolean()}} |
{error, cigogne@types:migrate_error()}.
get_applied_if_migration_zero(Connection) ->
case cigogne@internal@migrations:get_applied_migrations(Connection) of
{error, {p_g_o_query_error, {postgresql_error, Code, _, _}}} when Code =:= <<"42P01"/utf8>> ->
{ok, {[], false}};
{error, Err} ->
{error, Err};
{ok, Migs} ->
{ok, {Migs, true}}
end.
-file("src/cigogne.gleam", 121).
-spec create_engine_from_conn(pog:connection()) -> {ok, migration_engine()} |
{error, cigogne@types:migrate_error()}.
create_engine_from_conn(Connection) ->
gleam@result:'try'(
get_applied_if_migration_zero(Connection),
fun(_use0) ->
{Applied, Zero_applied} = _use0,
gleam@result:'try'(
cigogne@internal@fs:get_migrations(),
fun(Files) ->
{ok,
{migration_engine,
none,
Connection,
Applied,
Files,
Zero_applied}}
end
)
end
).
-file("src/cigogne.gleam", 138).
-spec create_engine_from_url(binary()) -> {ok, migration_engine()} |
{error, cigogne@types:migrate_error()}.
create_engine_from_url(Url) ->
gleam@result:'try'(
cigogne@internal@database:connect(Url),
fun(Connection) ->
gleam@result:'try'(
get_applied_if_migration_zero(Connection),
fun(_use0) ->
{Applied, Zero_applied} = _use0,
gleam@result:'try'(
cigogne@internal@fs:get_migrations(),
fun(Files) ->
{ok,
{migration_engine,
{some, Url},
Connection,
Applied,
Files,
Zero_applied}}
end
)
end
)
end
).
-file("src/cigogne.gleam", 107).
?DOC(
" Creates a MigrationEngine from a configuration.\n"
" This function will try to connect to the database.\n"
" Then it will fetch the applied migrations and the existing migration files.\n"
).
-spec create_migration_engine(database_config()) -> {ok, migration_engine()} |
{error, cigogne@types:migrate_error()}.
create_migration_engine(Config) ->
case Config of
{connection_config, Conn} ->
create_engine_from_conn(Conn);
env_var_config ->
gleam@result:'try'(
cigogne@internal@database:get_url(),
fun(Url) -> create_engine_from_url(Url) end
);
{pog_config, Conf} ->
create_engine_from_conn(
begin
_pipe = Conf,
pog_ffi:connect(_pipe)
end
);
{url_config, Url@1} ->
create_engine_from_url(Url@1)
end.
-file("src/cigogne.gleam", 210).
?DOC(" Create a new migration file in the `priv/migrations` folder with the provided name.\n").
-spec new_migration(binary()) -> {ok, nil} |
{error, cigogne@types:migrate_error()}.
new_migration(Name) ->
gleam@result:map(
cigogne@internal@fs:create_new_migration_file(
begin
_pipe = tempo@instant:now(),
_pipe@1 = tempo@instant:as_utc_datetime(_pipe),
tempo@datetime:drop_offset(_pipe@1)
end,
Name
),
fun(Path) ->
gleam_stdlib:println(
<<"Migration file created : "/utf8, Path/binary>>
)
end
).
-file("src/cigogne.gleam", 349).
?DOC(
" Get all defined migrations in your project.\n"
" Migration files are searched in the `priv/migrations` folder.\n"
).
-spec get_migrations() -> {ok, list(cigogne@types:migration())} |
{error, cigogne@types:migrate_error()}.
get_migrations() ->
cigogne@internal@fs:get_migrations().
-file("src/cigogne.gleam", 354).
?DOC(" Get details about the schema of the database at the provided url.\n").
-spec get_schema(binary()) -> {ok, binary()} |
{error, cigogne@types:migrate_error()}.
get_schema(Url) ->
cigogne@internal@database:get_schema(Url).
-file("src/cigogne.gleam", 360).
?DOC(
" Create or update a schema file with details of the schema of the database at the provided url.\n"
" The schema file is created at `./sql.schema`.\n"
).
-spec update_schema_file(binary()) -> {ok, nil} |
{error, cigogne@types:migrate_error()}.
update_schema_file(Url) ->
gleam@result:'try'(
get_schema(Url),
fun(Schema) -> _pipe = cigogne@internal@fs:write_schema_file(Schema),
gleam@result:map(
_pipe,
fun(_) ->
gleam_stdlib:println(<<"Schema file updated"/utf8>>)
end
) end
).
-file("src/cigogne.gleam", 368).
?DOC(
" Create or update a schema file if the engine configuration permits it.\n"
" See update_schema_file.\n"
).
-spec update_schema(migration_engine()) -> {ok, nil} |
{error, cigogne@types:migrate_error()}.
update_schema(Engine) ->
case erlang:element(2, Engine) of
none ->
gleam_stdlib:println(
<<"No URL provided, schema will not be updated"/utf8>>
),
{ok, nil};
{some, Url} ->
update_schema_file(Url)
end.
-file("src/cigogne.gleam", 423).
?DOC(" Print a MigrateError to the standard error stream.\n").
-spec print_error(cigogne@types:migrate_error()) -> nil.
print_error(Error) ->
cigogne@types:print_migrate_error(Error).
-file("src/cigogne.gleam", 445).
?DOC(" Create a \"zero\" migration that should be applied before the user's migrations\n").
-spec create_zero_migration(binary(), list(binary()), list(binary())) -> cigogne@types:migration().
create_zero_migration(Name, Queries_up, Queries_down) ->
{migration,
<<""/utf8>>,
cigogne@internal@utils:tempo_epoch(),
Name,
Queries_up,
Queries_down,
cigogne@internal@utils:make_sha256(
<<(begin
_pipe = Queries_up,
gleam@string:join(_pipe, <<";"/utf8>>)
end)/binary,
(begin
_pipe@1 = Queries_down,
gleam@string:join(_pipe@1, <<";"/utf8>>)
end)/binary>>
)}.
-file("src/cigogne.gleam", 22).
-spec cigogne_zero_migration() -> cigogne@types:migration().
cigogne_zero_migration() ->
create_zero_migration(
<<"CreateMigrationsTable"/utf8>>,
[<<"CREATE TABLE IF NOT EXISTS _migrations(
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name VARCHAR(255) NOT NULL,
sha256 VARCHAR(64) NOT NULL,
createdAt TIMESTAMP WITHOUT TIME ZONE NOT NULL,
appliedAt TIMESTAMP NOT NULL DEFAULT NOW()
);"/utf8>>],
[]
).
-file("src/cigogne.gleam", 463).
?DOC(" Checks if a migration is a zero migration (has been created with create_zero_migration)\n").
-spec is_zero_migration(cigogne@types:migration()) -> boolean().
is_zero_migration(Migration) ->
cigogne@internal@migrations:is_zero_migration(Migration).
-file("src/cigogne.gleam", 378).
-spec get_last_applied_migration(migration_engine()) -> {ok,
cigogne@types:migration()} |
{error, cigogne@types:migrate_error()}.
get_last_applied_migration(Engine) ->
case begin
_pipe = erlang:element(4, Engine),
gleam@list:last(_pipe)
end of
{error, nil} ->
{error, no_migration_to_rollback_error};
{ok, Mig} ->
gleam@bool:guard(
is_zero_migration(Mig),
{error, no_migration_to_rollback_error},
fun() ->
cigogne@internal@migrations:find_migration(
erlang:element(5, Engine),
Mig
)
end
)
end.
-file("src/cigogne.gleam", 429).
?DOC(
" Verify the hash integrity of applied migrations.\n"
" If an already applied migration has been modified, it should most likely be run again by the user.\n"
).
-spec verify_applied_migration_hashes(migration_engine()) -> {ok, nil} |
{error, cigogne@types:migrate_error()}.
verify_applied_migration_hashes(Engine) ->
gleam@list:try_each(
erlang:element(4, Engine),
fun(Mig) ->
gleam@bool:guard(
is_zero_migration(Mig),
{ok, nil},
fun() ->
gleam@result:'try'(
cigogne@internal@migrations:find_migration(
erlang:element(5, Engine),
Mig
),
fun(File) ->
case erlang:element(7, Mig) =:= erlang:element(
7,
File
) of
true ->
{ok, nil};
false ->
{error,
{file_hash_changed,
erlang:element(3, Mig),
erlang:element(4, Mig)}}
end
end
)
end
)
end
).
-file("src/cigogne.gleam", 290).
?DOC(
" Apply a migration to the database.\n"
" This function does not create a schema file.\n"
).
-spec apply_migration(migration_engine(), cigogne@types:migration()) -> {ok,
nil} |
{error, cigogne@types:migrate_error()}.
apply_migration(Engine, Migration) ->
gleam_stdlib:println(
<<"\nApplying migration "/utf8,
(cigogne@internal@fs:format_migration_name(Migration))/binary>>
),
Queries = begin
_pipe = gleam@list:map(erlang:element(5, Migration), fun pog:'query'/1),
lists:append(
_pipe,
[begin
_pipe@1 = pog:'query'(
<<"INSERT INTO _migrations(createdAt, name, sha256) VALUES ($1, $2, $3);"/utf8>>
),
_pipe@3 = pog:parameter(
_pipe@1,
pog:timestamp(
begin
_pipe@2 = erlang:element(3, Migration),
cigogne@internal@utils:tempo_to_pog_timestamp(
_pipe@2
)
end
)
),
_pipe@4 = pog:parameter(
_pipe@3,
pog_ffi:coerce(erlang:element(4, Migration))
),
pog:parameter(
_pipe@4,
pog_ffi:coerce(erlang:element(7, Migration))
)
end]
)
end,
_pipe@5 = cigogne@internal@database:execute_batch(
erlang:element(3, Engine),
Queries,
cigogne@internal@migrations:compare_migrations(
Migration,
cigogne_zero_migration()
)
=:= eq
),
gleam@result:map(
_pipe@5,
fun(_) ->
gleam_stdlib:println(
<<"Migration applied : "/utf8,
(cigogne@internal@fs:format_migration_name(Migration))/binary>>
)
end
).
-file("src/cigogne.gleam", 281).
-spec apply_migration_zero_if_not_applied(migration_engine()) -> {ok, nil} |
{error, cigogne@types:migrate_error()}.
apply_migration_zero_if_not_applied(Engine) ->
gleam@bool:guard(
erlang:element(6, Engine),
{ok, nil},
fun() -> apply_migration(Engine, cigogne_zero_migration()) end
).
-file("src/cigogne.gleam", 223).
?DOC(
" Apply the next migration that wasn't applied yet.\n"
" The migrations are acquired from priv/migrations/*.sql files.\n"
" This function does not create a schema file.\n"
).
-spec apply_next_migration(migration_engine()) -> {ok, nil} |
{error, cigogne@types:migrate_error()}.
apply_next_migration(Engine) ->
gleam@result:'try'(
apply_migration_zero_if_not_applied(Engine),
fun(_) ->
gleam@result:'try'(
verify_applied_migration_hashes(Engine),
fun(_) ->
gleam@result:'try'(
cigogne@internal@migrations:find_first_non_applied_migration(
erlang:element(5, Engine),
erlang:element(4, Engine)
),
fun(Migration) -> apply_migration(Engine, Migration) end
)
end
)
end
).
-file("src/cigogne.gleam", 173).
?DOC(
" Apply the next migration that wasn't applied yet.\n"
" This function will get the database url from the `DATABASE_URL` environment variable.\n"
" The migrations are then acquired from priv/migrations/*.sql files.\n"
" If successful, it will also create a file and write details of the new schema in it.\n"
).
-spec migrate_up() -> {ok, nil} | {error, cigogne@types:migrate_error()}.
migrate_up() ->
gleam@result:'try'(
create_migration_engine(env_var_config),
fun(Engine) ->
gleam@result:'try'(
apply_next_migration(Engine),
fun(_) -> update_schema(Engine) end
)
end
).
-file("src/cigogne.gleam", 271).
?DOC(
" Apply migrations until we reach the last defined migration.\n"
" The migrations are acquired from priv/migrations/*.sql files.\n"
" This function does not create a schema file.\n"
).
-spec execute_migrations_to_last(migration_engine()) -> {ok, nil} |
{error, cigogne@types:migrate_error()}.
execute_migrations_to_last(Engine) ->
gleam@result:'try'(
apply_migration_zero_if_not_applied(Engine),
fun(_) ->
gleam@result:'try'(
verify_applied_migration_hashes(Engine),
fun(_) ->
_pipe = cigogne@internal@migrations:find_all_non_applied_migration(
erlang:element(5, Engine),
erlang:element(4, Engine)
),
gleam@result:then(
_pipe,
fun(_capture) ->
gleam@list:try_each(
_capture,
fun(_capture@1) ->
apply_migration(Engine, _capture@1)
end
)
end
)
end
)
end
).
-file("src/cigogne.gleam", 203).
?DOC(
" Apply migrations until we reach the last defined migration.\n"
" This function will get the database url from the `DATABASE_URL` environment variable.\n"
" The migrations are then acquired from priv/migrations/*.sql files.\n"
" If successful, it will also create a file and write details of the new schema in it.\n"
).
-spec migrate_to_last() -> {ok, nil} | {error, cigogne@types:migrate_error()}.
migrate_to_last() ->
gleam@result:'try'(
create_migration_engine(env_var_config),
fun(Engine) ->
gleam@result:'try'(
execute_migrations_to_last(Engine),
fun(_) -> update_schema(Engine) end
)
end
).
-file("src/cigogne.gleam", 394).
-spec show() -> {ok, nil} | {error, cigogne@types:migrate_error()}.
show() ->
gleam@result:'try'(
create_migration_engine(env_var_config),
fun(Engine) ->
gleam@result:'try'(
apply_migration_zero_if_not_applied(Engine),
fun(_) ->
gleam@result:'try'(
verify_applied_migration_hashes(Engine),
fun(_) ->
gleam@result:'try'(
get_last_applied_migration(Engine),
fun(Last) ->
gleam_stdlib:println(
<<<<<<"Last applied migration: "/utf8,
(begin
_pipe = erlang:element(
3,
Last
),
tempo@naive_datetime:format(
_pipe,
{custom_naive,
<<"YYYYMMDDHHmmss"/utf8>>}
)
end)/binary>>/binary,
"-"/utf8>>/binary,
(erlang:element(4, Last))/binary>>
),
case erlang:element(2, Engine) of
none ->
gleam_stdlib:println(
<<"Couldn't get schema because no URL was provided"/utf8>>
),
{ok, nil};
{some, Url} ->
gleam@result:'try'(
get_schema(Url),
fun(Schema) ->
gleam_stdlib:println(
<<""/utf8>>
),
gleam_stdlib:println(Schema),
{ok, nil}
end
)
end
end
)
end
)
end
)
end
).
-file("src/cigogne.gleam", 469).
?DOC(
" Apply a migration to the database if it hasn't been applied.\n"
" This function does not create a schema file.\n"
).
-spec apply_migration_if_not_applied(
migration_engine(),
cigogne@types:migration()
) -> {ok, nil} | {error, cigogne@types:migrate_error()}.
apply_migration_if_not_applied(Engine, Migration) ->
gleam@bool:guard(
begin
_pipe = cigogne@internal@migrations:find_migration(
erlang:element(4, Engine),
Migration
),
gleam@result:is_ok(_pipe)
end,
{ok, nil},
fun() -> apply_migration(Engine, Migration) end
).
-file("src/cigogne.gleam", 319).
?DOC(
" Roll back a migration from the database.\n"
" This function does not create a schema file.\n"
).
-spec roll_back_migration(migration_engine(), cigogne@types:migration()) -> {ok,
nil} |
{error, cigogne@types:migrate_error()}.
roll_back_migration(Engine, Migration) ->
gleam_stdlib:println(
<<"\nRolling back migration "/utf8,
(cigogne@internal@fs:format_migration_name(Migration))/binary>>
),
Queries = begin
_pipe = gleam@list:map(erlang:element(6, Migration), fun pog:'query'/1),
lists:append(
_pipe,
[begin
_pipe@1 = pog:'query'(
<<"DELETE FROM _migrations WHERE name = $1 AND createdAt = $2;"/utf8>>
),
_pipe@2 = pog:parameter(
_pipe@1,
pog_ffi:coerce(erlang:element(4, Migration))
),
pog:parameter(
_pipe@2,
pog:timestamp(
begin
_pipe@3 = erlang:element(3, Migration),
cigogne@internal@utils:tempo_to_pog_timestamp(
_pipe@3
)
end
)
)
end]
)
end,
_pipe@4 = cigogne@internal@database:execute_batch(
erlang:element(3, Engine),
Queries,
cigogne@internal@migrations:compare_migrations(
Migration,
cigogne_zero_migration()
)
=:= eq
),
gleam@result:map(
_pipe@4,
fun(_) ->
gleam_stdlib:println(
<<"Migration rolled back : "/utf8,
(cigogne@internal@fs:format_migration_name(Migration))/binary>>
)
end
).
-file("src/cigogne.gleam", 238).
?DOC(
" Roll back the last applied migration.\n"
" The migrations are acquired from priv/migrations/*.sql files.\n"
" This function does not create a schema file.\n"
).
-spec roll_back_previous_migration(migration_engine()) -> {ok, nil} |
{error, cigogne@types:migrate_error()}.
roll_back_previous_migration(Engine) ->
gleam@result:'try'(
apply_migration_zero_if_not_applied(Engine),
fun(_) ->
gleam@result:'try'(
verify_applied_migration_hashes(Engine),
fun(_) ->
gleam@result:'try'(
get_last_applied_migration(Engine),
fun(Last) -> roll_back_migration(Engine, Last) end
)
end
)
end
).
-file("src/cigogne.gleam", 183).
?DOC(
" Roll back the last applied migration.\n"
" This function will get the database url from the `DATABASE_URL` environment variable.\n"
" The migrations are then acquired from priv/migrations/*.sql files.\n"
" If successful, it will also create a file and write details of the new schema in it.\n"
).
-spec migrate_down() -> {ok, nil} | {error, cigogne@types:migrate_error()}.
migrate_down() ->
gleam@result:'try'(
create_migration_engine(env_var_config),
fun(Engine) ->
gleam@result:'try'(
roll_back_previous_migration(Engine),
fun(_) -> update_schema(Engine) end
)
end
).
-file("src/cigogne.gleam", 250).
?DOC(
" Apply or roll back migrations until we reach the migration corresponding to the provided number.\n"
" The migrations are acquired from priv/migrations/*.sql files.\n"
" This function does not create a schema file.\n"
).
-spec execute_n_migrations(migration_engine(), integer()) -> {ok, nil} |
{error, cigogne@types:migrate_error()}.
execute_n_migrations(Engine, Count) ->
gleam@result:'try'(
apply_migration_zero_if_not_applied(Engine),
fun(_) ->
gleam@result:'try'(
verify_applied_migration_hashes(Engine),
fun(_) ->
_pipe = cigogne@internal@migrations:find_n_migrations_to_apply(
erlang:element(5, Engine),
erlang:element(4, Engine),
Count
),
gleam@result:then(
_pipe,
fun(_capture) ->
gleam@list:try_each(
_capture,
fun(Migration) -> case Count > 0 of
true ->
apply_migration(Engine, Migration);
false ->
roll_back_migration(
Engine,
Migration
)
end end
)
end
)
end
)
end
).
-file("src/cigogne.gleam", 193).
?DOC(
" Apply or roll back migrations until we reach the migration corresponding to the provided number.\n"
" This function will get the database url from the `DATABASE_URL` environment variable.\n"
" The migrations are then acquired from priv/migrations/*.sql files.\n"
" If successful, it will also create a file and write details of the new schema in it.\n"
).
-spec migrate_n(integer()) -> {ok, nil} | {error, cigogne@types:migrate_error()}.
migrate_n(Count) ->
gleam@result:'try'(
create_migration_engine(env_var_config),
fun(Engine) ->
gleam@result:'try'(
execute_n_migrations(Engine, Count),
fun(_) -> update_schema(Engine) end
)
end
).
-file("src/cigogne.gleam", 42).
-spec main() -> {ok, nil} | {error, nil}.
main() ->
_pipe = case erlang:element(4, argv:load()) of
[<<"show"/utf8>>] ->
show();
[<<"up"/utf8>>] ->
migrate_up();
[<<"down"/utf8>>] ->
migrate_down();
[<<"last"/utf8>>] ->
migrate_to_last();
[<<"apply"/utf8>>, X] ->
case gleam_stdlib:parse_int(X) of
{error, _} ->
show_usage();
{ok, Mig} ->
migrate_n(Mig)
end;
[<<"new"/utf8>>, Name] ->
new_migration(Name);
_ ->
show_usage()
end,
gleam@result:map_error(_pipe, fun cigogne@types:print_migrate_error/1).