# mgf-common v0.30.0 — Cutover guide

> **Audience:** consumer agents (and humans relaying for them)
> migrating from `mgf-common` v0.29.0 to v0.30.0.
> **What this is:** the canonical migration document for the v0.30
> federation paired-extraction. Companion guides:
> [`mgf-sqlalchemy/docs/cutover/v0.1.0.md`](https://codeberg.org/magogi-admin/mgf-sqlalchemy/src/branch/main/docs/cutover/v0.1.0.md),
> [`mgf-alembic/docs/cutover/v0.1.0.md`](https://codeberg.org/magogi-admin/mgf-alembic/src/branch/main/docs/cutover/v0.1.0.md),
> and [`mgf-fastapi/docs/cutover/v0.2.0.md`](https://codeberg.org/magogi-admin/mgf-fastapi/src/branch/main/docs/cutover/v0.2.0.md).

The third execution of the staged federation split per
[`docs/release/federation_roadmap.md`](../release/federation_roadmap.md).
The entire `mgf.common.db` and `mgf.common.alembic` subpackages move
to two new sibling distributions, plus the FastAPI-shaped helpers
migrate to mgf-fastapi v0.2.0:

- **`mgf-sqlalchemy`** v0.1.0 (import path `mgf.sqlalchemy`) — pure
  helpers `create_engine`, `create_sessionmaker`, `tenant_session`.
- **`mgf-alembic`** v0.1.0 (import path `mgf.alembic`) —
  `configure_env` (with PAPER-22 knobs).
- **`mgf-fastapi`** v0.2.0 (import path `mgf.fastapi.db`) —
  `get_session`, `setup_db` (gated behind the new
  `mgf-fastapi[sqlalchemy]` extra).

🔴 **breaking** for any consumer importing from `mgf.common.db.*` or
`mgf.common.alembic.*` or installing the
`[db]`/`[db-test]`/`[alembic]`/`[alembic-test]` extras. The migration
is a rename + dependency swap.

---

## At-a-glance

| Was (≤ mgf-common v0.29) | Is (mgf-common v0.30 + siblings) |
|---|---|
| `pip install 'mgf-common[db]'` | `pip install mgf-sqlalchemy` |
| `pip install 'mgf-common[db-test]'` | `pip install 'mgf-sqlalchemy[test]'` |
| `pip install 'mgf-common[alembic]'` | `pip install mgf-alembic` |
| `pip install 'mgf-common[alembic-test]'` | `pip install 'mgf-alembic[test]'` |
| `pip install 'mgf-common[db,fastapi]'` (FastAPI service) | `pip install 'mgf-fastapi[sqlalchemy]' mgf-sqlalchemy` |
| `from mgf.common.db import create_engine` | `from mgf.sqlalchemy import create_engine` |
| `from mgf.common.db import create_sessionmaker` | `from mgf.sqlalchemy import create_sessionmaker` |
| `from mgf.common.db import tenant_session` | `from mgf.sqlalchemy import tenant_session` |
| `from mgf.common.db import get_session` ⚠️ | `from mgf.fastapi.db import get_session` (sibling-different) |
| `from mgf.common.db import setup_db` ⚠️ | `from mgf.fastapi.db import setup_db` (sibling-different) |
| `from mgf.common.alembic import configure_env` | `from mgf.alembic import configure_env` |

**No code-shape changes.** `create_engine`, `create_sessionmaker`,
`tenant_session`, `configure_env` are byte-identical to mgf-common
v0.29.0 modulo the import paths. `get_session` and `setup_db` are
also byte-identical, just relocated to mgf-fastapi.

---

## Why FastAPI helpers ended up in `mgf-fastapi`, not `mgf-sqlalchemy`

The federation roadmap specified mgf-sqlalchemy as
**framework-agnostic** — no Starlette / FastAPI in its dep graph.
But two helpers in `mgf.common.db` were inherently FastAPI-shaped:

- `get_session` imports `starlette.requests.Request` at runtime
  (FastAPI Depends needs the type for introspection).
- `setup_db` takes a `FastAPI` app parameter and reads/writes
  `app.state.mgf_db_*`.

Pulling those into a "pure-SQLAlchemy" sibling would force a
Starlette runtime dep on consumers who never asked for FastAPI.
Splitting by shape — pure helpers in mgf-sqlalchemy, FastAPI helpers
in mgf-fastapi — keeps each sibling's dep graph honest.

The trade-off: FastAPI consumers who used these helpers now install
both `mgf-sqlalchemy` AND `mgf-fastapi[sqlalchemy]`. The `[sqlalchemy]`
extra is just a runtime dep on `mgf-sqlalchemy`; without it,
`import mgf.fastapi.db` raises `ImportError` at load time with a
message pointing at the missing extra.

Pure-SQLAlchemy consumers (data pipelines, CLI workers, non-FastAPI
services) shed a transitive Starlette import.

---

## Migration steps

### 1. Update your `pyproject.toml`

Pick the row that matches your existing shape:

#### Pure-SQLAlchemy consumer (no FastAPI helpers used)

```toml
# Before
dependencies = [
    "mgf-common[db]>=0.29,<0.30",
]

# After
dependencies = [
    "mgf-common>=0.30,<0.31",
    "mgf-sqlalchemy>=0.1,<0.2",
]
```

#### FastAPI consumer using `get_session` / `setup_db`

```toml
# Before
dependencies = [
    "mgf-common[db,fastapi]>=0.29,<0.30",  # or [db] + [fastapi] separately
]

# After
dependencies = [
    "mgf-common>=0.30,<0.31",
    "mgf-sqlalchemy>=0.1,<0.2",
    "mgf-fastapi[sqlalchemy]>=0.2,<0.3",
]
```

#### Alembic consumer

```toml
# Before
dependencies = [
    "mgf-common[alembic]>=0.29,<0.30",
]

# After
dependencies = [
    "mgf-common>=0.30,<0.31",
    "mgf-sqlalchemy>=0.1,<0.2",
    "mgf-alembic>=0.1,<0.2",
]
```

For test consumers add `[test]` to the relevant siblings.

### 2. Rewrite imports

```bash
# At your repo root.

# 1) FastAPI-shaped helpers go to mgf.fastapi.db (handle first — more
# specific full-line patterns beat the broader sed below).
git ls-files '*.py' | xargs sed -i \
    -e 's|from mgf\.common\.db import \(get_session\|setup_db\)|from mgf.fastapi.db import \1|g'

# 2) Pure-SQLAlchemy helpers go to mgf.sqlalchemy.
git ls-files '*.py' | xargs sed -i \
    -e 's|from mgf\.common\.db import \(create_engine\|create_sessionmaker\|tenant_session\)|from mgf.sqlalchemy import \1|g' \
    -e 's|from mgf\.common\.db\b|from mgf.sqlalchemy|g' \
    -e 's|import mgf\.common\.db\b|import mgf.sqlalchemy|g'

# 3) Alembic helper goes to mgf.alembic.
git ls-files '*.py' | xargs sed -i \
    -e 's|from mgf\.common\.alembic\b|from mgf.alembic|g' \
    -e 's|import mgf\.common\.alembic\b|import mgf.alembic|g'
```

For mixed imports like
`from mgf.common.db import create_engine, get_session, setup_db, tenant_session`,
split into two lines first by hand:

```python
# Before
from mgf.common.db import create_engine, get_session, setup_db, tenant_session

# After
from mgf.sqlalchemy import create_engine, tenant_session
from mgf.fastapi.db import get_session, setup_db
```

### 3. Re-run the four gates

```bash
uv sync
uv run ruff check
uv run mypy --strict src/
uv run pytest
```

### 4. Verify alembic migrations still run

```bash
uv run alembic upgrade head
```

The `configure_env` shape, including the PAPER-22
`include_object` / `include_schemas` knobs, is unchanged.

### 5. Re-run `mgf-fed catch-up`

```bash
mgf-fed catch-up
```

The audit's pin-checker recognises `mgf-sqlalchemy`, `mgf-alembic`,
and `mgf-fastapi[sqlalchemy]` as expected sibling deps. The
closed-box-leak checker also understands `mgf.sqlalchemy`,
`mgf.alembic`, and `mgf.fastapi.db` as sibling import paths.

---

## What changed internally

- **`mgf.common.db/`** and **`mgf.common.alembic/`** removed wholesale
  from `src/`. Their `__all__` exports moved to the siblings.
- **Test suites moved.** `tests/unit/db/` and `tests/unit/alembic/`
  removed from mgf-common. The tests live in
  `mgf-sqlalchemy/tests/unit/` (13), `mgf-alembic/tests/unit/` (16),
  and `mgf-fastapi/tests/unit/test_db.py` (6 — for the FastAPI
  Depends + lifespan integration shapes).
- **PUBLIC_API.json:** modules 18 → 16, total names 181 → 175.
- **`[db]`, `[db-test]`, `[alembic]`, `[alembic-test]` extras**
  removed from `pyproject.toml`. `[dev]` no longer pulls
  `sqlalchemy`, `aiosqlite`, `alembic`, `fastapi`, `uvicorn`,
  `httpx`, `respx` (the last two were incidental to `tests/unit/db/`
  integration tests).
- **`init-project` scaffold updated.** Per-kind dep lists now emit
  `mgf-fastapi[sqlalchemy]` + `mgf-sqlalchemy` + `mgf-alembic` for
  the `fastapi` kind; pin floors bump to `>=0.30,<0.31`.
- **`docs/recipes/db.md`** and **`docs/recipes/alembic.md`** removed
  from mgf-common; canonical recipes now ship from
  `mgf-sqlalchemy/docs/recipes/sqlalchemy.md` and
  `mgf-alembic/docs/recipes/alembic.md`.

---

## Per-consumer migration notes

### PlasmaMapper (FastAPI scientific platform)

**Migration touches:**
- `pyproject.toml` (replace `[db]` + `[fastapi]` extras with sibling deps).
- Every endpoint that uses DB session injection.
- `alembic/env.py` (one import rewrite).

Steps:
1. Bump pins per [Step 1 — FastAPI consumer](#fastapi-consumer-using-get_session--setup_db).
2. Run the sed pair from [Step 2](#2-rewrite-imports). Audit any
   mixed-import lines.
3. Rewrite `alembic/env.py` import (one line).
4. Re-run unit + integration tests.
5. PostGIS schema-filter callable (already plumbing through
   `include_object` per PAPER-22) is unchanged.

### inthewords (Django web)

**Not affected by mgf-sqlalchemy / mgf-alembic.** Django uses its own
ORM + migrations. Bump pin to `mgf-common>=0.30,<0.31`. No code
rewrite.

### vm-vmanager-app (Qt desktop)

**Not affected.** No DB / alembic / SQLAlchemy in vm-vmanager-app. Bump pin
to `mgf-common>=0.30,<0.31`. No rewrite.

### mgf-apiprobe

**Not affected.** No DB / alembic / SQLAlchemy in mgf-apiprobe.

---

## What's deliberately NOT in v0.30.0

- **Deprecation aliases.** Per the federation pattern (and AP-03's
  0.x window) we delete the subpackages cleanly. No
  `mgf.common.db = mgf.sqlalchemy` shim. Consumers rewrite imports
  in lockstep — the rewrite is mechanical.
- **A `mgf-db` meta-sibling.** `mgf-sqlalchemy` and `mgf-alembic`
  ship separately because alembic is genuinely optional. Consumers
  using SQLAlchemy without migrations don't pay alembic's install
  cost.
- **`mgf-django` extraction.** That's v0.31. Django consumers
  (inthewords) keep `mgf.common.django` for now.

---

## Engineering note

mgf-common v0.30.0 is the **third** of four planned extractions:

| Release | Sibling | Status |
|---|---|---|
| v0.28.0 | mgf-fastapi v0.1 | shipped |
| v0.29.0 | mgf-http v0.1 | shipped |
| **v0.30.0** | **mgf-sqlalchemy v0.1 + mgf-alembic v0.1 + mgf-fastapi v0.2** | **this release** |
| v0.31.0 | mgf-django | next |
| v0.32+ | stabilization | future |
| v1.0.0 | lock the federated surface | future |

The PEP 420 namespace package (no top-level `mgf/__init__.py` in
any sibling repo) means `mgf.sqlalchemy`, `mgf.alembic`,
`mgf.fastapi`, `mgf.fastapi.db`, `mgf.http`, and `mgf.common`
coexist seamlessly in the same Python process.
