EZMig (Easy Migration) is a lightweight, SQL-first database migration tool designed for simplicity and CI/CD.
Project description
EZMig (Easy Migration)
EZMig is a lightweight, SQL-first database migration tool designed for simplicity and CI/CD.
It is:
- simple
- deterministic
- database-agnostic
- language-agnostic (just SQL)
- easy to integrate into pipelines
Documentation: https://klinvesta.github.io/ezmig/
โจ Philosophy
EZMig follows a simple rule:
SQL is the source of truth.
No ORM. No schema diffing. No magic.
Just:
- versioned SQL files
- applied in order
- tracked safely
๐ Features
- โ Plain SQL migrations
- โ Linear, predictable execution
- โ Checksum validation (detect drift)
- โ Repeatable migrations (views, functions)
- โ Transaction support (when available)
- โ Rollback support via --ezmig:rollback
- โ Migration status tracking (pending, applied, changedโฆ)
- โ Replay / force apply for dev iteration
- โ CLI-first design (CI/CD ready)
- โ
Migration scaffolding via
ezmig new
๐๏ธ Supported Databases
| Database | Driver | Install | Example |
|---|---|---|---|
| SQLite | Built-in | uv tool install ezmig |
examples/sqlite |
| PostgreSQL | psycopg2 | uv tool install 'ezmig[postgres]' |
examples/postgres |
| Oracle | oracledb | uv tool install 'ezmig[oracle]' |
examples/oracle |
Install just what you need:
uv tool install ezmigโ SQLite support onlyuv tool install 'ezmig[postgres]'โ PostgreSQL supportuv tool install 'ezmig[oracle]'โ Oracle supportuv tool install 'ezmig[all]'โ All databasesuvx ezmig --helpโ run without installing
๐ฆ Installation
Linux one-line install
curl -fsSL https://raw.githubusercontent.com/klinvesta/ezmig/main/scripts/install.sh | BIN_DIR=$HOME/.local/bin sh
Standalone binary (no Python required)
Download a pre-built binary from the releases page.
uv tool install
uv tool install ezmig
uv tool install 'ezmig[postgres]'
uv tool install 'ezmig[oracle]'
uv tool install 'ezmig[all]'
uvx (run without installing)
uvx ezmig --help
uvx --from 'ezmig[postgres]' ezmig status --database dev
uvx --from 'ezmig[oracle]' ezmig plan --database dev
pipx
pipx install ezmig
pip
pip install ezmig # SQLite only
pip install 'ezmig[postgres]' # + PostgreSQL
pip install 'ezmig[oracle]' # + Oracle
pip install 'ezmig[all]' # everything
See the Installation docs for full platform instructions and local binary builds.
๐งฐ Usage
ezmig new "create users" # create versioned migration template
ezmig new "users_v" -r # create repeatable migration template
ezmig status --database dev # show current state for target
ezmig plan --database dev # show pending migrations for target
ezmig apply --database dev # apply migrations to target
ezmig apply --force --allow-replay --database dev # force apply all (dev)
ezmig replay --migration "202601*.sql" --allow-replay --database dev
ezmig replay --repeatable "*.sql" --allow-replay --database dev
ezmig rollback --database dev # rollback last migration on target
# inspect configured targets
ezmig config list
ezmig config show dev
ezmig config validate
๐ Project structure
migrations/
โโโ versioned/
โ โโโ 20260101000000_init.sql
โ โโโ 20260315093000_add_users.sql
โโโ repeatable/
โโโ my_function.sql
โโโ my_view.sql
๐ Migration format
Versioned migration
-- ezmig:apply
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);
-- ezmig:rollback
DROP TABLE users;
-- ezmig:applyโ applied during ezmig apply-- ezmig:rollbackโ used during rollback (ezmig rollback)- Required for rollback support
Repeatable migration
CREATE OR REPLACE VIEW active_users AS
SELECT * FROM users WHERE active = true;
๐ข Versioning
Versioned migrations
ezmig imposes no required filename format. Versioned migrations are sorted lexicographically by filename and executed in that order. You choose the naming convention.
Recommended convention: timestamps
For teams with more than one developer, the strongly recommended approach is a timestamp prefix:
20260323093000_add_payments_table.sql
20260323094512_add_audit_log.sql
Use YYYYMMDDHHmmss (14 digits). This works well because:
- Timestamps are globally unique per developer per second with no coordination needed.
- They sort correctly lexicographically, which is exactly how ezmig orders files.
- No two developers on separate branches will accidentally pick the same prefix.
- Merging branches never causes ordering conflicts โ each migration keeps its natural position in time.
Why not sequential numbers?
001,002,003only work on a single branch. As soon as two developers independently create003_foo.sqland003_bar.sqlon different branches, you have a conflict with no safe resolution. Timestamps eliminate this class of problem entirely.
Execution order
Files are sorted lexicographically by filename and applied strictly in that order. With a timestamp prefix, lexicographic order equals chronological order, so the behaviour is intuitive.
โ ๏ธ Never rename or edit a versioned migration file once it has been applied. ezmig stores the checksum of each file in
ezmig_migrations. A rename looks like a missing file; a content change looks like drift. Both cause an error.
Immutability
Once a versioned migration has been applied, it is immutable:
- Its checksum is recorded in
ezmig_migrations. - If the file content changes, ezmig detects the mismatch during validation and refuses to proceed.
- To modify the schema after the fact, add a new migration with a later timestamp.
Repeatable migrations
Repeatable migrations have no ordering constraints. They are:
- Identified by their filename (used as the primary key in
ezmig_migrations). - Tracked by checksum โ ezmig re-applies them whenever their content changes.
- Always applied after all versioned migrations in a given
ezmig applyrun. - Sorted alphabetically among themselves.
Repeatable migrations are ideal for objects that can be safely recreated: views, functions, and stored procedures.
๐ Migration states
ezmig classifies migrations internally:
- PENDING โ not yet applied
- APPLIED โ already executed
- CHANGED โ repeatable updated
- UNCHANGED โ repeatable unchanged
- UNKNOWN โ no adapter / offline mode
These states power both:
ezmig plan
ezmig status
๐งพ Migration table
ezmig tracks applied migrations using:
CREATE TABLE ezmig_migrations (
version VARCHAR PRIMARY KEY,
type VARCHAR NOT NULL,
checksum VARCHAR NOT NULL,
applied_at TIMESTAMP NOT NULL,
execution_ms INTEGER
);
๐ Rollback
Rollback is explicit and safe:
ezmig rollback # rollback last migration
ezmig rollback -n 3 # rollback last 3 migrations
- Executes -- ezmig:rollback blocks
- Runs in reverse order
- Wrapped in transactions (when supported)
๐ Replay / force apply (dev)
For iterative local development, you can force re-apply migrations.
ezmig replay --migration "202601*.sql" --allow-replay --database dev
ezmig replay --repeatable "*.sql" --allow-replay --database dev
ezmig replay --all --allow-replay --database dev
# alias to replay everything in the target profile
ezmig apply --force --allow-replay --database dev
Safety gates:
- replay/force is disabled by default
- enable per-target with
allow_replay = trueinezmig.toml, or pass--allow-replay
๐ Validation
Before applying migrations, ezmig checks:
- checksum integrity
- missing migrations
- order consistency
Use it directly:
ezmig validate
ezmig validate exits with:
0when validation passes1when validation fails
Validation fails include:
- applied versioned migration file missing on disk
- checksum drift on applied versioned migration
- out-of-order applied versioned migration history
Typical failure output:
Validation failed:
- Applied migration missing on disk: 20260101000000_init.sql
- Checksum drift detected for migration: 20260102000000_add_age.sql
Repeatable migration checksum changes are reported as warnings (they are expected to be re-applied on ezmig apply).
โ๏ธ Configuration
ezmig.toml:
[default]
database = "dev"
[migration.default]
versioned = "./migrations/versioned"
repeatable = [
"./migrations/repeatable",
"./migrations/views",
"./migrations/packages",
"./migrations/gtt",
]
[database.dev]
url = "postgresql://user:pass@localhost:5432/db"
migration = "default"
categories = { env = "dev", location = "local" }
Target selection options for DB commands:
--database <name>--category <key=value>(repeatable)--group <name>
See docs/configuration.md for full schema and examples.
๐ CI/CD Example
ezmig validate --database prod
ezmig plan --database prod
ezmig apply --database prod
Fail-fast CI pattern:
ezmig validate --database prod && ezmig apply --database prod
๐ Documentation
Published docs: https://klinvesta.github.io/ezmig/
Project documentation is organized under docs/ using MkDocs.
Build docs locally:
uv sync --group docs
uv run --group docs mkdocs serve
๐งฑ Design goals
- Keep the core small
- Avoid abstraction layers
- Favor explicit SQL over generated code
- Make failures obvious and safe
โ ๏ธ Non-goals
ezmig intentionally does NOT:
- โ generate schema diffs
- โ manage ORM models
- โ support complex migration graphs
Why EZMig?
Because schema evolution should be:
- incremental
- continuous
- and quietly effective
๐ License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ezmig-0.1.0b4.tar.gz.
File metadata
- Download URL: ezmig-0.1.0b4.tar.gz
- Upload date:
- Size: 23.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01fcbaad117ad6e206fb41bb0f20e8fb4d56eb106fb465a1dedb2cc30a1cf1bf
|
|
| MD5 |
9511096ff0d7d24b6d2185a64597a6d7
|
|
| BLAKE2b-256 |
2799a952815d717bda209eac37901168b2449056c921d072e4eb4496255a88b7
|
Provenance
The following attestation bundles were made for ezmig-0.1.0b4.tar.gz:
Publisher:
release.yml on klinvesta/ezmig
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ezmig-0.1.0b4.tar.gz -
Subject digest:
01fcbaad117ad6e206fb41bb0f20e8fb4d56eb106fb465a1dedb2cc30a1cf1bf - Sigstore transparency entry: 1206763916
- Sigstore integration time:
-
Permalink:
klinvesta/ezmig@02265f450d1ce0fb6ba83693b9caff3785f5f191 -
Branch / Tag:
refs/heads/dev - Owner: https://github.com/klinvesta
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@02265f450d1ce0fb6ba83693b9caff3785f5f191 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ezmig-0.1.0b4-py3-none-any.whl.
File metadata
- Download URL: ezmig-0.1.0b4-py3-none-any.whl
- Upload date:
- Size: 28.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
103342a229ff48cf84f696b4ec7bf6c494ae22c5189cc1c9620f908f5b37a998
|
|
| MD5 |
981c3e6ed1b75320c726e632530bcf18
|
|
| BLAKE2b-256 |
e9babcdc6dd4c1eb6a8f28eec10b7e1185be85a400c966ba8c662adb9281e445
|
Provenance
The following attestation bundles were made for ezmig-0.1.0b4-py3-none-any.whl:
Publisher:
release.yml on klinvesta/ezmig
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ezmig-0.1.0b4-py3-none-any.whl -
Subject digest:
103342a229ff48cf84f696b4ec7bf6c494ae22c5189cc1c9620f908f5b37a998 - Sigstore transparency entry: 1206763959
- Sigstore integration time:
-
Permalink:
klinvesta/ezmig@02265f450d1ce0fb6ba83693b9caff3785f5f191 -
Branch / Tag:
refs/heads/dev - Owner: https://github.com/klinvesta
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@02265f450d1ce0fb6ba83693b9caff3785f5f191 -
Trigger Event:
push
-
Statement type: