gabriel / musehub public
test_database_section61.py python
207 lines 8.2 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 72 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) -> 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 def test_musehub_objects_has_repo_deleted_at_composite() -> None:
54 from musehub.db.musehub_models import MusehubObject
55 assert "ix_musehub_objects_repo_deleted_at" in _index_names(MusehubObject), (
56 "MusehubObject must have a composite index on (repo_id, deleted_at) "
57 "for the per-repo quota SUM query."
58 )
59
60
61 def test_musehub_issues_has_repo_state_composite() -> None:
62 from musehub.db.musehub_models import MusehubIssue
63 assert "ix_musehub_issues_repo_state" in _index_names(MusehubIssue), (
64 "MusehubIssue must have a composite index on (repo_id, state) "
65 "for the open/closed issue list."
66 )
67
68
69 def test_musehub_issues_has_repo_number_composite() -> None:
70 from musehub.db.musehub_models import MusehubIssue
71 assert "ix_musehub_issues_repo_number" in _index_names(MusehubIssue), (
72 "MusehubIssue must have a composite index on (repo_id, number) "
73 "for direct issue URL lookup."
74 )
75
76
77 def test_musehub_proposals_has_repo_state_composite() -> None:
78 from musehub.db.musehub_models import MusehubProposal
79 assert "ix_musehub_proposals_repo_state" in _index_names(MusehubProposal), (
80 "MusehubProposal must have a composite index on (repo_id, state) "
81 "for the open/closed/merged proposal list."
82 )
83
84
85 def test_musehub_proposals_has_repo_number_composite() -> None:
86 from musehub.db.musehub_models import MusehubProposal
87 assert "ix_musehub_proposals_repo_number" in _index_names(MusehubProposal), (
88 "MusehubProposal must have a composite index on (repo_id, proposal_number)."
89 )
90
91
92 def test_musehub_symbol_index_has_repo_built_at_composite() -> None:
93 from musehub.db.musehub_models import MusehubSymbolIndex
94 assert "ix_musehub_symbol_index_repo_built_at" in _index_names(MusehubSymbolIndex), (
95 "MusehubSymbolIndex must have a composite index on (repo_id, built_at) "
96 "for the latest-index lookup."
97 )
98
99
100 # ---------------------------------------------------------------------------
101 # Migration 0022 references all composite indexes
102 # ---------------------------------------------------------------------------
103
104 def test_migration_0022_creates_composite_indexes() -> None:
105 """Migration 0022 upgrade() must create all expected composite indexes."""
106 import inspect
107 from alembic.config import Config
108 from alembic.script import ScriptDirectory
109
110 cfg = Config(str(_REPO_ROOT / "alembic.ini"))
111 cfg.set_main_option("script_location", str(_REPO_ROOT / "alembic"))
112 sd = ScriptDirectory.from_config(cfg)
113
114 rev = next((r for r in sd.walk_revisions() if r.revision.startswith("0022")), None)
115 assert rev is not None, "Revision 0022 not found — composite index migration is missing."
116 assert rev.module is not None
117
118 up_src = inspect.getsource(getattr(rev.module, "upgrade"))
119 expected_indexes = [
120 "ix_musehub_commits_repo_branch",
121 "ix_musehub_commits_repo_timestamp",
122 "ix_musehub_issues_repo_state",
123 "ix_musehub_issues_repo_number",
124 "ix_musehub_proposals_repo_state",
125 "ix_musehub_objects_repo_deleted_at",
126 "ix_musehub_symbol_index_repo_built_at",
127 ]
128 missing = [idx for idx in expected_indexes if idx not in up_src]
129 assert not missing, f"Migration 0022 upgrade() is missing these indexes: {missing}"
130
131
132 # ---------------------------------------------------------------------------
133 # Connection pool configuration
134 # ---------------------------------------------------------------------------
135
136 def test_pool_size_configured() -> None:
137 """SQLAlchemy pool must be configured with pool_size ≥ 10."""
138 from musehub.config import settings
139 assert settings.db_pool_timeout > 0, "db_pool_timeout must be > 0"
140
141
142 def test_slow_query_threshold_configured() -> None:
143 """slow_query_threshold_ms must be set in config."""
144 from musehub.config import settings
145 assert settings.slow_query_threshold_ms >= 0, (
146 "slow_query_threshold_ms must be a non-negative integer. "
147 "Set to 0 to disable, or a positive value to enable slow query logging."
148 )
149
150
151 def test_slow_query_threshold_is_100ms_or_less_by_default() -> None:
152 """Default slow query threshold must be ≤ 100 ms (matches checklist requirement)."""
153 from musehub.config import settings
154 assert settings.slow_query_threshold_ms <= 100, (
155 f"Default slow_query_threshold_ms={settings.slow_query_threshold_ms} "
156 "exceeds the 100 ms checklist requirement."
157 )
158
159
160 # ---------------------------------------------------------------------------
161 # Slow query listener — registered on the engine
162 # ---------------------------------------------------------------------------
163
164 def test_slow_query_listener_registered_in_database_py() -> None:
165 """database.py must register before_cursor_execute and after_cursor_execute listeners."""
166 db_src = (_REPO_ROOT / "musehub" / "db" / "database.py").read_text()
167 assert "before_cursor_execute" in db_src, (
168 "database.py must register a 'before_cursor_execute' event listener "
169 "to time query execution."
170 )
171 assert "after_cursor_execute" in db_src, (
172 "database.py must register an 'after_cursor_execute' event listener "
173 "to log slow queries."
174 )
175 assert "SLOW QUERY" in db_src, (
176 "database.py must log 'SLOW QUERY' warnings for slow statements."
177 )
178
179
180 # ---------------------------------------------------------------------------
181 # Query runbook
182 # ---------------------------------------------------------------------------
183
184 def test_db_query_runbook_exists() -> None:
185 """docs/db-query-runbook.md must exist with the top-10 query analysis."""
186 runbook = _REPO_ROOT / "docs" / "db-query-runbook.md"
187 assert runbook.exists(), (
188 "docs/db-query-runbook.md is missing. "
189 "This file documents the EXPLAIN ANALYZE results for the top-10 queries."
190 )
191
192
193 def test_db_query_runbook_covers_top_queries() -> None:
194 """Runbook must document all 10 high-traffic query patterns."""
195 runbook = (_REPO_ROOT / "docs" / "db-query-runbook.md").read_text()
196 required = [
197 "musehub_repos",
198 "musehub_commits",
199 "musehub_issues",
200 "musehub_proposals",
201 "musehub_symbol_index",
202 "musehub_objects",
203 "EXPLAIN",
204 "SLOW QUERY",
205 ]
206 missing = [kw for kw in required if kw not in runbook]
207 assert not missing, f"db-query-runbook.md is missing coverage for: {missing}"
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 72 days ago