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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ if not run_migrations(db_path, migrations_dir):
print("Database migration failed!")
```

`arun_migrations` is the awaitable form of `run_migrations` for async applications. It runs a custom backend's hooks on the calling event loop, and runs the SQLite path in a worker thread.

This will create a db if needed. Then, fastmigrate will detect every validly-named migration script in the migrations directory, select the ones with version numbers greater than the current db version number, and run the migration in alphabetical order, updating the db's version number as it proceeds, stopping if any migration fails.

This will guarantee that all subsequent code will encounter a database at the schema version defined by your highest-numbered migration script. So when you deploy updates to your app, those updates should include any new migration scripts along with modifications to code, which should now expect the new db schema.
Expand Down
2 changes: 2 additions & 0 deletions docsrc/custom_backends.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ DB-specific operations to it:
`.py` and `.sh` migrations are executed as separate processes as usual, with
`str(db)` passed as the first positional argument.

`fastmigrate.arun_migrations()` does the same from async code. It awaits the hooks on the calling event loop instead of a private one.

## Minimal required functions

Your `migrations/config.py` must define the following functions.
Expand Down
4 changes: 2 additions & 2 deletions fastmigrate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@



from fastmigrate.core import ( run_migrations, setup_logging, create_db, ensure_versioned_db, get_db_version, create_db_backup, create_database_backup,)
from fastmigrate.core import ( run_migrations, arun_migrations, setup_logging, create_db, ensure_versioned_db, get_db_version, create_db_backup, create_database_backup,)

# Optional: recreate_table depends on apswutils, which is not required for the
# core migration runner.
Expand All @@ -12,5 +12,5 @@
def recreate_table(*args, **kwargs): # type: ignore
raise ImportError( "fastmigrate.recreate_table requires the optional 'apswutils' dependency")

__all__ = ["run_migrations", "setup_logging", "create_db", "get_db_version", "create_db_backup", "recreate_table",
__all__ = ["run_migrations", "arun_migrations", "setup_logging", "create_db", "get_db_version", "create_db_backup", "recreate_table",
"ensure_versioned_db", "create_database_backup"]
17 changes: 16 additions & 1 deletion fastmigrate/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@



__all__ = ["run_migrations", "create_db", "get_db_version", "create_db_backup", "setup_logging", "ensure_versioned_db", "create_database_backup"]
__all__ = ["run_migrations", "arun_migrations", "create_db", "get_db_version", "create_db_backup", "setup_logging", "ensure_versioned_db", "create_database_backup"]

_logger = logging.getLogger("fastmigrate")
_logger.addHandler(logging.NullHandler())
Expand Down Expand Up @@ -495,6 +495,21 @@ async def _run_migrations_with_backend_async( db: Any, migrations_dir: Path, bac
if backend.close_connection is not None: await _maybe_await(backend.close_connection(conn))


async def arun_migrations( db_path: Any, migrations_dir: Path, verbose: bool = False,) -> bool:
"""Awaitable form of ``run_migrations`` for async applications.

A custom backend's hooks run on the calling event loop. The SQLite path
runs ``run_migrations`` in a worker thread.

Returns True if all migrations succeed, False otherwise.
"""
if verbose: setup_logging(True)
migrations_dir = Path(migrations_dir)
backend = _load_user_backend(migrations_dir)
if backend is None: return await asyncio.to_thread(run_migrations, db_path, migrations_dir)
_logger.debug(f"db_exists={_debug_db_exists(db_path)}")
return bool(await _run_migrations_with_backend_async(db_path, migrations_dir, backend, verbose))

def run_migrations( db_path: Any, migrations_dir: Path, verbose: bool = False,) -> bool:
"""Run all pending migrations.

Expand Down
6 changes: 4 additions & 2 deletions tests/test_custom_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
"""


import asyncio
import textwrap
import sqlite3
from pathlib import Path

import pytest

from fastmigrate.core import run_migrations
from fastmigrate.core import arun_migrations, run_migrations


def test_custom_config_with_sqlalchemy_sqlite(tmp_path: Path) -> None:
Expand Down Expand Up @@ -175,7 +176,8 @@ async def execute_sql(conn, sql: str):
"INSERT INTO things VALUES (1, 'hello');"
)

assert run_migrations(db_path, migrations_dir, verbose=True) is True
assert asyncio.run(arun_migrations(db_path, migrations_dir, verbose=True)) is True
assert run_migrations(db_path, migrations_dir) is True

conn = duckdb.connect(str(db_path))
try:
Expand Down