feat(db_store): decouple migration from store construction
Remove auto-migration from DbStore.__init__; add explicit check_migrations() and migrate() methods. The CLI now checks both stores on every startup and prompts the user to confirm before migrating. supervise.prepare() calls .migrate() directly now that __init__ no longer does it implicitly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,11 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from ..audit_store import AuditStore
|
||||
from ..db_store import DbVersionError
|
||||
from ..log import Die, die, error
|
||||
from ..manifest import ManifestError
|
||||
from ..queue_store import QueueStore
|
||||
from ._common import PROG
|
||||
from . import list as _list_mod
|
||||
from .cleanup import cmd_cleanup
|
||||
@@ -74,6 +77,21 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if handler is None:
|
||||
usage()
|
||||
die(f"unknown command: {command}")
|
||||
queue_store = QueueStore("")
|
||||
audit_store = AuditStore()
|
||||
if not queue_store.check_migrations() or not audit_store.check_migrations():
|
||||
sys.stderr.write("bot-bottle: database schema is out of date\n")
|
||||
sys.stderr.write("Migrate now? [y/N] ")
|
||||
sys.stderr.flush()
|
||||
try:
|
||||
answer = sys.stdin.readline().strip().lower()
|
||||
except EOFError:
|
||||
answer = ""
|
||||
if answer != "y":
|
||||
error("migration required — re-run and confirm to migrate")
|
||||
return 1
|
||||
queue_store.migrate()
|
||||
audit_store.migrate()
|
||||
try:
|
||||
return handler(rest) or 0
|
||||
except ManifestError as e:
|
||||
|
||||
+22
-3
@@ -11,6 +11,10 @@ except ImportError:
|
||||
from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
||||
|
||||
|
||||
class DbVersionError(Exception):
|
||||
"""Raised when the on-disk schema is behind the current migration list."""
|
||||
|
||||
|
||||
class DbStore:
|
||||
"""Base for SQLite-backed stores. Subclasses resolve db_path then call super().__init__."""
|
||||
|
||||
@@ -18,14 +22,29 @@ class DbStore:
|
||||
self.db_path = db_path
|
||||
self._migrations = migrations
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._init()
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def _init(self) -> None:
|
||||
def check_migrations(self) -> bool:
|
||||
"""Return True if the DB is fully up-to-date, False if migration is needed."""
|
||||
if not self.db_path.exists():
|
||||
return False
|
||||
try:
|
||||
with self._connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT version FROM schema_versions WHERE module = ?",
|
||||
(self._migrations.schema_key,),
|
||||
).fetchone()
|
||||
except sqlite3.OperationalError:
|
||||
return False
|
||||
version = row[0] if row else 0
|
||||
return version == len(self._migrations.migrations)
|
||||
|
||||
def migrate(self) -> None:
|
||||
"""Apply any pending migrations and set permissions on the DB file."""
|
||||
with self._connect() as conn:
|
||||
self._migrations.apply(conn)
|
||||
self._chmod()
|
||||
@@ -37,4 +56,4 @@ class DbStore:
|
||||
pass
|
||||
|
||||
|
||||
__all__ = ["DbStore"]
|
||||
__all__ = ["DbStore", "DbVersionError"]
|
||||
|
||||
@@ -357,8 +357,8 @@ class Supervise(ABC):
|
||||
must be set by the launch step before .start runs."""
|
||||
del stage_dir
|
||||
db_path = host_db_path()
|
||||
QueueStore(slug)
|
||||
AuditStore(db_path)
|
||||
QueueStore(slug).migrate()
|
||||
AuditStore(db_path).migrate()
|
||||
return SupervisePlan(
|
||||
slug=slug,
|
||||
db_path=db_path,
|
||||
|
||||
Reference in New Issue
Block a user