Recently Written · git

subplz-web

git clone https://github.com/equwal/subplz-web

Log | Files | Refs


commit f2084ad859803b32a8c12da0e5255eb9fa77c9ed
equwal <truex@equwal.com>
2026-09-26 21:58:49 -0700

Copy the tables of every model module in the burst database copy

The burst runbook runs python -m tools.copy_database in a new
interpreter, once when burst mode goes on and once when it goes off.
The tool imported only backend.db, so Base.metadata held only the
tables of backend/db.py. The copy skipped caption_balances,
subrep_licences and waitlist. After the switch, init_db made these
tables again, empty, and each user lost the cloud caption hours that
they paid for. A model table of a later module, for example
play_purchases, had the same problem.

The tool now imports backend.main. backend.main imports every model
module, as the running app does, so the copy gets the same tables as
init_db.

The new test runs the command in a subprocess, because the test
process imported backend.main already through tests/conftest.py.
Without the fix, the test fails: the target has no caption_balances,
subrep_licences or waitlist table.

 tests/test_copy_database.py | 35 ++++++++++++++++++++++++++++++++++-
 tools/copy_database.py      |  4 ++++
 2 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/tests/test_copy_database.py b/tests/test_copy_database.py
index bb70a67..2f39fc9 100644
--- a/tests/test_copy_database.py
+++ b/tests/test_copy_database.py
@@ -2,15 +2,21 @@
 
 from __future__ import annotations
 
+import os
+import subprocess
+import sys
 from datetime import date, datetime, timedelta, timezone
+from pathlib import Path
 
 import pytest
 from hypothesis import HealthCheck, given, settings
 from hypothesis import strategies as st
-from sqlalchemy import create_engine, select
+from sqlalchemy import create_engine, inspect, select
 from sqlalchemy.orm import Session
 
+from backend.captions import CaptionBalance
 from backend.db import Account, Artifact, Base, Job, JobStatus, Purchase
+from backend.subrep import SubrepLicence, WaitlistEntry
 from tools.copy_database import copy_database
 
 T0 = datetime(2026, 9, 22, tzinfo=timezone.utc)
@@ -81,3 +87,30 @@ def test_the_copy_refuses_a_target_with_rows(tmp_path):
             s.commit()
     with pytest.raises(RuntimeError, match="accounts"):
         copy_database(source, target)
+
+
+def test_the_command_copies_the_tables_of_every_model_module(tmp_path):
+    """The burst runbook runs the tool in a new interpreter. There, only the
+    modules that the tool imports put their tables in Base.metadata. This test
+    process imported backend.main already (conftest), so only a subprocess
+    shows a model module that the tool does not import."""
+    source, target = f"sqlite:///{tmp_path / 'source.db'}", f"sqlite:///{tmp_path / 'target.db'}"
+    engine = create_engine(source)
+    Base.metadata.create_all(engine)
+    with Session(engine) as s:
+        s.add(Account(id="acct_1", device_token="dev_1", email="reader@example.com"))
+        s.add(CaptionBalance(account_id="acct_1", seconds_left=20 * 3600, seconds_used=60))
+        s.add(SubrepLicence(account_id="acct_1", refresh_key="key_1", subscription_id="sub_1",
+                            status="active", plan_id="subrep_year", period_end=T0))
+        s.add(WaitlistEntry(kind="ios", email="reader@example.com", account_id="acct_1"))
+        s.commit()
+
+    env = {**os.environ, "SUBPLZ_WEB_DATA_DIR": str(tmp_path)}
+    root = Path(__file__).resolve().parent.parent
+    out = subprocess.run([sys.executable, "-m", "tools.copy_database", source, target],
+                         cwd=root, env=env, capture_output=True, text=True, timeout=120,
+                         check=False)
+    assert out.returncode == 0, out.stderr
+
+    assert set(Base.metadata.tables) <= set(inspect(create_engine(target)).get_table_names())
+    assert rows(target) == rows(source)
diff --git a/tools/copy_database.py b/tools/copy_database.py
index b59be97..2ab5c3d 100644
--- a/tools/copy_database.py
+++ b/tools/copy_database.py
@@ -18,6 +18,10 @@ from datetime import datetime, timezone
 
 from sqlalchemy import DateTime, create_engine, func, insert, select
 
+# The runbook runs this tool in a new interpreter. There, Base.metadata holds
+# only the tables of the modules that Python imported. backend.main imports
+# every model module, as the app does, so the copy gets every table.
+import backend.main  # noqa: F401
 from backend.db import Base
 
 BATCH = 1000