gabriel / musehub public
test_database.py python
202 lines 8.2 KB
Raw
sha256:9b711047e27df5ac91681c74aadfb0e31f69ffd4269932ea52f0c113764d8c0a docs(phase-03): rewrite Domain Protocol — AddressedMergePlu… Sonnet 4.6 minor ⚠ breaking 23 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_repo_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 # repo_id moved to MusehubCommitRef; repo-scoped queries filter via that table.
39 from musehub.db.musehub_repo_models import MusehubCommitRef
40 assert "ix_musehub_commit_refs_repo_id" in _index_names(MusehubCommitRef), (
41 "MusehubCommitRef must have an index on repo_id for HEAD lookup queries "
42 "(replaces the old ix_musehub_commits_repo_branch composite)."
43 )
44
45
46 def test_musehub_commits_has_repo_timestamp_composite() -> None:
47 # timestamp ordering is on MusehubCommit; repo scoping is via MusehubCommitRef.
48 from musehub.db.musehub_repo_models import MusehubCommit
49 assert "ix_musehub_commits_timestamp" in _index_names(MusehubCommit), (
50 "MusehubCommit must have an index on timestamp for the recent-commits feed "
51 "(replaces the old ix_musehub_commits_repo_timestamp composite)."
52 )
53
54
55
56 def test_musehub_issues_has_repo_state_composite() -> None:
57 from musehub.db.musehub_social_models import MusehubIssue
58 assert "ix_musehub_issues_repo_state" in _index_names(MusehubIssue), (
59 "MusehubIssue must have a composite index on (repo_id, state) "
60 "for the open/closed issue list."
61 )
62
63
64 def test_musehub_issues_has_repo_number_composite() -> None:
65 from musehub.db.musehub_social_models import MusehubIssue
66 assert "ix_musehub_issues_repo_number" in _index_names(MusehubIssue), (
67 "MusehubIssue must have a composite index on (repo_id, number) "
68 "for direct issue URL lookup."
69 )
70
71
72 def test_musehub_proposals_has_repo_state_composite() -> None:
73 from musehub.db.musehub_social_models import MusehubProposal
74 assert "ix_musehub_proposals_repo_state" in _index_names(MusehubProposal), (
75 "MusehubProposal must have a composite index on (repo_id, state) "
76 "for the open/closed/merged proposal list."
77 )
78
79
80 def test_musehub_proposals_has_repo_number_composite() -> None:
81 from musehub.db.musehub_social_models import MusehubProposal
82 assert "ix_musehub_proposals_repo_number" in _index_names(MusehubProposal), (
83 "MusehubProposal must have a composite index on (repo_id, proposal_number)."
84 )
85
86
87 def test_musehub_intel_results_has_repo_type_composite() -> None:
88 from musehub.db.musehub_intel_models import MusehubIntelResult
89 assert "ix_musehub_intel_results_repo_type" in _index_names(MusehubIntelResult), (
90 "MusehubIntelResult must have a composite index on (repo_id, intel_type) "
91 "for O(1) result lookup."
92 )
93
94
95 # ---------------------------------------------------------------------------
96 # Migration 0022 references all composite indexes
97 # ---------------------------------------------------------------------------
98
99 def test_migration_0022_creates_composite_indexes() -> None:
100 """Consolidated migration 0001 upgrade() must create all expected composite indexes."""
101 import inspect
102 from alembic.config import Config
103 from alembic.script import ScriptDirectory
104
105 cfg = Config(str(_REPO_ROOT / "alembic.ini"))
106 cfg.set_main_option("script_location", str(_REPO_ROOT / "alembic"))
107 sd = ScriptDirectory.from_config(cfg)
108
109 rev = next((r for r in sd.walk_revisions() if r.revision == "0001"), None)
110 assert rev is not None, "Revision 0001 not found — consolidated schema migration is missing."
111 assert rev.module is not None
112
113 up_src = inspect.getsource(getattr(rev.module, "upgrade"))
114 expected_indexes = [
115 "ix_musehub_commits_repo_branch",
116 "ix_musehub_commits_repo_timestamp",
117 "ix_musehub_issues_repo_state",
118 "ix_musehub_issues_repo_number",
119 "ix_musehub_proposals_repo_state",
120 "ix_musehub_proposals_repo_number",
121 "ix_musehub_intel_results_repo_type",
122 ]
123 missing = [idx for idx in expected_indexes if idx not in up_src]
124 assert not missing, f"Migration 0001 upgrade() is missing these indexes: {missing}"
125
126
127 # ---------------------------------------------------------------------------
128 # Connection pool configuration
129 # ---------------------------------------------------------------------------
130
131 def test_pool_size_configured() -> None:
132 """SQLAlchemy pool must be configured with pool_size ≥ 10."""
133 from musehub.config import settings
134 assert settings.db_pool_timeout > 0, "db_pool_timeout must be > 0"
135
136
137 def test_slow_query_threshold_configured() -> None:
138 """slow_query_threshold_ms must be set in config."""
139 from musehub.config import settings
140 assert settings.slow_query_threshold_ms >= 0, (
141 "slow_query_threshold_ms must be a non-negative integer. "
142 "Set to 0 to disable, or a positive value to enable slow query logging."
143 )
144
145
146 def test_slow_query_threshold_is_100ms_or_less_by_default() -> None:
147 """Default slow query threshold must be ≤ 100 ms (matches checklist requirement)."""
148 from musehub.config import settings
149 assert settings.slow_query_threshold_ms <= 100, (
150 f"Default slow_query_threshold_ms={settings.slow_query_threshold_ms} "
151 "exceeds the 100 ms checklist requirement."
152 )
153
154
155 # ---------------------------------------------------------------------------
156 # Slow query listener — registered on the engine
157 # ---------------------------------------------------------------------------
158
159 def test_slow_query_listener_registered_in_database_py() -> None:
160 """database.py must register before_cursor_execute and after_cursor_execute listeners."""
161 db_src = (_REPO_ROOT / "musehub" / "db" / "database.py").read_text()
162 assert "before_cursor_execute" in db_src, (
163 "database.py must register a 'before_cursor_execute' event listener "
164 "to time query execution."
165 )
166 assert "after_cursor_execute" in db_src, (
167 "database.py must register an 'after_cursor_execute' event listener "
168 "to log slow queries."
169 )
170 assert "SLOW QUERY" in db_src, (
171 "database.py must log 'SLOW QUERY' warnings for slow statements."
172 )
173
174
175 # ---------------------------------------------------------------------------
176 # Query runbook
177 # ---------------------------------------------------------------------------
178
179 def test_db_query_runbook_exists() -> None:
180 """docs/db-query-runbook.md must exist with the top-10 query analysis."""
181 runbook = _REPO_ROOT / "docs" / "db-query-runbook.md"
182 assert runbook.exists(), (
183 "docs/db-query-runbook.md is missing. "
184 "This file documents the EXPLAIN ANALYZE results for the top-10 queries."
185 )
186
187
188 def test_db_query_runbook_covers_top_queries() -> None:
189 """Runbook must document all 10 high-traffic query patterns."""
190 runbook = (_REPO_ROOT / "docs" / "db-query-runbook.md").read_text()
191 required = [
192 "musehub_repos",
193 "musehub_commits",
194 "musehub_issues",
195 "musehub_proposals",
196 "musehub_intel_results",
197 "musehub_objects",
198 "EXPLAIN",
199 "SLOW QUERY",
200 ]
201 missing = [kw for kw in required if kw not in runbook]
202 assert not missing, f"db-query-runbook.md is missing coverage for: {missing}"
File History 1 commit
sha256:9b711047e27df5ac91681c74aadfb0e31f69ffd4269932ea52f0c113764d8c0a docs(phase-03): rewrite Domain Protocol — AddressedMergePlu… Sonnet 4.6 minor 23 days ago