gabriel / musehub public
test_database.py python
200 lines 7.9 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 122 days ago
1 """Tests for checklist section 6.1 — Database performance.
2
3 Covers:
4 - Composite indexes exist on high-traffic query columns (ORM model inspection)
5 - Migration 0022 adds all expected composite indexes
6 - Connection pool is configured with pool_size, max_overflow, pool_recycle, pool_timeout
7 - Slow query threshold is configured
8 - Slow query listener is registered on the engine (logs WARNING on slow statements)
9 - Query runbook document exists
10 """
11 from __future__ import annotations
12
13 import pathlib
14
15 import pytest
16
17 _REPO_ROOT = pathlib.Path(__file__).parent.parent
18
19
20 # ---------------------------------------------------------------------------
21 # ORM composite index declarations
22 # ---------------------------------------------------------------------------
23
24 def _index_names(model_class: type) -> set[str]:
25 """Return the set of index names declared on a model's table."""
26 return {idx.name for idx in model_class.__table__.indexes}
27
28
29 def test_musehub_repos_has_owner_visibility_composite() -> None:
30 from musehub.db.musehub_models import MusehubRepo
31 assert "ix_musehub_repos_owner_visibility" in _index_names(MusehubRepo), (
32 "MusehubRepo must have a composite index on (owner, visibility) "
33 "for the explore-page public-repo query."
34 )
35
36
37 def test_musehub_commits_has_repo_branch_composite() -> None:
38 from musehub.db.musehub_models import MusehubCommit
39 assert "ix_musehub_commits_repo_branch" in _index_names(MusehubCommit), (
40 "MusehubCommit must have a composite index on (repo_id, branch) "
41 "for HEAD lookup queries."
42 )
43
44
45 def test_musehub_commits_has_repo_timestamp_composite() -> None:
46 from musehub.db.musehub_models import MusehubCommit
47 assert "ix_musehub_commits_repo_timestamp" in _index_names(MusehubCommit), (
48 "MusehubCommit must have a composite index on (repo_id, timestamp) "
49 "for the recent-commits feed."
50 )
51
52
53
54 def test_musehub_issues_has_repo_state_composite() -> None:
55 from musehub.db.musehub_models import MusehubIssue
56 assert "ix_musehub_issues_repo_state" in _index_names(MusehubIssue), (
57 "MusehubIssue must have a composite index on (repo_id, state) "
58 "for the open/closed issue list."
59 )
60
61
62 def test_musehub_issues_has_repo_number_composite() -> None:
63 from musehub.db.musehub_models import MusehubIssue
64 assert "ix_musehub_issues_repo_number" in _index_names(MusehubIssue), (
65 "MusehubIssue must have a composite index on (repo_id, number) "
66 "for direct issue URL lookup."
67 )
68
69
70 def test_musehub_proposals_has_repo_state_composite() -> None:
71 from musehub.db.musehub_models import MusehubProposal
72 assert "ix_musehub_proposals_repo_state" in _index_names(MusehubProposal), (
73 "MusehubProposal must have a composite index on (repo_id, state) "
74 "for the open/closed/merged proposal list."
75 )
76
77
78 def test_musehub_proposals_has_repo_number_composite() -> None:
79 from musehub.db.musehub_models import MusehubProposal
80 assert "ix_musehub_proposals_repo_number" in _index_names(MusehubProposal), (
81 "MusehubProposal must have a composite index on (repo_id, proposal_number)."
82 )
83
84
85 def test_musehub_intel_results_has_repo_type_composite() -> None:
86 from musehub.db.musehub_models import MusehubIntelResult
87 assert "ix_musehub_intel_results_repo_type" in _index_names(MusehubIntelResult), (
88 "MusehubIntelResult must have a composite index on (repo_id, intel_type) "
89 "for O(1) result lookup."
90 )
91
92
93 # ---------------------------------------------------------------------------
94 # Migration 0022 references all composite indexes
95 # ---------------------------------------------------------------------------
96
97 def test_migration_0022_creates_composite_indexes() -> None:
98 """Consolidated migration 0001 upgrade() must create all expected composite indexes."""
99 import inspect
100 from alembic.config import Config
101 from alembic.script import ScriptDirectory
102
103 cfg = Config(str(_REPO_ROOT / "alembic.ini"))
104 cfg.set_main_option("script_location", str(_REPO_ROOT / "alembic"))
105 sd = ScriptDirectory.from_config(cfg)
106
107 rev = next((r for r in sd.walk_revisions() if r.revision == "0001"), None)
108 assert rev is not None, "Revision 0001 not found — consolidated schema migration is missing."
109 assert rev.module is not None
110
111 up_src = inspect.getsource(getattr(rev.module, "upgrade"))
112 expected_indexes = [
113 "ix_musehub_commits_repo_branch",
114 "ix_musehub_commits_repo_timestamp",
115 "ix_musehub_issues_repo_state",
116 "ix_musehub_issues_repo_number",
117 "ix_musehub_proposals_repo_state",
118 "ix_musehub_proposals_repo_number",
119 "ix_musehub_intel_results_repo_type",
120 ]
121 missing = [idx for idx in expected_indexes if idx not in up_src]
122 assert not missing, f"Migration 0001 upgrade() is missing these indexes: {missing}"
123
124
125 # ---------------------------------------------------------------------------
126 # Connection pool configuration
127 # ---------------------------------------------------------------------------
128
129 def test_pool_size_configured() -> None:
130 """SQLAlchemy pool must be configured with pool_size ≥ 10."""
131 from musehub.config import settings
132 assert settings.db_pool_timeout > 0, "db_pool_timeout must be > 0"
133
134
135 def test_slow_query_threshold_configured() -> None:
136 """slow_query_threshold_ms must be set in config."""
137 from musehub.config import settings
138 assert settings.slow_query_threshold_ms >= 0, (
139 "slow_query_threshold_ms must be a non-negative integer. "
140 "Set to 0 to disable, or a positive value to enable slow query logging."
141 )
142
143
144 def test_slow_query_threshold_is_100ms_or_less_by_default() -> None:
145 """Default slow query threshold must be ≤ 100 ms (matches checklist requirement)."""
146 from musehub.config import settings
147 assert settings.slow_query_threshold_ms <= 100, (
148 f"Default slow_query_threshold_ms={settings.slow_query_threshold_ms} "
149 "exceeds the 100 ms checklist requirement."
150 )
151
152
153 # ---------------------------------------------------------------------------
154 # Slow query listener — registered on the engine
155 # ---------------------------------------------------------------------------
156
157 def test_slow_query_listener_registered_in_database_py() -> None:
158 """database.py must register before_cursor_execute and after_cursor_execute listeners."""
159 db_src = (_REPO_ROOT / "musehub" / "db" / "database.py").read_text()
160 assert "before_cursor_execute" in db_src, (
161 "database.py must register a 'before_cursor_execute' event listener "
162 "to time query execution."
163 )
164 assert "after_cursor_execute" in db_src, (
165 "database.py must register an 'after_cursor_execute' event listener "
166 "to log slow queries."
167 )
168 assert "SLOW QUERY" in db_src, (
169 "database.py must log 'SLOW QUERY' warnings for slow statements."
170 )
171
172
173 # ---------------------------------------------------------------------------
174 # Query runbook
175 # ---------------------------------------------------------------------------
176
177 def test_db_query_runbook_exists() -> None:
178 """docs/db-query-runbook.md must exist with the top-10 query analysis."""
179 runbook = _REPO_ROOT / "docs" / "db-query-runbook.md"
180 assert runbook.exists(), (
181 "docs/db-query-runbook.md is missing. "
182 "This file documents the EXPLAIN ANALYZE results for the top-10 queries."
183 )
184
185
186 def test_db_query_runbook_covers_top_queries() -> None:
187 """Runbook must document all 10 high-traffic query patterns."""
188 runbook = (_REPO_ROOT / "docs" / "db-query-runbook.md").read_text()
189 required = [
190 "musehub_repos",
191 "musehub_commits",
192 "musehub_issues",
193 "musehub_proposals",
194 "musehub_intel_results",
195 "musehub_objects",
196 "EXPLAIN",
197 "SLOW QUERY",
198 ]
199 missing = [kw for kw in required if kw not in runbook]
200 assert not missing, f"db-query-runbook.md is missing coverage for: {missing}"
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 122 days ago