src/rally_runtime@migrate.erl

-module(rally_runtime@migrate).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/rally_runtime/migrate.gleam").
-export([error_to_string/1, run/2]).
-export_type([migration_error/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.

?MODULEDOC(
    " SQL migration runner for SQLite. Reads numbered .sql files from a\n"
    " directory, tracks the last applied version in a schema_migrations\n"
    " table, and runs pending migrations inside transactions. Failed\n"
    " migrations roll back and leave the version at the last success.\n"
).

-type migration_error() :: {table_create_failed, binary()} |
    {version_query_failed, binary()} |
    {version_init_failed, binary()} |
    {dir_read_failed, binary()} |
    {file_read_failed, binary(), binary()} |
    {migration_failed, binary(), binary()} |
    {version_update_failed, binary()} |
    {filename_parse_failed, binary()}.

-file("src/rally_runtime/migrate.gleam", 26).
-spec error_to_string(migration_error()) -> binary().
error_to_string(Error) ->
    case Error of
        {table_create_failed, Message} ->
            <<"Failed to create schema_migrations: "/utf8, Message/binary>>;

        {version_query_failed, Message@1} ->
            <<"Failed to get migration version: "/utf8, Message@1/binary>>;

        {version_init_failed, Message@2} ->
            <<"Failed to init schema_migrations: "/utf8, Message@2/binary>>;

        {dir_read_failed, Message@3} ->
            <<"Failed to read migrations directory: "/utf8, Message@3/binary>>;

        {file_read_failed, Filename, Message@4} ->
            <<<<<<"Failed to read "/utf8, Filename/binary>>/binary, ": "/utf8>>/binary,
                Message@4/binary>>;

        {migration_failed, Filename@1, Message@5} ->
            <<<<<<"Migration "/utf8, Filename@1/binary>>/binary,
                    " failed: "/utf8>>/binary,
                Message@5/binary>>;

        {version_update_failed, Message@6} ->
            <<"Failed to update migration version: "/utf8, Message@6/binary>>;

        {filename_parse_failed, Filename@2} ->
            <<"Invalid migration filename (expected NNN_name.sql): "/utf8,
                Filename@2/binary>>
    end.

-file("src/rally_runtime/migrate.gleam", 170).
-spec run_migration_sql(sqlight:connection(), integer(), binary(), binary()) -> {ok,
        nil} |
    {error, migration_error()}.
run_migration_sql(Conn, Num, File, Sql) ->
    case sqlight:exec(Sql, Conn) of
        {ok, _} ->
            case sqlight:exec(
                <<<<"UPDATE schema_migrations SET last_migration = "/utf8,
                        (erlang:integer_to_binary(Num))/binary>>/binary,
                    ";"/utf8>>,
                Conn
            ) of
                {ok, _} ->
                    case sqlight:exec(<<"COMMIT"/utf8>>, Conn) of
                        {ok, _} ->
                            {ok, nil};

                        {error, E} ->
                            {error,
                                {version_update_failed, erlang:element(3, E)}}
                    end;

                {error, E@1} ->
                    _ = sqlight:exec(<<"ROLLBACK"/utf8>>, Conn),
                    {error, {version_update_failed, erlang:element(3, E@1)}}
            end;

        {error, E@2} ->
            _ = sqlight:exec(<<"ROLLBACK"/utf8>>, Conn),
            {error, {migration_failed, File, erlang:element(3, E@2)}}
    end.

-file("src/rally_runtime/migrate.gleam", 133).
-spec run_pending(sqlight:connection(), binary(), list({integer(), binary()})) -> {ok,
        nil} |
    {error, migration_error()}.
run_pending(Conn, Dir, Files) ->
    case Files of
        [] ->
            {ok, nil};

        [{Num, File} | Rest] ->
            Path = <<<<Dir/binary, "/"/utf8>>/binary, File/binary>>,
            gleam@result:'try'(
                begin
                    _pipe = simplifile:read(Path),
                    gleam@result:map_error(
                        _pipe,
                        fun(E) ->
                            {file_read_failed,
                                File,
                                simplifile:describe_error(E)}
                        end
                    )
                end,
                fun(Sql) ->
                    gleam_stdlib:println(
                        <<<<<<"  migration "/utf8,
                                    (erlang:integer_to_binary(Num))/binary>>/binary,
                                ": "/utf8>>/binary,
                            File/binary>>
                    ),
                    gleam@result:'try'(
                        begin
                            _pipe@1 = sqlight:exec(<<"BEGIN"/utf8>>, Conn),
                            gleam@result:map_error(
                                _pipe@1,
                                fun(E@1) ->
                                    {migration_failed,
                                        File,
                                        erlang:element(3, E@1)}
                                end
                            )
                        end,
                        fun(_) ->
                            gleam@result:'try'(
                                run_migration_sql(Conn, Num, File, Sql),
                                fun(_) -> run_pending(Conn, Dir, Rest) end
                            )
                        end
                    )
                end
            )
    end.

-file("src/rally_runtime/migrate.gleam", 207).
-spec parse_number(binary()) -> {ok, integer()} | {error, migration_error()}.
parse_number(Filename) ->
    case gleam@string:split(Filename, <<"_"/utf8>>) of
        [Num_str | _] ->
            _pipe = gleam_stdlib:parse_int(Num_str),
            gleam@result:replace_error(_pipe, {filename_parse_failed, Filename});

        _ ->
            {error, {filename_parse_failed, Filename}}
    end.

-file("src/rally_runtime/migrate.gleam", 96).
-spec get_current_version(sqlight:connection()) -> {ok, integer()} |
    {error, migration_error()}.
get_current_version(Conn) ->
    Decoder = begin
        gleam@dynamic@decode:field(
            0,
            {decoder, fun gleam@dynamic@decode:decode_int/1},
            fun(Version) -> gleam@dynamic@decode:success(Version) end
        )
    end,
    case sqlight:'query'(
        <<"SELECT last_migration FROM schema_migrations LIMIT 1"/utf8>>,
        Conn,
        [],
        Decoder
    ) of
        {ok, [Version@1]} ->
            {ok, Version@1};

        {ok, []} ->
            _pipe = sqlight:exec(
                <<"INSERT INTO schema_migrations (last_migration) VALUES (0);"/utf8>>,
                Conn
            ),
            _pipe@1 = gleam@result:map_error(
                _pipe,
                fun(E) -> {version_init_failed, erlang:element(3, E)} end
            ),
            gleam@result:map(_pipe@1, fun(_) -> 0 end);

        {ok, _} ->
            _ = sqlight:exec(
                <<"DELETE FROM schema_migrations; INSERT INTO schema_migrations (last_migration) VALUES (0);"/utf8>>,
                Conn
            ),
            {ok, 0};

        {error, E@1} ->
            {error, {version_query_failed, erlang:element(3, E@1)}}
    end.

-file("src/rally_runtime/migrate.gleam", 47).
-spec run(sqlight:connection(), binary()) -> {ok, nil} |
    {error, migration_error()}.
run(Conn, Dir) ->
    gleam@result:'try'(
        begin
            _pipe = sqlight:exec(
                <<"CREATE TABLE IF NOT EXISTS schema_migrations (
        last_migration INTEGER NOT NULL
      );"/utf8>>,
                Conn
            ),
            gleam@result:map_error(
                _pipe,
                fun(E) -> {table_create_failed, erlang:element(3, E)} end
            )
        end,
        fun(_) ->
            gleam@result:'try'(
                get_current_version(Conn),
                fun(Current) ->
                    gleam@result:'try'(
                        begin
                            _pipe@1 = simplifile_erl:read_directory(Dir),
                            gleam@result:map_error(
                                _pipe@1,
                                fun(E@1) ->
                                    {dir_read_failed,
                                        simplifile:describe_error(E@1)}
                                end
                            )
                        end,
                        fun(Files) ->
                            gleam@result:'try'(
                                begin
                                    _pipe@2 = Files,
                                    _pipe@3 = gleam@list:filter(
                                        _pipe@2,
                                        fun(F) ->
                                            gleam_stdlib:string_ends_with(
                                                F,
                                                <<".sql"/utf8>>
                                            )
                                        end
                                    ),
                                    _pipe@4 = gleam@list:sort(
                                        _pipe@3,
                                        fun gleam@string:compare/2
                                    ),
                                    gleam@list:try_map(
                                        _pipe@4,
                                        fun(File) ->
                                            gleam@result:'try'(
                                                parse_number(File),
                                                fun(Number) ->
                                                    {ok, {Number, File}}
                                                end
                                            )
                                        end
                                    )
                                end,
                                fun(Migrations) ->
                                    Pending = begin
                                        _pipe@5 = Migrations,
                                        gleam@list:filter(
                                            _pipe@5,
                                            fun(F@1) ->
                                                {Number@1, _} = F@1,
                                                Number@1 > Current
                                            end
                                        )
                                    end,
                                    case Pending of
                                        [] ->
                                            gleam_stdlib:println(
                                                <<<<"  migrations: up to date (v"/utf8,
                                                        (erlang:integer_to_binary(
                                                            Current
                                                        ))/binary>>/binary,
                                                    ")"/utf8>>
                                            ),
                                            {ok, nil};

                                        _ ->
                                            run_pending(Conn, Dir, Pending)
                                    end
                                end
                            )
                        end
                    )
                end
            )
        end
    ).