v0.154.0
 1"""How far apart are this database's schema and this checkout's code?
 2
 3Two directions, and they mean very different things:
 4
 5- **behind** — migrations on disk the database hasn't applied. Self-correcting;
 6  `plain postgres sync` applies them on the next `plain dev`.
 7- **ahead** — migrations recorded in the database with no file on disk. Not
 8  self-correcting, and the usual cause is switching branches: you applied a
 9  migration on one branch, switched away, and the table is still there.
10"""
11
12from __future__ import annotations
13
14from collections.abc import Generator
15from contextlib import contextmanager
16from typing import Any
17
18
19@contextmanager
20def _connected(url: str) -> Generator[Any]:
21    """Open `url` and install it as the active connection.
22
23    The migration recorder reads applied rows through `Migration.query`, which
24    uses the *global* connection rather than one passed in — so it has to be
25    installed, the same way `use_test_database` does it.
26    """
27    from plain.postgres.connection import DatabaseConnection
28    from plain.postgres.database_url import parse_database_url
29    from plain.postgres.db import _db_conn
30    from plain.postgres.sources import DirectSource
31
32    conn = DatabaseConnection(DirectSource(parse_database_url(url)))
33    token = _db_conn.set(conn)
34    try:
35        yield conn
36    finally:
37        _db_conn.reset(token)
38        conn.close()
39
40
41def pending_migration_count(url: str) -> int:
42    """Migrations on disk that this database hasn't applied yet."""
43    from plain.postgres.migrations.executor import MigrationExecutor
44
45    with _connected(url) as conn:
46        executor = MigrationExecutor(conn)
47        return len(executor.migration_plan(executor.loader.graph.leaf_nodes()))
48
49
50def migrations_not_on_disk(url: str) -> list[tuple[str, str]]:
51    """Migrations this database has applied that no longer exist as files.
52
53    This is the "your database is ahead of your code" signal. The same
54    comparison backs the `postgres.prunable_migrations` preflight check, but
55    the remedy differs by context: prune is right when a migration was deleted
56    for good, and wrong when you simply switched branches and will switch back.
57    """
58    from plain.postgres.migrations.loader import MigrationLoader
59    from plain.postgres.migrations.recorder import MigrationRecorder
60
61    with _connected(url) as conn:
62        loader = MigrationLoader(conn, ignore_no_migrations=True)
63        applied = MigrationRecorder(conn).applied_migrations()
64        on_disk = loader.disk_migrations or {}
65        return sorted(m for m in applied if m not in on_disk)