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 )