-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).