Metadata-Version: 2.4
Name: oneport-migrate
Version: 0.1.0
Summary: Database-migration safety gate for Python — Django, Alembic, and raw SQL, with AI blast-radius judgment
Author-email: Oneport <eng@oneport.dev>
License: MIT License
        
        Copyright (c) 2026 Oneport
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://oneport.dev
Project-URL: Repository, https://github.com/oneport/oneport-migrate
Project-URL: Changelog, https://github.com/oneport/oneport-migrate/blob/main/CHANGELOG.md
Keywords: migrations,django,alembic,postgres,ci,safety,database
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.1.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: pyyaml>=6.0.0
Requires-Dist: rich>=13.0.0
Provides-Extra: claude
Requires-Dist: anthropic>=0.25.0; extra == "claude"
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.4.0; extra == "dev"
Dynamic: license-file

# Oneport Migrate

**The database-migration safety gate for Python teams.** Django migrations,
Alembic revisions, and raw `.sql` — checked by a deterministic rule engine,
judged by an LLM that knows your repo, and wired into CI as a blocking gate.

Migrations are the scariest deploy moment: `squawk` lints Postgres SQL only,
`strong_migrations` is Rails only. Nothing covers the Python world's actual
migration formats. Oneport Migrate does.

## How it works

Two layers, strictly separated:

1. **Deterministic rule engine** — parses your migrations (Django via AST,
   Alembic via AST, raw SQL via regex) into a normalized operation model and
   matches it against a YAML rule catalog (`OPM001`+). This layer alone
   decides **whether** an operation is dangerous. No model, no network, no
   flakiness — the same migration always produces the same findings.

2. **LLM blast-radius layer** (Gemini or Claude) — given the findings, the
   migration source, and your models/schema files, it judges **how** dangerous
   in *this* repo ("the `users` table is written on every request — a 4-minute
   lock here is an outage"), writes a safe multi-step rewrite plan
   (expand → backfill in batches → contract), and a plain-English verdict.
   It can adjust a finding's severity **only** when your team guidelines
   explicitly justify it — never on its own judgment.

If the LLM is unavailable (no key, rate limit, bad output), the gate still
works: deterministic findings and the exit code stand on their own.

## Install

```bash
pip install oneport-migrate
export GEMINI_API_KEY=...   # free at https://aistudio.google.com/apikey
```

Claude instead of Gemini: `pip install oneport-migrate[claude]` and set
`ANTHROPIC_API_KEY`.

## Usage

```bash
# One file, a directory, git state, or a PR
oneport-migrate check app/migrations/0042_drop_legacy.py
oneport-migrate check migrations/
oneport-migrate check --staged
oneport-migrate check --head
oneport-migrate check https://github.com/org/repo/pull/42

# Post findings as an inline PR review (requires GITHUB_TOKEN)
oneport-migrate check https://github.com/org/repo/pull/42 --post

# Options
oneport-migrate check migrations/ --db mysql --format json --min-severity error
oneport-migrate check migrations/ --no-llm     # deterministic rules only
oneport-migrate rules list
```

### Exit codes (the CI contract)

| Code | Meaning |
|------|---------|
| 0 | clean, or warnings/info only |
| 1 | blocking findings (error/critical) |
| 2 | usage / config / auth error |

Blocking is computed from the **full** finding set before any display
filtering — `--min-severity critical` can hide an error from the output, but
never from the exit code.

## Rule catalog

| ID | Severity | What it catches |
|----|----------|-----------------|
| OPM001 | critical | Dropping a column |
| OPM002 | critical | Dropping a table |
| OPM003 | critical | TRUNCATE / DELETE without WHERE |
| OPM010 | error | ALTER COLUMN TYPE (table rewrite under exclusive lock) |
| OPM011 | error | Adding a NOT NULL column without a default |
| OPM012 | error | CREATE INDEX without CONCURRENTLY (Postgres only) |
| OPM013 | error | Adding NOT NULL to an existing column |
| OPM020 | warning | Missing reverse migration (reverse_code / downgrade / reverse_sql) |
| OPM021 | error | Data migration inside a schema transaction |
| OPM030 | warning | Column rename (breaks rolling deploys) |
| OPM031 | warning | Table rename (breaks rolling deploys) |

Indexes on tables created in the same migration are exempt from OPM012 (the
table is empty). OPM012 only fires for `--db postgres`.

### Project config — `.oneportmigraterc`

```yaml
db: postgres
rules:
  ignore: [OPM031]
  severity:
    OPM012: warning     # deterministic override, applies before the LLM runs
output:
  format: inline
  min_severity: warning
```

### Team guidelines — `.oneport/guidelines.md`

The same file oneport-review uses. The LLM reads it and may downgrade or
upgrade a finding's severity **only** when an entry explicitly covers it,
quoting the guideline in its reason:

```markdown
- our users table is small (<10k rows), downgrade lock severity
- the events table is 400M rows and append-only — never rewrite in place
```

## Honest limits

- **Raw SQL parsing is regex-based.** Statements are split on `;` without
  full string-literal awareness; `$$`-quoted function bodies and dynamic SQL
  (`EXECUTE format(...)`) are not analyzed. When the parser hits these it
  attaches a note to the output instead of guessing. Misses are false
  negatives — it never invents a finding.
- **Django `AlterField` / Alembic `alter_column(type_=...)`**: a single
  migration file doesn't show the *old* column definition, so every field
  alteration is flagged as a potential rewrite. You (or the LLM, with repo
  context) decide if it's actually a no-op.
- **`RunSQL` / `op.execute` with non-literal arguments** (variables,
  f-strings) can't be inspected — recorded as unanalyzed raw SQL with a note.
- **Table sizes are unknown to the rule engine.** "Large-table" judgment is
  exactly what the LLM layer is for; without an API key you get the
  deterministic worst-case severities.
- **`--staged` / `--head`** read file contents from the working tree.

## CI

See [examples/workflows/migrate-gate.yml](examples/workflows/migrate-gate.yml):

```yaml
- run: pip install oneport-migrate
- run: oneport-migrate check --head --post "$PR_URL"
  env:
    GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

`--post` submits one inline PR review per revision; a hidden marker in the
review body prevents duplicate reviews when CI re-runs on the same commit.

## Privacy

Serverless: your migration files go directly from your machine/CI to the
model API using **your** key. No Oneport server ever sees your code. See
[PRIVACY.md](PRIVACY.md).

## Part of the Oneport suite

- **oneport-review** — CodeRabbit-class AI code review
- **oneport-migrate** — this tool

## License

MIT
