-module(storch). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([migrate/2, get_migrations/1]). -export_type([migration/0, migration_error/0]). -type migration() :: {migration, integer(), binary()}. -type migration_error() :: {directory_not_exist, binary()} | {invalid_migration_name, binary()} | {invalid_migration_id, binary()} | {transaction_error, sqlight:error()} | {migrations_table_error, sqlight:error()} | {migration_script_error, integer(), sqlight:error()}. -spec migration_error(migration_error()) -> fun((any()) -> migration_error()). migration_error(Error) -> fun(_) -> Error end. -spec migrate(list(migration()), sqlight:connection()) -> {ok, nil} | {error, migration_error()}. migrate(Migrations, Connection) -> Transaction = (gleam@result:'try'( begin _pipe = sqlight:exec(<<"begin transaction;"/utf8>>, Connection), gleam@result:map_error( _pipe, fun(Field@0) -> {transaction_error, Field@0} end ) end, fun(_) -> gleam@result:'try'( begin _pipe@1 = sqlight:exec( <<"create table if not exists storch_migrations (id integer, applied integer);"/utf8>>, Connection ), gleam@result:map_error( _pipe@1, fun(Field@0) -> {migrations_table_error, Field@0} end ) end, fun(_) -> Migrations_decoder = gleam@dynamic:tuple2( fun gleam@dynamic:int/1, fun sqlight:decode_bool/1 ), Applications = gleam@list:try_each( Migrations, fun(Migration) -> gleam@result:'try'( begin _pipe@2 = sqlight:'query'( <<"select id, applied from storch_migrations where id = ?;"/utf8>>, Connection, [sqlight:int( erlang:element(2, Migration) )], Migrations_decoder ), gleam@result:map_error( _pipe@2, fun(Field@0) -> {migrations_table_error, Field@0} end ) end, fun(Migrated) -> Already_applied = case Migrated of [] -> false; [{_, Applied}] -> Applied; _ -> erlang:error(#{gleam_error => panic, message => <<"Multiple migrations with the same id in the storch migrations table"/utf8>>, module => <<"storch"/utf8>>, function => <<"migrate"/utf8>>, line => 78}) end, gleam@bool:guard( Already_applied, {ok, nil}, fun() -> gleam@result:'try'( begin _pipe@3 = sqlight:exec( erlang:element( 3, Migration ), Connection ), gleam@result:map_error( _pipe@3, fun(_capture) -> {migration_script_error, erlang:element( 2, Migration ), _capture} end ) end, fun(_) -> gleam@result:'try'( begin _pipe@4 = sqlight:'query'( <<"insert into storch_migrations (id, applied) values (?,?) returning *;"/utf8>>, Connection, [sqlight:int( erlang:element( 2, Migration ) ), sqlight:bool( true )], Migrations_decoder ), gleam@result:map_error( _pipe@4, fun(Field@0) -> {migrations_table_error, Field@0} end ) end, fun(_) -> {ok, nil} end ) end ) end ) end ) end ), gleam@result:'try'( Applications, fun(_) -> gleam@result:'try'( begin _pipe@5 = sqlight:exec( <<"commit;"/utf8>>, Connection ), gleam@result:map_error( _pipe@5, fun(Field@0) -> {transaction_error, Field@0} end ) end, fun(_) -> {ok, nil} end ) end ) end ) end )), case Transaction of {ok, _} -> {ok, nil}; {error, Err} -> gleam@io:println(<<"error running migration"/utf8>>), gleam@io:debug(Err), gleam@io:println(<<"rolling back"/utf8>>), _ = sqlight:exec(<<"rollback;"/utf8>>, Connection), {error, Err} end. -spec read_migrations(list(binary())) -> {ok, list({integer(), binary()})} | {error, migration_error()}. read_migrations(Paths) -> gleam@list:try_map( Paths, fun(Path) -> Filename = filepath:base_name(Path), gleam@result:'try'( begin _pipe = gleam@string:split_once(Filename, <<"_"/utf8>>), gleam@result:map_error( _pipe, migration_error({invalid_migration_name, Filename}) ) end, fun(_use0) -> {Id, _} = _use0, gleam@result:'try'( begin _pipe@1 = gleam@int:parse(Id), gleam@result:map_error( _pipe@1, migration_error({invalid_migration_id, Id}) ) end, fun(Id@1) -> _assert_subject = simplifile:read(Path), {ok, Contents} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"storch"/utf8>>, function => <<"read_migrations"/utf8>>, line => 152}) end, {ok, {Id@1, Contents}} end ) end ) end ). -spec get_migration_filenames(binary()) -> {ok, list(binary())} | {error, migration_error()}. get_migration_filenames(Directory) -> gleam@result:'try'( begin _pipe = simplifile:is_directory(Directory), gleam@result:map_error( _pipe, migration_error({directory_not_exist, Directory}) ) end, fun(Is_dir) -> gleam@bool:guard( not Is_dir, {error, {directory_not_exist, Directory}}, fun() -> gleam@result:'try'( begin _pipe@1 = simplifile:get_files(Directory), gleam@result:map_error( _pipe@1, migration_error( {directory_not_exist, Directory} ) ) end, fun(Filenames_raw) -> _pipe@4 = gleam@list:map( Filenames_raw, fun(Path) -> gleam@result:'try'( filepath:extension(Path), fun(Extension) -> Base_path = filepath:directory_name( Path ), Filename = begin _pipe@2 = filepath:base_name( Path ), filepath:strip_extension( _pipe@2 ) end, gleam@bool:guard( Extension /= <<"sql"/utf8>>, {error, nil}, fun() -> gleam@bool:guard( Base_path /= Directory, {error, nil}, fun() -> gleam@result:'try'( gleam@string:split_once( Filename, <<"_"/utf8>> ), fun(_use0) -> {Numbers, _} = _use0, gleam@result:'try'( begin _pipe@3 = gleam@regex:from_string( <<"^[0-9]+$"/utf8>> ), gleam@result:nil_error( _pipe@3 ) end, fun( Regex ) -> gleam@bool:guard( not gleam@regex:check( Regex, Numbers ), {error, nil}, fun( ) -> {ok, Path} end ) end ) end ) end ) end ) end ) end ), _pipe@5 = gleam@result:values(_pipe@4), {ok, _pipe@5} end ) end ) end ). -spec get_migrations(binary()) -> {ok, list(migration())} | {error, migration_error()}. get_migrations(Directory) -> gleam@result:'try'( get_migration_filenames(Directory), fun(Filenames) -> gleam@result:'try'( read_migrations(Filenames), fun(Raw_migrations) -> _pipe = gleam@list:map( Raw_migrations, fun(Raw) -> {migration, erlang:element(1, Raw), erlang:element(2, Raw)} end ), _pipe@1 = gleam@list:sort( _pipe, fun(A, B) -> gleam@int:compare( erlang:element(2, A), erlang:element(2, B) ) end ), {ok, _pipe@1} end ) end ).