Yoyo Migrations
Yoyo Migrations is a robust, database-agnostic migration tool for Python projects, enabling users to manage SQL-based schema changes. It supports various database systems with both synchronous and asynchronous drivers. Currently at version 9.0.0, it maintains an active release cadence, introducing new features and refining API usability while periodically updating Python version support.
Warnings
- breaking Yoyo 9.0.0 dropped support for Python 3.5 and 3.6. Users must be on Python 3.7 or newer to use Yoyo 9.x.
- breaking In Yoyo 9.0.0, the database connection parameters changed from multiple `dbapi`-style arguments (e.g., `host='localhost', user='user'`) to a single URL connection string (e.g., `postgresql://user:password@host/dbname`).
- gotcha Yoyo 9.0.0 introduced extensive `asyncio` support. The `yoyo.connections.connect()` function now returns an `AsyncConnection` object if an async driver is detected, requiring all subsequent database operations (e.g., `apply_migrations`, `cursor().execute()`) to be `await`ed. For sync drivers, it returns a `SyncConnection` and no `await` is needed.
- gotcha Migration files (SQL or Python scripts) should ideally be idempotent or carefully managed to avoid issues during re-application or rollbacks. If a migration is not idempotent, re-running it on an already migrated database could lead to errors.
- gotcha While Yoyo has a programmatic API, its primary interface is the CLI, which relies on a `yoyo.ini` configuration file and a dedicated `migrations` directory. Programmatic usage requires explicitly passing migration directories and connection details, as `yoyo.ini` is not automatically picked up.
Install
-
pip install yoyo-migrations -
pip install yoyo-migrations[psycopg2-binary] -
pip install yoyo-migrations[mysqlclient]
Imports
- get_migrations
from yoyo import get_migrations
- connect
from yoyo.connections import connect
- Migration
from yoyo.migrations import Migration
Quickstart
import asyncio
import os
from pathlib import Path
from yoyo import get_migrations
from yoyo.connections import connect
# Create a temporary directory for migrations
TEMP_MIGRATIONS_DIR = Path('./temp_yoyo_migrations_quickstart')
TEMP_MIGRATIONS_DIR.mkdir(exist_ok=True)
# Create dummy migration files
(TEMP_MIGRATIONS_DIR / '0001.create_table.sql').write_text(
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);'
)
(TEMP_MIGRATIONS_DIR / '0002.add_email_field.sql').write_text(
'ALTER TABLE users ADD COLUMN email TEXT;'
)
# Use an in-memory SQLite database for the example
DB_URI = "sqlite:///:memory:"
async def run_yoyo_programmatically():
print(f"Connecting to database: {DB_URI}")
backend = await connect(DB_URI)
try:
migrations = get_migrations(TEMP_MIGRATIONS_DIR)
print(f"Found {len(migrations)} migrations in {TEMP_MIGRATIONS_DIR}")
with backend.transaction():
to_apply = backend.to_apply(migrations)
if to_apply:
applied_migrations = await backend.apply_migrations(to_apply)
print(f"Successfully applied {len(applied_migrations)} migrations.")
else:
print("No new migrations to apply.")
# Example of checking current schema (simplified)
cursor = await backend.cursor()
await cursor.execute("PRAGMA table_info(users);")
schema = await cursor.fetchall()
print("Current 'users' table schema:")
for col in schema:
print(f" - {col[1]} ({col[2]})") # col[1]=name, col[2]=type
await cursor.close()
finally:
await backend.close()
# Clean up temporary migration files and directory
for f in TEMP_MIGRATIONS_DIR.iterdir():
f.unlink()
TEMP_MIGRATIONS_DIR.rmdir()
if __name__ == '__main__':
asyncio.run(run_yoyo_programmatically())