Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2126,3 +2126,50 @@ shfmt-fix: shell-linters-install ## 🎨 Auto-format *.sh in place
@echo "🎨 Formatting shell scripts with shfmt -w…"
@shfmt -w -i 4 -ci $(SHELL_SCRIPTS)
@echo "✅ shfmt formatting done."


# 🛢️ ALEMBIC DATABASE MIGRATIONS
# =============================================================================
# help: 🛢️ ALEMBIC DATABASE MIGRATIONS
# help: alembic-install - Install Alembic CLI (and SQLAlchemy) in the current env
# help: db-new - Create a new migration (override with MSG="your title")
# help: db-up - Upgrade DB to the latest revision (head)
# help: db-down - Downgrade one revision (override with REV=<id|steps>)
# help: db-current - Show the current head revision for the database
# help: db-history - Show the full migration graph / history
# help: db-revision-id - Echo just the current revision id (handy for scripting)
# -----------------------------------------------------------------------------

# ──────────────────────────
# Internals & defaults
# ──────────────────────────
ALEMBIC ?= alembic # Override to e.g. `poetry run alembic`
MSG ?= "auto migration"
REV ?= -1 # Default: one step down; can be hash, -n, +n, etc.

.PHONY: alembic-install db-new db-up db-down db-current db-history db-revision-id

alembic-install:
@echo "➜ Installing Alembic …"
pip install --quiet alembic sqlalchemy

db-new:
@echo "➜ Generating revision: $(MSG)"
$(ALEMBIC) revision --autogenerate -m $(MSG)

db-up:
@echo "➜ Upgrading database to head …"
$(ALEMBIC) upgrade head

db-down:
@echo "➜ Downgrading database → $(REV) …"
$(ALEMBIC) downgrade $(REV)

db-current:
$(ALEMBIC) current

db-history:
$(ALEMBIC) history --verbose

db-revision-id:
@$(ALEMBIC) current --verbose | awk '/Current revision/ {print $$3}'
141 changes: 141 additions & 0 deletions alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# A generic, single database configuration.

[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .


# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =

# max length of characters to apply to the "slug" field
# truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions

# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os

# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname


[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples

# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME

# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME

# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARNING
handlers = console
qualname =

[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
172 changes: 172 additions & 0 deletions alembic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Alembic Migration Guide for `mcpgateway`

> Creating, applying, and managing schema migrations with Alembic.

---

## Table of Contents

1. [Why Alembic?](#why-alembic)
2. [Prerequisites](#prerequisites)
3. [Directory Layout](#directory-layout)
4. [Everyday Workflow](#everyday-workflow)
5. [Helpful Make Targets](#helpful-make-targets)
6. [Troubleshooting](#troubleshooting)
7. [Further Reading](#further-reading)

---

## Why Alembic?

- **Versioned DDL** — Revisions are timestamped, diff-able, and reversible.
- **Autogeneration** — Detects model vs. DB drift and writes `op.create_table`, `op.add_column`, etc.
- **Multi-DB Support** — Works with SQLite, PostgreSQL, MySQL—anything SQLAlchemy supports.
- **Zero Runtime Cost** — Only runs when you call it (dev, CI, deploy).

---

## Prerequisites

```bash
# Activate your virtual environment first
pip install --upgrade alembic
```

You do not need to set up `alembic.ini`, `env.py`, or metadata wiring - they're already configured.

---

## Directory Layout

```
alembic.ini
alembic/
├── env.py
├── script.py.mako
└── versions/
├── 20250626235501_initial_schema.py
└── ...
```

* `alembic.ini`: Configuration file
* `env.py`: Connects Alembic to your models and DB settings
* `script.py.mako`: Template for new revisions (keep this!)
* `versions/`: Contains all migration scripts

---

## Everyday Workflow

> **1 Edit → 2 Revision → 3 Upgrade**

| Step | What you do |
| ------------------------ | ----------------------------------------------------------------------------- |
| **1. Change models** | Modify SQLAlchemy models in `mcpgateway.db` or its submodules. |
| **2. Generate revision** | Run: `MSG="add users table"` then `alembic revision --autogenerate -m "$MSG"` |
| **3. Review** | Open the new file in `alembic/versions/`. Verify the operations are correct. |
| **4. Upgrade DB** | Run: `alembic upgrade head` |
| **5. Commit** | Run: `git add alembic/versions/*.py` |

### Other Common Commands

```bash
alembic current # Show current DB revision
alembic history --verbose # Show all migrations and their order
alembic downgrade -1 # Roll back one revision
alembic downgrade <rev> # Roll back to a specific revision hash
```

---

## ✅ Make Targets: Alembic Migration Commands

These targets help you manage database schema migrations using Alembic.

> You must have a valid `alembic/` setup and a working SQLAlchemy model base (`Base.metadata`).

---

### 💡 List all available targets (with help)

```bash
make help
```

This will include the Alembic section:

```
# 🛢️ Alembic tasks
db-new Autogenerate revision (MSG="title")
db-up Upgrade DB to head
db-down Downgrade one step (REV=-1 or hash)
db-current Show current DB revision
db-history List the migration graph
```

---

### 🔨 Commands

| Command | Description |
| -------------------------- | ------------------------------------------------------ |
| `make db-new MSG="..."` | Generate a new migration based on model changes. |
| `make db-up` | Apply all unapplied migrations. |
| `make db-down` | Roll back the latest migration (`REV=-1` by default). |
| `make db-down REV=abc1234` | Roll back to a specific revision by hash. |
| `make db-current` | Print the current revision ID applied to the database. |
| `make db-history` | Show the full migration history and graph. |

---

### 📌 Examples

```bash
# Create a new migration with a custom message
make db-new MSG="add users table"

# Apply it to the database
make db-up

# Downgrade the last migration
make db-down

# Downgrade to a specific revision
make db-down REV=cf1283d7fa92

# Show the current applied revision
make db-current

# Show all migration history
make db-history
```

---

### 🛑 Notes

* You must **edit models first** before `make db-new` generates anything useful.
* Always **review generated migration files** before committing.
* Don't forget to run `make db-up` on CI or deploy if using migrations to manage schema.

---

## Troubleshooting

| Symptom | Cause / Fix |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Empty migration (`pass`)** | Alembic couldn't detect models. Make sure all model classes are imported before `Base.metadata` is used (already handled in your `env.py`). |
| **`Can't locate revision ...`** | You deleted or renamed a revision file that the DB is pointing to. Either restore it or run `alembic stamp base` and recreate the revision. |
| **`script.py.mako` missing** | This file is required. Run `alembic init alembic` in a temp folder and copy the missing template into your project. |
| **SQLite foreign key limitations** | SQLite doesn't allow dropping constraints. Use `create table → copy → drop` flow manually, or plan around it. |
| **DB not updating** | Did you forget to run `alembic upgrade head`? Check with `alembic current`. |
| **Wrong DB URL or config errors** | Confirm `settings.database_url` is valid. Check `env.py` and your `.env`/config settings. Alembic ignores `alembic.ini` for URLs in your setup. |
| **Model changes not detected** | Alembic only picks up declarative models in `Base.metadata`. Ensure all models are imported and not behind `if TYPE_CHECKING:` or other lazy imports. |

---

## Further Reading

* Official docs: [https://alembic.sqlalchemy.org](https://alembic.sqlalchemy.org)
* Autogenerate docs: [https://alembic.sqlalchemy.org/en/latest/autogenerate.html](https://alembic.sqlalchemy.org/en/latest/autogenerate.html)

---
Loading
Loading