Metadata-Version: 2.4
Name: migrolint
Version: 0.1.0
Summary: Framework-agnostic linter for database migration folders — catch duplicate version numbers, sequence gaps, and missing down-migrations before CI does. Zero dependencies.
Author: yyfjj
License: MIT
Project-URL: Homepage, https://github.com/jjdoor/migrolint-py
Project-URL: Repository, https://github.com/jjdoor/migrolint-py
Project-URL: Issues, https://github.com/jjdoor/migrolint-py/issues
Keywords: migration,migrations,database,linter,sql,flyway,golang-migrate,dbmate,goose,cli,pre-commit,devops
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# migrolint

**A framework-agnostic linter for your database migrations folder.** It catches
the boring, expensive mistakes that pass locally and only blow up when CI
actually runs the migrations — duplicate version numbers, sequence gaps, and
`up` migrations with no matching `down`. No database connection, no framework
lock-in, **zero dependencies** (pure standard library).

```bash
pip install migrolint

migrolint                      # auto-detects ./migrations, db/migrations, ...
migrolint db/migrations --strict
```

## The problem

Two developers branch off `main`, each adds `0007_add_index.sql`, both merge.
Now your migrations folder has two migrations claiming version `0007`. Your
runner picks one and silently skips the other — or aborts the whole deploy.
This is a [known, recurring failure](https://github.com/golang-migrate/migrate/issues/574)
in every sequence-numbered migration tool.

The existing linters (`django-migration-linter`, Flyway's own checks) are tied
to one framework or need a live database. If you use raw SQL with goose,
dbmate, golang-migrate, or a hand-rolled folder, there's nothing that just
*looks at the filenames* and tells you they're sane. That's migrolint.

## What it checks

| Rule | Severity | Meaning |
|------|----------|---------|
| `DUPE_NUM` | **error** | two migrations share a version number (the merge-collision bug) |
| `MISSING_DOWN` | warning | an `up` migration has no matching `down` (only flagged if the project uses up/down splits) |
| `SEQ_GAP` | warning | a hole in an integer sequence — usually a deleted or un-merged migration |
| `BAD_FORMAT` | warning | a file whose name no known convention recognizes |

## Naming conventions it understands

migrolint reads version numbers out of the filename, across the conventions
people actually use:

| Convention | Example |
|------------|---------|
| Flyway | `V1__init.sql`, `U1__undo.sql`, `R__refresh.sql`, `V1.1__patch.sql` |
| golang-migrate / dbmate | `0001_create_users.up.sql` + `0001_create_users.down.sql` |
| goose / Rails | `20230101120000_create_users.sql` (timestamp prefix) |
| minimalist | `1_init.sql`, `2-add-index.sql` |

Timestamp-style versions are recognized but exempt from `SEQ_GAP` (they're not
meant to be contiguous). Well-known non-migration files (`schema.rb`,
`structure.sql`, `seeds.rb`, …) and any non-migration extension are skipped.

## Usage

```bash
migrolint                      # scan the first migrations dir it finds
migrolint db/migrations        # scan a specific dir
migrolint app/migrations svc/migrations   # scan several
migrolint --json               # machine-readable, for tooling
migrolint --strict             # warnings become errors (exit 1) — good for CI
migrolint --ext .sql,.py       # override which extensions count
migrolint --ignore baseline.sql,seed.sql
```

You can also run it as a module: `python -m migrolint db/migrations`.

### As a pre-commit / CI gate

```yaml
# .pre-commit-config.yaml
- repo: local
  hooks:
    - id: migrolint
      name: migrolint
      entry: migrolint --strict
      language: system
      pass_filenames: false
```

```yaml
# GitHub Actions
- run: pipx run migrolint --strict
```

## Example output

```
migrolint db/migrations (14 files, 12 migrations)

  ✗ DUPE_NUM     version 0007 used by 2 migrations:
       0007_add_index.up.sql
       0007_add_orders_fk.up.sql
  ⚠ MISSING_DOWN 0009_drop_legacy.up.sql — no matching .down file
  ⚠ SEQ_GAP      missing version(s): 5

1 error, 2 warnings.
```

## Exit codes

| Code | Meaning |
|------|---------|
| `0` | clean (or only warnings, without `--strict`) |
| `1` | errors found — or warnings, when `--strict` is set |
| `2` | usage / IO error (no migrations dir, unreadable path) |

## Also available for Node

Same checks, same flags: [`npx migrolint`](https://www.npmjs.com/package/migrolint)
(source: [migrolint](https://github.com/jjdoor/migrolint)). Both ports read
filenames identically, so a mixed-language team gets the same verdict.

## License

MIT
