v0.154.0
 1"""Noticing when you've switched branches out from under your database.
 2
 3Databases record the branch they were last used on. When that changes and the
 4database turns out to be *ahead* of the code — carrying tables and migration
 5records this branch has never heard of — we say so, because nothing else will.
 6The app keeps working, queries keep succeeding, and the schema quietly doesn't
 7match the code.
 8
 9We report and step aside rather than acting. Repointing someone's database
10without being asked is worse than the stale schema it would fix, and the useful
11remedies (`plain db fork`, `plain db use`, `plain db reset`) are one command
12away once you know what happened.
13"""
14
15from __future__ import annotations
16
17from pathlib import Path
18
19import click
20
21from .cluster import Cluster
22from .identity import current_branch, resolve_database_name
23from .resolve import is_managed, open_cluster
24from .schema_state import migrations_not_on_disk
25
26
27def check_branch_switch(project_root: Path) -> None:
28    """Warn if this database belongs to a branch you've since left.
29
30    Only ever looks at a database we manage, and only reports — see the module
31    docstring. Never raises: this is advisory, and a failure here must not stop
32    `plain dev` from starting.
33    """
34    if not is_managed(project_root):
35        return
36
37    cluster = open_cluster(project_root, create=False)
38    if cluster is None:
39        return
40
41    db_name = resolve_database_name(project_root)
42    if not cluster.database_exists(db_name):
43        return
44
45    branch = current_branch(project_root)
46    metadata = cluster.get_metadata(db_name) or {}
47    recorded_branch = metadata.get("branch")
48
49    if branch and recorded_branch != branch:
50        _report_if_ahead(
51            cluster,
52            db_name=db_name,
53            previous_branch=recorded_branch,
54            branch=branch,
55        )
56        cluster.update_metadata(db_name, branch=branch)
57
58
59def _report_if_ahead(
60    cluster: Cluster,
61    *,
62    db_name: str,
63    previous_branch: str | None,
64    branch: str,
65) -> None:
66    ahead = migrations_not_on_disk(cluster.url(db_name))
67    if not ahead:
68        return
69
70    listed = ", ".join(f"{package}.{name}" for package, name in ahead[:3])
71    if len(ahead) > 3:
72        listed += f" (and {len(ahead) - 3} more)"
73
74    origin = f" from {previous_branch!r}" if previous_branch else ""
75    click.secho(
76        f"⚠ Database {db_name!r} is ahead of this branch.",
77        fg="yellow",
78    )
79    click.echo(
80        f"  It still has {len(ahead)} migration(s){origin} that {branch!r} "
81        f"doesn't have on disk: {listed}."
82    )
83    click.echo(
84        "  Your schema has tables this branch's code doesn't know about. To get "
85        "a clean one:\n"
86        "    plain db fork <name> --from <main>   # a copy without those changes\n"
87        "    plain db reset                       # empty, then `plain postgres sync`\n"
88        "  Or keep going — nothing is broken, the schema is just wider than the code."
89    )