diff --git a/README.md b/README.md index f6c0a14..2eb8c59 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docsrc/custom_backends.qmd b/docsrc/custom_backends.qmd index 56e395b..9484aa3 100644 --- a/docsrc/custom_backends.qmd +++ b/docsrc/custom_backends.qmd @@ -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. diff --git a/fastmigrate/__init__.py b/fastmigrate/__init__.py index 85be2c8..23f9f9f 100644 --- a/fastmigrate/__init__.py +++ b/fastmigrate/__init__.py @@ -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. @@ -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"] diff --git a/fastmigrate/core.py b/fastmigrate/core.py index b97b2fa..69a68f8 100644 --- a/fastmigrate/core.py +++ b/fastmigrate/core.py @@ -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()) @@ -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. diff --git a/tests/test_custom_backend.py b/tests/test_custom_backend.py index 9c9af52..1cfa404 100644 --- a/tests/test_custom_backend.py +++ b/tests/test_custom_backend.py @@ -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: @@ -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: