Skip to content

Commit 021901c

Browse files
committed
feat(database): introduce comprehensive database toolkit for management and analysis
- Added a new database toolkit script to enhance developer experience with various database management functionalities. - Implemented commands for health checks, performance analysis, query explanation, statistics reset, and migrations. - Integrated rich console output for better visualization of database metrics and statuses. - Established an asynchronous command structure using Click for improved command-line interface interactions.
1 parent 6ba20a6 commit 021901c

File tree

2 files changed

+605
-0
lines changed

2 files changed

+605
-0
lines changed

scripts/database_toolkit.py

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
#!/usr/bin/env python3
2+
"""
3+
🛠️ Database Toolkit - Developer Experience Enhancement
4+
5+
Professional database management CLI based on py-pglite patterns.
6+
Provides debugging, analysis, and maintenance capabilities.
7+
8+
Usage:
9+
python scripts/database_toolkit.py --help
10+
python scripts/database_toolkit.py analyze-performance
11+
python scripts/database_toolkit.py explain-query "SELECT * FROM guild WHERE tags @> ARRAY['gaming']"
12+
python scripts/database_toolkit.py health-check
13+
python scripts/database_toolkit.py reset-stats
14+
python scripts/database_toolkit.py migrate
15+
"""
16+
17+
import asyncio
18+
import json
19+
20+
# Add project root to path for imports
21+
import sys
22+
from pathlib import Path
23+
24+
import click
25+
from loguru import logger
26+
from rich.console import Console
27+
from rich.syntax import Syntax
28+
from rich.table import Table
29+
30+
sys.path.insert(0, str(Path(__file__).parent.parent))
31+
32+
from src.tux.database.service import DatabaseService
33+
34+
console = Console()
35+
36+
37+
async def get_db_service() -> DatabaseService:
38+
"""Get configured database service."""
39+
service = DatabaseService(echo=False)
40+
await service.connect()
41+
return service
42+
43+
44+
@click.group()
45+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging")
46+
def cli(verbose: bool):
47+
"""🛠️ Professional Database Toolkit for TuxBot"""
48+
if verbose:
49+
logger.add(sys.stderr, level="DEBUG")
50+
console.print("🛠️ [bold blue]TuxBot Database Toolkit[/bold blue]", style="bold")
51+
52+
53+
@cli.command()
54+
async def health_check():
55+
"""🏥 Perform comprehensive database health check."""
56+
console.print("🔍 Running health check...", style="yellow")
57+
58+
try:
59+
service = await get_db_service()
60+
health = await service.health_check()
61+
62+
if health["status"] == "healthy":
63+
console.print("✅ Database is healthy!", style="green")
64+
65+
table = Table(title="Database Health Status")
66+
table.add_column("Metric", style="cyan")
67+
table.add_column("Value", style="magenta")
68+
69+
for key, value in health.items():
70+
if key != "status":
71+
table.add_row(key.replace("_", " ").title(), str(value))
72+
73+
console.print(table)
74+
else:
75+
console.print(f"❌ Database unhealthy: {health.get('error', 'Unknown error')}", style="red")
76+
77+
except Exception as e:
78+
console.print(f"❌ Health check failed: {e}", style="red")
79+
80+
81+
@cli.command()
82+
async def analyze_performance():
83+
"""📊 Analyze database performance metrics."""
84+
console.print("📊 Analyzing database performance...", style="yellow")
85+
86+
try:
87+
service = await get_db_service()
88+
metrics = await service.get_database_metrics()
89+
90+
# Pool metrics
91+
console.print("\n🔄 [bold]Connection Pool Status[/bold]")
92+
pool_table = Table()
93+
pool_table.add_column("Metric", style="cyan")
94+
pool_table.add_column("Value", style="green")
95+
96+
for key, value in metrics["pool"].items():
97+
pool_table.add_row(key.replace("_", " ").title(), str(value))
98+
console.print(pool_table)
99+
100+
# Table statistics
101+
if metrics["tables"]:
102+
console.print("\n📋 [bold]Table Statistics[/bold]")
103+
table_stats = Table()
104+
table_stats.add_column("Table", style="cyan")
105+
table_stats.add_column("Live Tuples", style="green")
106+
table_stats.add_column("Inserts", style="blue")
107+
table_stats.add_column("Updates", style="yellow")
108+
table_stats.add_column("Deletes", style="red")
109+
table_stats.add_column("Seq Scans", style="magenta")
110+
table_stats.add_column("Index Scans", style="bright_green")
111+
112+
for table in metrics["tables"]:
113+
table_stats.add_row(
114+
table["tablename"],
115+
str(table["live_tuples"]),
116+
str(table["inserts"]),
117+
str(table["updates"]),
118+
str(table["deletes"]),
119+
str(table["seq_scan"]),
120+
str(table["idx_scan"]),
121+
)
122+
console.print(table_stats)
123+
124+
# Database-wide stats
125+
if metrics["database"]:
126+
console.print("\n🗄️ [bold]Database Statistics[/bold]")
127+
db_table = Table()
128+
db_table.add_column("Metric", style="cyan")
129+
db_table.add_column("Value", style="green")
130+
131+
for key, value in metrics["database"].items():
132+
if value is not None:
133+
db_table.add_row(key.replace("_", " ").title(), str(value))
134+
console.print(db_table)
135+
136+
except Exception as e:
137+
console.print(f"❌ Performance analysis failed: {e}", style="red")
138+
139+
140+
@cli.command()
141+
@click.argument("query", type=str)
142+
async def explain_query(query: str):
143+
"""🔍 Analyze query execution plan."""
144+
console.print(f"🔍 Analyzing query: {query}", style="yellow")
145+
146+
try:
147+
service = await get_db_service()
148+
analysis = await service.analyze_query_performance(query)
149+
150+
console.print("\n📋 [bold]Query Analysis[/bold]")
151+
console.print(Syntax(query, "sql", theme="monokai", line_numbers=True))
152+
153+
plan = analysis["plan"]
154+
if plan:
155+
console.print("\n⚡ [bold]Execution Plan[/bold]")
156+
execution_time = plan.get("Execution Time", "N/A")
157+
planning_time = plan.get("Planning Time", "N/A")
158+
159+
console.print(f"Planning Time: {planning_time} ms", style="blue")
160+
console.print(f"Execution Time: {execution_time} ms", style="green")
161+
162+
# Pretty print the plan as JSON
163+
plan_json = json.dumps(plan, indent=2)
164+
console.print(Syntax(plan_json, "json", theme="monokai", line_numbers=True))
165+
else:
166+
console.print("❌ No execution plan available", style="red")
167+
168+
except Exception as e:
169+
console.print(f"❌ Query analysis failed: {e}", style="red")
170+
171+
172+
@cli.command()
173+
async def reset_stats():
174+
"""🔄 Reset database statistics counters."""
175+
console.print("🔄 Resetting database statistics...", style="yellow")
176+
177+
try:
178+
service = await get_db_service()
179+
success = await service.reset_database_stats()
180+
181+
if success:
182+
console.print("✅ Database statistics reset successfully!", style="green")
183+
else:
184+
console.print("❌ Failed to reset statistics", style="red")
185+
186+
except Exception as e:
187+
console.print(f"❌ Statistics reset failed: {e}", style="red")
188+
189+
190+
@cli.command()
191+
async def migrate():
192+
"""🚀 Run database migrations."""
193+
console.print("🚀 Running database migrations...", style="yellow")
194+
195+
try:
196+
service = await get_db_service()
197+
success = await service.run_migrations()
198+
199+
if success:
200+
console.print("✅ Migrations completed successfully!", style="green")
201+
else:
202+
console.print("❌ Migrations failed", style="red")
203+
204+
except Exception as e:
205+
console.print(f"❌ Migration failed: {e}", style="red")
206+
207+
208+
@cli.command()
209+
@click.option("--table", "-t", help="Specific table to analyze")
210+
async def table_stats(table: str | None = None):
211+
"""📊 Get detailed table statistics."""
212+
console.print(f"📊 Analyzing table statistics{'for ' + table if table else ''}...", style="yellow")
213+
214+
try:
215+
service = await get_db_service()
216+
217+
# Get statistics for specific models
218+
controllers = [
219+
("guild", service.guild),
220+
("guild_config", service.guild_config),
221+
("case", service.case),
222+
]
223+
224+
for name, controller in controllers:
225+
if table and name != table:
226+
continue
227+
228+
console.print(f"\n📋 [bold]{name.title()} Table Statistics[/bold]")
229+
stats = await controller.get_table_statistics()
230+
231+
if stats:
232+
stats_table = Table()
233+
stats_table.add_column("Metric", style="cyan")
234+
stats_table.add_column("Value", style="green")
235+
236+
for key, value in stats.items():
237+
if value is not None:
238+
stats_table.add_row(key.replace("_", " ").title(), str(value))
239+
console.print(stats_table)
240+
else:
241+
console.print(f"❌ No statistics available for {name}", style="red")
242+
243+
except Exception as e:
244+
console.print(f"❌ Table statistics failed: {e}", style="red")
245+
246+
247+
@cli.command()
248+
async def demo_advanced_queries():
249+
"""🎮 Demonstrate PostgreSQL advanced features."""
250+
console.print("🎮 Demonstrating advanced PostgreSQL queries...", style="yellow")
251+
252+
try:
253+
service = await get_db_service()
254+
guild_controller = service.guild
255+
256+
console.print("\n1️⃣ [bold]JSON Query Demo[/bold]")
257+
console.print("Searching guilds with specific metadata...")
258+
259+
# This would work with the enhanced Guild model
260+
try:
261+
guilds = await guild_controller.find_with_json_query(
262+
"metadata",
263+
"$.settings.auto_mod",
264+
True,
265+
)
266+
console.print(f"Found {len(guilds)} guilds with auto_mod enabled", style="green")
267+
except Exception as e:
268+
console.print(f"JSON query demo not available: {e}", style="yellow")
269+
270+
console.print("\n2️⃣ [bold]Array Query Demo[/bold]")
271+
console.print("Searching guilds with gaming tag...")
272+
273+
try:
274+
guilds = await guild_controller.find_with_array_contains("tags", "gaming")
275+
console.print(f"Found {len(guilds)} gaming guilds", style="green")
276+
except Exception as e:
277+
console.print(f"Array query demo not available: {e}", style="yellow")
278+
279+
console.print("\n3️⃣ [bold]Performance Analysis Demo[/bold]")
280+
console.print("Analyzing query performance...")
281+
282+
try:
283+
performance = await guild_controller.explain_query_performance()
284+
console.print("Query performance analysis completed", style="green")
285+
console.print(f"Model: {performance['model']}")
286+
except Exception as e:
287+
console.print(f"Performance demo not available: {e}", style="yellow")
288+
289+
except Exception as e:
290+
console.print(f"❌ Demo failed: {e}", style="red")
291+
292+
293+
def main():
294+
"""Main entry point with async support."""
295+
296+
# Patch click commands to support async
297+
for command in cli.commands.values():
298+
if asyncio.iscoroutinefunction(command.callback):
299+
original_callback = command.callback
300+
301+
def create_wrapper(callback):
302+
def wrapper(*args, **kwargs):
303+
return asyncio.run(callback(*args, **kwargs))
304+
305+
return wrapper
306+
307+
command.callback = create_wrapper(original_callback)
308+
309+
cli()
310+
311+
312+
if __name__ == "__main__":
313+
main()

0 commit comments

Comments
 (0)