Current section
Files
Jump to
Current section
Files
src/storch.erl
-module(storch).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([migrate/2, get_migrations/1, main/0]).
-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 => 230})
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 => 304})
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
).
-spec handle_new_cmd(list(binary()), boolean()) -> nil.
handle_new_cmd(Args, Help_flag) ->
Timestamp = os:system_time(second),
{_, Dir} = begin
_pipe = gleam@list:find(
gleam@list:window_by_2(Args),
fun(Tuple) -> case Tuple of
{<<"--dir"/utf8>>, _} ->
true;
{<<"--migrations-dir"/utf8>>, _} ->
true;
{<<"-d"/utf8>>, _} ->
true;
_ ->
false
end end
),
gleam@result:unwrap(_pipe, {<<"default"/utf8>>, <<"./"/utf8>>})
end,
case {Help_flag, Args} of
{true, _} ->
gleam@io:println(
<<"
gleam run -m storch -- new <migration name>
options:
--dir, --migrations-dir, -d Directory to create the migration in, default: ./
--help, -h Show this help message
"/utf8>>
);
{_, []} ->
gleam@io:println(<<"Please provide a migration name"/utf8>>);
{_, [Name | _]} ->
Filename = <<<<<<(gleam@int:to_string(Timestamp))/binary, "_"/utf8>>/binary,
(justin:snake_case(Name))/binary>>/binary,
".sql"/utf8>>,
Path = filepath:join(Dir, Filename),
_ = simplifile:create_file(Path),
_ = simplifile:write(Path, <<"-- "/utf8, Name/binary>>),
nil
end.
-spec handle_schema_dump(list(binary()), boolean()) -> nil.
handle_schema_dump(Args, Help_flag) ->
{_, Migrations_dir} = begin
_pipe = gleam@list:find(
gleam@list:window_by_2(Args),
fun(Window) -> case Window of
{<<"--migrations-dir"/utf8>>, _} ->
true;
{<<"-d"/utf8>>, _} ->
true;
_ ->
false
end end
),
gleam@result:unwrap(_pipe, {<<""/utf8>>, <<"./migrations"/utf8>>})
end,
{_, Outfile} = begin
_pipe@1 = gleam@list:find(
gleam@list:window_by_2(Args),
fun(Window@1) -> case Window@1 of
{<<"--file-name"/utf8>>, _} ->
true;
{<<"-f"/utf8>>, _} ->
true;
_ ->
false
end end
),
gleam@result:unwrap(_pipe@1, {<<""/utf8>>, <<"./schema.sql"/utf8>>})
end,
case Help_flag of
true ->
gleam@io:println(
<<"
gleam run -m storch -- schema
options:
--migrations-dir, -d Migrations directory location, default: ./migrations
--file-name, -f Name of the resulting file, default: ./schema.sql
"/utf8>>
);
false ->
sqlight:with_connection(
<<":memory:"/utf8>>,
fun(Connection) ->
Schema_result = (gleam@result:'try'(
get_migrations(Migrations_dir),
fun(Migrations) ->
gleam@result:'try'(
migrate(Migrations, Connection),
fun(_) ->
gleam@result:'try'(
begin
_pipe@2 = sqlight:'query'(
<<"SELECT * FROM sqlite_schema"/utf8>>,
Connection,
[],
gleam@dynamic:tuple5(
fun gleam@dynamic:string/1,
fun gleam@dynamic:string/1,
fun gleam@dynamic:string/1,
fun gleam@dynamic:int/1,
gleam@dynamic:optional(
fun gleam@dynamic:string/1
)
)
),
gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {transaction_error, Field@0} end
)
end,
fun(Sql_dumps) ->
{ok,
begin
_pipe@3 = gleam@list:filter(
Sql_dumps,
fun(Row) ->
case erlang:element(
5,
Row
) of
{some, _} ->
true;
none ->
false
end
end
),
gleam@list:map(
_pipe@3,
fun(Row@1) ->
gleam@option:unwrap(
erlang:element(
5,
Row@1
),
<<""/utf8>>
)
end
)
end}
end
)
end
)
end
)),
case Schema_result of
{error, Err} ->
gleam@io:debug(Err),
nil;
{ok, Sql_list} ->
_ = begin
_pipe@4 = gleam@list:map(
Sql_list,
fun(Str) -> <<Str/binary, ";\n\n"/utf8>> end
),
_pipe@5 = gleam@string:concat(_pipe@4),
simplifile:write(Outfile, _pipe@5)
end,
nil
end
end
)
end.
-spec main() -> nil.
main() ->
Help_flag = gleam@list:any(
erlang:element(4, argv:load()),
fun(Flag) -> case Flag of
<<"--help"/utf8>> ->
true;
<<"-h"/utf8>> ->
true;
<<"help"/utf8>> ->
true;
_ ->
false
end end
),
case erlang:element(4, argv:load()) of
[<<"new"/utf8>> | Rest] ->
handle_new_cmd(Rest, Help_flag);
[<<"schema"/utf8>> | Rest@1] ->
handle_schema_dump(Rest@1, Help_flag);
_ ->
gleam@io:println(
<<"
gleam run -m storch -- <command> [options]
commands:
new <migration name>
Generate a new migration script. A timestamp will
be prepended as the migration id, to ensure ordering
of migration scripts.
schema <path to migrations folder>
Dump the schema of the sqlite database into a schema.sql
file. Pass in the path (absolute or relative) to the
migrations directory.
"/utf8>>
)
end.