1from __future__ import annotations
2
3import sys
4
5import click
6
7from ..convergence import execute_plan, plan_convergence
8
9
10@click.command()
11@click.option(
12 "--yes",
13 "-y",
14 is_flag=True,
15 help="Skip confirmation prompt.",
16)
17@click.option(
18 "--drop-undeclared",
19 is_flag=True,
20 help="Drop indexes and constraints not declared on any model.",
21)
22def converge(yes: bool, drop_undeclared: bool) -> None:
23 """Fix schema mismatches between models and the database.
24
25 Detects and fixes:
26 - Missing indexes (using CONCURRENTLY)
27 - Missing constraints (check, unique)
28 - NOT VALID constraints needing validation
29
30 With --drop-undeclared, also drops indexes and constraints that exist in the
31 database but are not declared on any model.
32
33 Without --drop-undeclared, exits non-zero if undeclared constraints remain
34 (constraints affect database behavior). Undeclared indexes are reported but
35 do not block success.
36
37 Each fix is applied and committed independently so partial
38 failures don't block subsequent fixes.
39 """
40 plan = plan_convergence()
41 items = plan.executable(drop_undeclared=drop_undeclared)
42 success = True
43
44 if items:
45 click.secho(
46 f"{len(items)} fix{'es' if len(items) != 1 else ''} to apply:\n",
47 bold=True,
48 )
49 for item in items:
50 click.echo(f" {item.describe()}")
51
52 click.echo()
53
54 if not yes:
55 if not click.confirm("Apply these changes?"):
56 return
57
58 click.echo()
59
60 result = execute_plan(items)
61
62 for r in result.results:
63 if r.ok:
64 click.echo(f" {r.sql}")
65 else:
66 click.secho(f" FAILED: {r.item.describe()} — {r.error}", fg="red")
67
68 click.echo()
69 click.secho(f" {result.summary}", fg="green" if result.ok else "yellow")
70 if not result.ok_for_sync:
71 success = False
72
73 if plan.blocked:
74 click.echo()
75 click.secho(" Schema changes require a staged rollout:", fg="red", bold=True)
76 for item in plan.blocked:
77 click.secho(f" {item.drift.describe()}", fg="red")
78 if item.guidance:
79 click.secho(f" {item.guidance}", fg="red", dim=True)
80 success = False
81
82 if not drop_undeclared and plan.blocking_cleanup:
83 click.echo()
84 click.secho(" Undeclared constraints still in database:", fg="red", bold=True)
85 for item in plan.blocking_cleanup:
86 click.secho(f" {item.describe()}", fg="red")
87 click.secho(" Rerun with --drop-undeclared to remove them.", fg="red")
88 success = False
89
90 if not drop_undeclared and plan.optional_cleanup:
91 click.echo()
92 for item in plan.optional_cleanup:
93 click.echo(f" {item.describe()}")
94 click.echo(" Run with --drop-undeclared to remove undeclared indexes.")
95
96 if not success:
97 sys.exit(1)
98 elif not items:
99 click.secho("Schema is converged — nothing to fix.", fg="green")