Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.3.4] - 2026-10-04
Changed
- UTF-8 test literals now use OTP 27 sigils (
~B'用户表'), dropping the/utf8segments; dynamically assembled binaries keep their<<...>>form. CLAUDE.mdrenamed toAGENTS.md(the cross-tool convention) with coding conventions (sigil //utf8/-moduledoc) documented;CLAUDE.mdis now a one-line pointer.- All 7 modules now carry a
-moduledocattribute (converted from legacy%% @docheaders); docs are published via rebar3_ex_doc.
[0.3.3] - 2026-09-22
Fixed
erlang_migrate_source:scan/1no longer silently skips*.up.sqlfiles whose{version}_{title}prefix fails to parse. A mistyped migration name now fails the scan with{error, {invalid_migration_filename, [{File, Reason}...]}}instead of the migration being invisibly never applied.- All three drivers now check the
COMMITresult of theset_versionand migration transactions. A failedCOMMIT(e.g. dropped connection) previously reportedok, recording version state that was never persisted; it now surfaces as{error, {commit_failed, _}}so the dirty protocol stays truthful. - MySQL
exec_sql/2: row-returning statements use{ok, Columns, Rows}for one result set or{ok, ResultSets}for multiple result sets in mysql-otp. Both were previously treated as failures (false dirty state) and are now accepted as success. create/2spec widened fromiodata()tounicode:chardata()(UTF-8 titles are a documented feature; codepoint lists were always accepted, the spec was too narrow).
Added
strict_bootstrap => failconfig key. The first strict run on an existing install backfills the history by assuming every file version=<current was applied — late-merged never-executed files get marked applied forever.failrefuses the guess with{error, {strict_bootstrap_needed, Current}}; recovery is an explicitforce/2or a deliberatestrict_bootstrap => backfillrun (default:backfill, unchanged). An explicitly configured value other thanfailorbackfillis rejected with{error, {invalid_strict_bootstrap, Value}}instead of failing open.
Tests
- Real-database integration suites: SQLite (always-on in CI via a fresh EUnit VM,
avoiding NIF reload after meck; no server needed)
plus env-gated PostgreSQL (
EM_PG_*) and MySQL (EM_MYSQL_*) suites covering lifecycle, multi-statement/UTF-8 files, dirty-state recovery, cross-connection lock contention, concurrent migration (exactly-once), strict/out-of-order andstrict_bootstrap. Disposable-container runner:test/integration/run.sh.
Build / Release
rebar.lockregenerated empty. The previously tracked stale lock (from the era when drivers were hard deps, epgsql pinned 4.7.1) leaked into the Hex 0.3.2 package metadata as non-optional requirements on epgsql/mysql/esqlite, contradicting the zero-runtime-dependency design. Publishing runbook now guards the lock file beforerebar3 hex build.
[0.3.2] - 2026-08-28
Fixed
down(Config, N)andgoto(Config, Version)now retain the correct target version after a partial rollback. Previously, rolling back exactly the selected range could clear migration tracking and cause a laterup/1to replay the full history.
[0.3.1] - 2026-06-10
Fixed
- edoc: module header used raw
=<and<table>which the edoc XML parser rejects, breakingrebar3 edoc/ hex doc generation.
[0.3.0] - 2026-06-10
Added
- Strict mode (
strict => true): records every applied migration in a<table>_historytable and failsup/1,2with{error, {out_of_order, Versions}}when a file version=<current was never applied — closes the silent-skip gap of timestamp versioning with multiple developers. First run backfills history;force/2rebuilds it;drop/1drops it;dry_runbypasses it. - New optional driver callback
applied_versions/2; implemented in the PG, MySQL and SQLite drivers. create/2: generate a{utc_timestamp}_{title}.up/.down.sqlpair (UTCYYYYMMDDHHMMSSversion, +1 bump on same-second collision, UTF-8 titles supported).- CLI escript
erlang_migrate_cli(rebar3 escriptize):new <title> [dir]— file generation only, keeping the zero-dependency design.
Fixed
- UTF-8:
erlang_migrate_sourcecrashed (badarg) on non-Latin1 migration titles (list_to_binary→unicode:characters_to_binary); core log lines used~sinstead of~ts, mojibaking UTF-8 titles.
[0.2.3] - 2026-06-06
Fixed
set_version(all drivers): wrap DELETE+UPSERT in a single transaction to prevent partial state on process crash (#1)run_one_up/run_one_down: retryset_version(false)on transient failure afterexec_sqlsucceeds — configurable viaset_version_retriesandset_version_retry_ms(#2)exec_sql(MySQL, SQLite): wrap inBEGIN/COMMIT/ROLLBACK— was unprotected, PG already had it (#5)force/2: validate that the requested version exists in source files whendiris configured; rejects phantom versions like999999(#6)- Table name regex: allow schema-qualified names (
public.schema_migrations) in all three drivers (#11)
Added
- GracefulStop: send
erlang_migrate_abortto the migration process to abort between migrations (MigPid ! erlang_migrate_abort) (#3) - dry_run mode: set
dry_run => truein Config to log what would be applied without touching the database (#14) - Structured logging: logger callback now supports
fun(Level, Meta, Msg)(3-arity) in addition to the existingfun(Level, Msg)—Metais a map carryingversion,title,dry_run, etc. (#16) - Log "applied up N" / "applied down N" on success for each migration (#16)
goto/2: usedropwhile/takewhilefor O(range) traversal instead of full-list comprehension (#15)
Tests
- 95 EUnit tests (was 57), 0 failures; 38 new tests covering all fixes above
[0.2.2] - 2026-05-28
Fixed
exec_sql(PG): replace broken SAVEPOINT approach withBEGIN/COMMIT/ROLLBACKset_version(all drivers): replace non-atomic DELETE+INSERT with upsert (PG:ON CONFLICT DO UPDATE, MySQL:REPLACE INTO, SQLite:INSERT OR REPLACE)apply_up/apply_down/version/force: replaceok = Driver:...badmatch patterns withcaseexpressions for proper{error, _}propagation- Table name injection: add
validate_table_name/1whitelist (^[a-zA-Z_][a-zA-Z0-9_]*$) in all three drivers driver/1: validate driver module is loaded viacode:which/1before use- Remove dead code:
clear_dirty/2export and function body removed fromerlang_migrate_pg - Fix
current_version/2edoc: correct return type to{ok, Version, Dirty} - README: fix PostgreSQL minimum version (
18+→10+), sync install example versions to0.2.2, fix epgsql version to4.8.0, correct Migration history table description
Refactored
- Extract
run_one_up/6andrun_one_down/7to reduceapply_up/apply_downnesting from 5 to 1 level
[0.2.1] - 2026-05-28
Fixed
- Fix edoc module header: use backtick+single-quote syntax (dir') instead of double backtick to resolve doc chunk generation error
[0.2.0] - 2026-05-28
Added
- Multi-database support via
driverconfig key (defaults toerlang_migrate_pg) erlang_migrate_mysql— MySQL 8+ driver usingmysql-otp,GET_LOCKadvisory lockerlang_migrate_sqlite— SQLite 3+ driver usingesqlite, OTPglobal:set_lockerlang_migrate_driverbehaviour — implement all 8 callbacks to add a new databasedown/1— roll back all applied migrationsgoto/2— jump to any version (auto up or down)version/1now returns{ok, Version, Dirty}(was{ok, Version})lock_timeoutconfig key — advisory lock wait timeout in ms (default 15 000)loggerconfig key — optionalfun(Level, Msg) -> okcallback- 33 EUnit tests for core logic + 24 tests for MySQL/SQLite drivers (57 total)
Changed
- Single-row tracking table — aligned with golang-migrate:
set_versionis nowDELETE + INSERTso the table always holds at most one row;force/2is correct with no stale dirty rows - Zero hard dependencies —
epgsql,mysql,esqliteare all opt-in; add only the driver you use to your ownrebar.config tableconfig key — customise the tracking table name without touchingerlang_migratesource
Fixed
force/2stale-dirty-row bug (multi-row model left dirty rows thatforcecould not clear; single-row model eliminates the issue)erlang:phash2range — was16#7FFFFFFFFFFFFFFF(too large); now1 bsl 30
[0.1.0] - 2026-05-28
Added
- Initial release
- Plain SQL migration files (
.up.sql/.down.sqlpairs) - Sequential integer versioning (
000001_title.up.sql) - Migration state stored in
schema_migrationstable in the target PostgreSQL database - PostgreSQL advisory lock — safe for concurrent multi-node Erlang clusters
- Dirty state machine — blocks further runs after partial failure
force/2to recover from dirty stateup/1,up/2— apply all or N pending migrationsdown/2— roll back N applied migrationsversion/1— query current versiondrop/1— drop theschema_migrationstable- Minimal dependencies: only
epgsql - 7 EUnit tests for file scanner (
erlang_migrate_source)