From d05b0b39a85a51979d35abb488f9f12b608c53c6 Mon Sep 17 00:00:00 2001 From: Tim Stranske Date: Tue, 15 Sep 2026 01:30:20 -0500 Subject: [PATCH] fix(ui): resolve SQLite manager primary-key spelling in dashboard and daily report Use resolve_manager_id_column for manager selectors, QC summaries, and daily-report fallback joins so databases seeded with managers.id behave the same as manager_id. Co-authored-by: Cursor --- tests/test_daily_report_views.py | 72 ++++++++++++++++++++++++++++++++ tests/test_dashboard.py | 25 ++++++++++- ui/daily_report.py | 11 +++-- ui/dashboard.py | 9 +++- 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/tests/test_daily_report_views.py b/tests/test_daily_report_views.py index 024dcf9c..230456c7 100644 --- a/tests/test_daily_report_views.py +++ b/tests/test_daily_report_views.py @@ -4,6 +4,7 @@ from pathlib import Path import pandas as pd +import pytest import streamlit as st from ui import daily_report @@ -112,6 +113,77 @@ def metric(self, label, value, delta=None): assert any(call[0] == "Increased" and call[1] == "1" for call in metric_calls) +def _setup_fallback_diff_db(tmp_path: Path, manager_pk: str = "manager_id") -> str: + db_path = tmp_path / f"daily_report_fallback_{manager_pk}.db" + conn = sqlite3.connect(db_path) + conn.execute(f"CREATE TABLE managers ({manager_pk} INTEGER PRIMARY KEY, name TEXT NOT NULL)") + conn.execute( + "CREATE TABLE daily_diffs (" + "manager_id INTEGER NOT NULL, report_date TEXT NOT NULL, cusip TEXT NOT NULL, " + "name_of_issuer TEXT, delta_type TEXT NOT NULL, shares_prev INTEGER, shares_curr INTEGER, " + "value_prev REAL, value_curr REAL)" + ) + conn.execute( + "CREATE TABLE news_items (" + "manager_id INTEGER, headline TEXT, url TEXT, published_at DATETIME, " + "source TEXT, topics TEXT, confidence REAL)" + ) + conn.execute( + "CREATE TABLE activism_filings (" + "filing_id INTEGER PRIMARY KEY, manager_id INTEGER, filing_type TEXT, " + "subject_company TEXT, filed_date DATE)" + ) + conn.execute( + "CREATE TABLE activism_events (" + "event_id INTEGER PRIMARY KEY, manager_id INTEGER, filing_id INTEGER, event_type TEXT, " + "subject_company TEXT, ownership_pct REAL, previous_pct REAL, delta_pct REAL, " + "detected_at DATETIME)" + ) + conn.execute(f"INSERT INTO managers({manager_pk}, name) VALUES (1, 'Fallback Manager')") + conn.execute( + "INSERT INTO daily_diffs(manager_id, report_date, cusip, name_of_issuer, delta_type, " + "shares_prev, shares_curr, value_prev, value_curr) " + "VALUES (1, '2024-05-01', 'AAA', 'Issuer Alpha', 'INCREASE', 1000, 2500, 10000, 16000)" + ) + conn.execute( + "INSERT INTO news_items(manager_id, headline, url, published_at, source, topics, confidence) " + "VALUES (1, 'Headline', 'https://example.com', '2024-05-01 10:00:00', 'Wire', 'topic', 0.9)" + ) + conn.execute( + "INSERT INTO activism_filings(filing_id, manager_id, filing_type, subject_company, filed_date) " + "VALUES (10, 1, 'SC 13D', 'Target Co', '2024-05-01')" + ) + conn.execute( + "INSERT INTO activism_events(event_id, manager_id, filing_id, event_type, subject_company, " + "ownership_pct, previous_pct, delta_pct, detected_at) " + "VALUES (20, 1, 10, 'initial_stake', 'Target Co', 5.0, 0.0, 5.0, '2024-05-01 12:00:00')" + ) + conn.commit() + conn.close() + return str(db_path) + + +@pytest.mark.parametrize("manager_pk", ["manager_id", "id"]) +def test_daily_report_fallback_queries_resolve_manager_primary_key( + tmp_path, monkeypatch, manager_pk: str +): + db_path = _setup_fallback_diff_db(tmp_path, manager_pk=manager_pk) + monkeypatch.setenv("DB_PATH", db_path) + st.cache_data.clear() + + diffs = daily_report.load_diffs("2024-05-01") + assert len(diffs) == 1 + assert diffs.iloc[0]["manager_name"] == "Fallback Manager" + + news = daily_report.load_news("2024-05-01") + assert len(news) == 1 + assert news.iloc[0]["manager_name"] == "Fallback Manager" + + activism = daily_report.load_activism_events("2024-05-01") + assert len(activism) == 1 + assert activism.iloc[0]["manager_name"] == "Fallback Manager" + + def test_daily_report_page_renders_under_500ms_with_ten_managers(monkeypatch): manager_rows = [ { diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 224aab18..4afd8a2a 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -5,6 +5,7 @@ from time import perf_counter import pandas as pd +import pytest import streamlit as st sys.path.append(str(Path(__file__).resolve().parents[1])) @@ -41,10 +42,10 @@ ) -def setup_db(tmp_path: Path) -> str: +def setup_db(tmp_path: Path, manager_pk: str = "manager_id") -> str: db_path = tmp_path / "dev.db" conn = sqlite3.connect(db_path) - conn.execute("CREATE TABLE managers (manager_id INTEGER PRIMARY KEY, name TEXT)") + conn.execute(f"CREATE TABLE managers ({manager_pk} INTEGER PRIMARY KEY, name TEXT)") conn.execute( # No `filed` column: schema.sql keeps filing dates on filings.filed_date. "CREATE TABLE holdings (cik TEXT, accession TEXT, nameOfIssuer TEXT, " @@ -787,6 +788,26 @@ def test_load_managers_sorted(tmp_path: Path, monkeypatch): assert list(df["manager_id"]) == [1, 2] +@pytest.mark.parametrize("manager_pk", ["manager_id", "id"]) +def test_load_managers_and_qc_summary_resolve_manager_primary_key( + tmp_path: Path, monkeypatch, manager_pk: str +): + db_path = setup_db(tmp_path, manager_pk=manager_pk) + monkeypatch.setenv("DB_PATH", db_path) + st.cache_data.clear() + + managers = load_managers() + assert list(managers["manager_id"]) == [1, 2] + assert list(managers["name"]) == ["Alpha Partners", "Zulu Capital"] + + summary = load_all_managers_summary() + assert summary["total_managers"] == 2 + stale = summary["stale_managers"] + assert not stale.empty + assert set(stale["manager_id"]) == {1, 2} + assert set(stale["name"]) == {"Alpha Partners", "Zulu Capital"} + + class FakeStreamlit: def __init__(self): self.session_state = {} diff --git a/ui/daily_report.py b/ui/daily_report.py index cfb9cebd..a0135a59 100644 --- a/ui/daily_report.py +++ b/ui/daily_report.py @@ -7,7 +7,7 @@ import pandas as pd import streamlit as st -from adapters.base import connect_db +from adapters.base import connect_db, resolve_manager_id_column from api.signals import query_contrarian_signals, query_crowded_trades from . import require_login @@ -26,10 +26,11 @@ def load_diffs(date: str) -> pd.DataFrame: FROM mv_daily_report WHERE report_date = {placeholder} ORDER BY manager_name, delta_type""" + id_column = resolve_manager_id_column(conn) fallback_query = f"""SELECT m.name AS manager_name, d.cusip, d.name_of_issuer, d.delta_type, d.shares_prev, d.shares_curr, d.value_prev, d.value_curr FROM daily_diffs d - JOIN managers m ON m.manager_id = d.manager_id + JOIN managers m ON m.{id_column} = d.manager_id WHERE d.report_date = {placeholder} ORDER BY manager_name, d.delta_type""" try: @@ -51,11 +52,12 @@ def load_news(date: str) -> pd.DataFrame: conn = connect_db() try: if isinstance(conn, sqlite3.Connection): + id_column = resolve_manager_id_column(conn) query = ( "SELECT n.headline, n.url, n.published_at, n.source, n.topics, n.confidence, " "m.name AS manager_name " "FROM news_items n " - "LEFT JOIN managers m ON m.manager_id = n.manager_id " + f"LEFT JOIN managers m ON m.{id_column} = n.manager_id " "WHERE date(n.published_at) = ? " "ORDER BY n.published_at DESC " "LIMIT 50" @@ -94,12 +96,13 @@ def load_activism_events(date: str) -> pd.DataFrame: is_sqlite = isinstance(conn, sqlite3.Connection) placeholder = "?" if is_sqlite else "%s" try: + id_column = resolve_manager_id_column(conn) query = ( "SELECT m.name AS manager_name, ae.event_type, ae.subject_company, " "ae.ownership_pct, ae.previous_pct, ae.delta_pct, af.filed_date " "FROM activism_events ae " "JOIN activism_filings af ON af.filing_id = ae.filing_id " - "LEFT JOIN managers m ON m.manager_id = ae.manager_id " + f"LEFT JOIN managers m ON m.{id_column} = ae.manager_id " f"WHERE af.filed_date = {placeholder} " "ORDER BY af.filed_date DESC, ae.detected_at DESC" ) diff --git a/ui/dashboard.py b/ui/dashboard.py index c7e2a859..9db112cc 100644 --- a/ui/dashboard.py +++ b/ui/dashboard.py @@ -360,8 +360,9 @@ def load_qc_flags(manager_id: int) -> dict[str, Any]: def load_managers() -> pd.DataFrame: conn = connect_db() try: + id_column = resolve_manager_id_column(conn) df = pd.read_sql_query( - "SELECT manager_id, name FROM managers ORDER BY name", + f"SELECT {id_column} AS manager_id, name FROM managers ORDER BY name", conn, ) except Exception: @@ -1020,7 +1021,11 @@ def load_all_managers_summary() -> dict[str, Any]: ) summary["recent_activity"] = activity_df.sort_values("activity_date") - managers_df = pd.read_sql_query("SELECT manager_id, name FROM managers", conn) + id_column = resolve_manager_id_column(conn) + managers_df = pd.read_sql_query( + f"SELECT {id_column} AS manager_id, name FROM managers", + conn, + ) filings_df = pd.read_sql_query( "SELECT manager_id, filing_id, type, filed_date FROM filings", conn )