-module(migrant@database). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([exec/2, 'query'/4, create_migrations_table/2, filter_applied_migrations/3, apply_migrations/2]). -spec exec(sqlight:connection(), binary()) -> {ok, nil} | {error, migrant@types:error()}. exec(Db, Sql) -> _pipe = sqlight:exec(Sql, Db), _pipe@1 = gleam@result:replace(_pipe, nil), gleam@result:map_error( _pipe@1, fun(Field@0) -> {database_error, Field@0} end ). -spec 'query'( sqlight:connection(), binary(), list(sqlight:value()), fun((gleam@dynamic:dynamic_()) -> {ok, GBB} | {error, list(gleam@dynamic:decode_error())}) ) -> {ok, list(GBB)} | {error, migrant@types:error()}. 'query'(Db, Sql, Args, Decoder) -> _pipe = sqlight:'query'(Sql, Db, Args, Decoder), gleam@result:map_error(_pipe, fun(Field@0) -> {database_error, Field@0} end). -spec create_migrations_table( sqlight:connection(), fun(() -> {ok, nil} | {error, migrant@types:error()}) ) -> {ok, nil} | {error, migrant@types:error()}. create_migrations_table(Db, Next) -> gleam@io:println(<<"-> Creating migrations table"/utf8>>), Query = <<" CREATE TABLE IF NOT EXISTS __migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); "/utf8>>, case exec(Db, Query) of {ok, _} -> Next(); {error, E} -> {error, E} end. -spec pluralise_migration(integer()) -> binary(). pluralise_migration(Count) -> case Count of 1 -> <<"migration"/utf8>>; _ -> <<"migrations"/utf8>> end. -spec filter_applied_migrations( sqlight:connection(), gleam@dict:dict(binary(), migrant@types:migration()), fun((gleam@dict:dict(binary(), migrant@types:migration())) -> {ok, nil} | {error, migrant@types:error()}) ) -> {ok, nil} | {error, migrant@types:error()}. filter_applied_migrations(Db, Migrations, Next) -> gleam@io:println(<<"-> Filtering applied migrations"/utf8>>), Sql = <<"SELECT name FROM __migrations ORDER BY id, name ASC;"/utf8>>, case 'query'( Db, Sql, [], gleam@dynamic:element(0, fun gleam@dynamic:string/1) ) of {ok, Applied} -> _pipe = Migrations, _pipe@1 = gleam@dict:drop(_pipe, Applied), (fun(M) -> Count = gleam@dict:size(M), case Count of 0 -> gleam@io:println(<<"-> No migrations to apply"/utf8>>), {ok, nil}; _ -> gleam@io:println( <<<<<<<<"-> Found "/utf8, (gleam@int:to_string( gleam@dict:size(M) ))/binary>>/binary, " "/utf8>>/binary, (pluralise_migration(Count))/binary>>/binary, " to apply"/utf8>> ), Next(M) end end)(_pipe@1); {error, E} -> gleam@io:debug(E), gleam@io:println(<<"-> Failed to filter applied migrations"/utf8>>), {error, E} end. -spec rollback({binary(), migrant@types:migration()}, sqlight:connection()) -> {ok, nil} | {error, migrant@types:error()}. rollback(Migration_tuple, Db) -> {Name, Migration} = Migration_tuple, case erlang:element(3, Migration) of {some, Sql} -> case exec(Db, Sql) of {ok, _} -> {ok, nil}; {error, E} -> {error, E} end; none -> {error, {migration_error, <<"Unable to rollback migration: "/utf8, Name/binary>>, rollback_error}} end. -spec mark_migration_as_applied( sqlight:connection(), {binary(), migrant@types:migration()} ) -> {ok, nil} | {error, migrant@types:error()}. mark_migration_as_applied(Db, Migration_tuple) -> {Name, _} = Migration_tuple, Sql = <<"INSERT INTO __migrations (name) VALUES (?) returning id;"/utf8>>, case 'query'( Db, Sql, [sqlight:text(Name)], gleam@dynamic:element(0, fun gleam@dynamic:int/1) ) of {ok, _} -> {ok, nil}; {error, E} -> {error, E} end. -spec apply({binary(), migrant@types:migration()}, sqlight:connection()) -> {ok, nil} | {error, migrant@types:error()}. apply(Migration_tuple, Db) -> {Name, Migration} = Migration_tuple, case erlang:element(2, Migration) of {some, Sql} -> gleam@io:println(<<"-> Applying migration: "/utf8, Name/binary>>), case exec(Db, Sql) of {ok, _} -> case mark_migration_as_applied(Db, Migration_tuple) of {ok, _} -> {ok, nil}; {error, E} -> gleam@io:println( <<"-> Failed to mark migration as applied: "/utf8, Name/binary>> ), gleam@io:debug(E), gleam@io:println( <<"-> Rolling back migration: "/utf8, Name/binary>> ), case rollback(Migration_tuple, Db) of {ok, _} -> {error, {migration_error, <<"Rollback complete, failed to mark migration as applied: "/utf8, Name/binary>>, E}}; {error, E@1} -> gleam@io:println( <<"-> Failed to rollback migration: "/utf8, Name/binary>> ), {error, E@1} end end, {ok, nil}; {error, E@2} -> gleam@io:println( <<"-> Failed to apply migration: "/utf8, Name/binary>> ), gleam@io:debug(E@2), gleam@io:println( <<"-> Rolling back migration: "/utf8, Name/binary>> ), case rollback(Migration_tuple, Db) of {ok, _} -> {error, {migration_error, <<"Rollback complete, failed to apply migration: "/utf8, Name/binary>>, E@2}}; {error, E@3} -> {error, E@3} end end; none -> gleam@io:println( <<<<"-> Skipping migration: "/utf8, Name/binary>>/binary, " no `up` query"/utf8>> ), {ok, nil} end. -spec run_migration( list({binary(), migrant@types:migration()}), sqlight:connection(), integer() ) -> {ok, integer()} | {error, migrant@types:error()}. run_migration(Migrations, Db, Count) -> case Migrations of [] -> {ok, Count}; [Migration | Rest] -> case apply(Migration, Db) of {ok, _} -> run_migration(Rest, Db, Count + 1); {error, E} -> {error, E} end end. -spec sort_migrations(gleam@dict:dict(binary(), migrant@types:migration())) -> list({binary(), migrant@types:migration()}). sort_migrations(Migrations) -> _pipe = Migrations, _pipe@1 = gleam@dict:to_list(_pipe), gleam@list:sort( _pipe@1, fun(A, B) -> {Name_a, _} = A, {Name_b, _} = B, gleam@string:compare(Name_a, Name_b) end ). -spec apply_migrations( sqlight:connection(), gleam@dict:dict(binary(), migrant@types:migration()) ) -> {ok, nil} | {error, migrant@types:error()}. apply_migrations(Db, Migrations) -> gleam@io:println(<<"-> Applying migrations"/utf8>>), Count = begin _pipe = Migrations, _pipe@1 = sort_migrations(_pipe), run_migration(_pipe@1, Db, 0) end, case Count of {ok, Count@1} -> gleam@io:println( <<<<"-> Applied "/utf8, (gleam@int:to_string(Count@1))/binary>>/binary, " migrations"/utf8>> ), {ok, nil}; {error, E} -> {error, E} end.