#!/usr/bin/env python3 """One-off migration: `edgar_filings` primary key `accn` → `(cik, accn)` (co-registrants share accessions). # production (M3U96b) — BEFORE deploying the code that expects the composite key; stop the writers first: pm2 stop hfmarketdata-edgar-incremental hfmarketdata-edgar-backfill cd ~/hfmarketdata && HFMD_DATA_ROOT=~/firstratedata venv/bin/python scripts/migrate_edgar_filings.py pm2 start hfmarketdata-edgar-incremental The API may keep serving during the copy (WAL: readers are not blocked; a write to edgar_filings would wait on the busy timeout, hence stopping the ingestion jobs). The table is rebuilt in ONE transaction (`edgar_filings_new` ← INSERT OR IGNORE, drop, rename, 4 indexes) — on failure nothing changes. Idempotent: running it on a migrated database prints the state and exits 0. `--check` only reports. Expected duration: 1.37 M rows ≈ 212 MB → copy ~5–10 s + indexes ~10–20 s on the M3 Ultra SSD (≈ 30–60 s worst case); the WAL grows by ~300 MB and is checkpointed at the end. Author: Simon-Pierre Boucher """ from __future__ import annotations import argparse import logging import sys import time from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE.parent / "hfmarketdata" / "api")) log = logging.getLogger("migrate_edgar_filings") def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--check", action="store_true", help="report the current key shape and exit") ap.add_argument("-v", "--verbose", action="store_true") args = ap.parse_args() logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") from sqlalchemy import text from core.config import settings from core.db import engine from fundamentals import migrations log.info("database: %s", settings.state_db) with engine.connect() as con: state = migrations.filings_pk_is_composite(con) n = migrations.filings_row_count(con) if state is not None else 0 if state is None: log.info("edgar_filings does not exist yet — nothing to migrate (init_db will create it with the composite key)") return 0 if state: log.info("edgar_filings already keyed on (cik, accn) — %d rows, nothing to do", n) return 0 log.info("edgar_filings keyed on accn alone: %d rows to copy", n) if args.check: return 2 t0 = time.time() with engine.begin() as con: con.execute(text("PRAGMA busy_timeout=600000")) copied = migrations.migrate_filings_pk(con) with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as con: con.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")) ok = migrations.filings_pk_is_composite(con) log.info("done: %d rows copied (%d dropped as exact duplicates), composite key: %s, %.1fs", copied, n - copied, ok, time.time() - t0) return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())