db-storage-manager 1.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- db_storage_manager/__init__.py +7 -0
- db_storage_manager/analysis/__init__.py +27 -0
- db_storage_manager/analysis/capacity.py +181 -0
- db_storage_manager/analysis/optimization.py +155 -0
- db_storage_manager/analysis/performance.py +127 -0
- db_storage_manager/analysis/prediction.py +169 -0
- db_storage_manager/analysis/profiling.py +101 -0
- db_storage_manager/analysis/query_suggestions.py +515 -0
- db_storage_manager/api/__init__.py +19 -0
- db_storage_manager/api/__main__.py +5 -0
- db_storage_manager/api/sanitize.py +55 -0
- db_storage_manager/api/server.py +529 -0
- db_storage_manager/api/service.py +247 -0
- db_storage_manager/api/static/app.js +135 -0
- db_storage_manager/api/static/index.html +47 -0
- db_storage_manager/api/static/style.css +145 -0
- db_storage_manager/audit.py +91 -0
- db_storage_manager/backups/__init__.py +46 -0
- db_storage_manager/backups/base.py +70 -0
- db_storage_manager/backups/googledrive.py +276 -0
- db_storage_manager/backups/local.py +354 -0
- db_storage_manager/backups/manager.py +958 -0
- db_storage_manager/backups/pipeline.py +230 -0
- db_storage_manager/backups/protondrive.py +1076 -0
- db_storage_manager/backups/retention.py +175 -0
- db_storage_manager/backups/s3.py +392 -0
- db_storage_manager/backups/scheduler.py +406 -0
- db_storage_manager/backups/scratch.py +107 -0
- db_storage_manager/bundled_plugins/__init__.py +34 -0
- db_storage_manager/bundled_plugins/__pycache__/__init__.cpython-312.pyc +0 -0
- db_storage_manager/bundled_plugins/connection-email-alerts/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/bundled_plugins/connection-email-alerts/plugin.json +11 -0
- db_storage_manager/bundled_plugins/connection-email-alerts/plugin.py +38 -0
- db_storage_manager/bundled_plugins/multi-connection-monitor/plugin.json +11 -0
- db_storage_manager/bundled_plugins/multi-connection-monitor/plugin.py +37 -0
- db_storage_manager/cloud/__init__.py +31 -0
- db_storage_manager/cloud/aws.py +245 -0
- db_storage_manager/cloud/azure.py +210 -0
- db_storage_manager/cloud/connect_auth.py +114 -0
- db_storage_manager/cloud/credentials.py +226 -0
- db_storage_manager/cloud/engines.py +86 -0
- db_storage_manager/cloud/errors.py +15 -0
- db_storage_manager/cloud/gcp.py +248 -0
- db_storage_manager/cloud/manager.py +175 -0
- db_storage_manager/config.py +291 -0
- db_storage_manager/dashboard_prefs.py +140 -0
- db_storage_manager/data/__init__.py +15 -0
- db_storage_manager/data/comparison.py +87 -0
- db_storage_manager/data/migration.py +48 -0
- db_storage_manager/data/quality.py +114 -0
- db_storage_manager/data/sync.py +62 -0
- db_storage_manager/db/__init__.py +35 -0
- db_storage_manager/db/base.py +143 -0
- db_storage_manager/db/clickhouse.py +194 -0
- db_storage_manager/db/factory.py +75 -0
- db_storage_manager/db/influxdb.py +247 -0
- db_storage_manager/db/mongo.py +313 -0
- db_storage_manager/db/mysql.py +424 -0
- db_storage_manager/db/optional_deps.py +53 -0
- db_storage_manager/db/oracle.py +280 -0
- db_storage_manager/db/pool.py +338 -0
- db_storage_manager/db/postgres.py +518 -0
- db_storage_manager/db/redis.py +284 -0
- db_storage_manager/db/session.py +64 -0
- db_storage_manager/db/sqlite.py +272 -0
- db_storage_manager/db/sqlserver.py +319 -0
- db_storage_manager/desktop_identity.py +64 -0
- db_storage_manager/experimental.py +67 -0
- db_storage_manager/export/__init__.py +17 -0
- db_storage_manager/export/companion.py +198 -0
- db_storage_manager/gui/__init__.py +3 -0
- db_storage_manager/gui/about.py +174 -0
- db_storage_manager/gui/advanced_dashboard.py +174 -0
- db_storage_manager/gui/backups.py +1552 -0
- db_storage_manager/gui/changelog.py +63 -0
- db_storage_manager/gui/charts.py +157 -0
- db_storage_manager/gui/connections.py +1124 -0
- db_storage_manager/gui/dashboard.py +598 -0
- db_storage_manager/gui/enhanced_query.py +299 -0
- db_storage_manager/gui/icons.py +326 -0
- db_storage_manager/gui/jobs.py +134 -0
- db_storage_manager/gui/login.py +125 -0
- db_storage_manager/gui/main_window.py +578 -0
- db_storage_manager/gui/monitoring.py +1135 -0
- db_storage_manager/gui/plugins.py +764 -0
- db_storage_manager/gui/proton_drive_config.py +181 -0
- db_storage_manager/gui/query.py +365 -0
- db_storage_manager/gui/restore_dialog.py +172 -0
- db_storage_manager/gui/s3_config.py +146 -0
- db_storage_manager/gui/settings.py +1445 -0
- db_storage_manager/gui/sidebar.py +125 -0
- db_storage_manager/gui/tray.py +225 -0
- db_storage_manager/gui/updates.py +269 -0
- db_storage_manager/gui/users.py +606 -0
- db_storage_manager/gui/utils.py +733 -0
- db_storage_manager/i18n/__init__.py +8 -0
- db_storage_manager/i18n/languages.py +22 -0
- db_storage_manager/i18n/manager.py +133 -0
- db_storage_manager/i18n/translations/de.json +723 -0
- db_storage_manager/i18n/translations/en.json +723 -0
- db_storage_manager/i18n/translations/es.json +723 -0
- db_storage_manager/i18n/translations/et.json +723 -0
- db_storage_manager/i18n/translations/fr.json +723 -0
- db_storage_manager/i18n/translations/id.json +723 -0
- db_storage_manager/i18n/translations/ja.json +723 -0
- db_storage_manager/i18n/translations/ko.json +723 -0
- db_storage_manager/i18n/translations/pt.json +723 -0
- db_storage_manager/i18n/translations/ru.json +723 -0
- db_storage_manager/i18n/translations/zh.json +723 -0
- db_storage_manager/importers/__init__.py +29 -0
- db_storage_manager/importers/env_file.py +328 -0
- db_storage_manager/importers/oauth_platforms.py +138 -0
- db_storage_manager/importers/persist.py +175 -0
- db_storage_manager/importers/redact.py +100 -0
- db_storage_manager/importers/ssh_keys.py +352 -0
- db_storage_manager/main.py +66 -0
- db_storage_manager/monitoring/__init__.py +27 -0
- db_storage_manager/monitoring/alerts.py +302 -0
- db_storage_manager/monitoring/anomaly.py +251 -0
- db_storage_manager/monitoring/connection_email.py +255 -0
- db_storage_manager/monitoring/health.py +211 -0
- db_storage_manager/monitoring/metrics.py +247 -0
- db_storage_manager/monitoring/monitor.py +248 -0
- db_storage_manager/monitoring/multi_watch.py +215 -0
- db_storage_manager/monitoring/notifications.py +941 -0
- db_storage_manager/native/__init__.py +340 -0
- db_storage_manager/plugins/__init__.py +34 -0
- db_storage_manager/plugins/base.py +135 -0
- db_storage_manager/plugins/data_comparison.py +45 -0
- db_storage_manager/plugins/licensing.py +616 -0
- db_storage_manager/plugins/loader.py +132 -0
- db_storage_manager/plugins/manifest.py +261 -0
- db_storage_manager/plugins/marketplace.py +566 -0
- db_storage_manager/plugins/migration.py +51 -0
- db_storage_manager/plugins/pricing.py +219 -0
- db_storage_manager/plugins/registry.py +92 -0
- db_storage_manager/plugins/sdk.py +82 -0
- db_storage_manager/premium_plugins/__init__.py +46 -0
- db_storage_manager/premium_plugins/__pycache__/__init__.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/encrypted-export/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/encrypted-export/plugin.json +21 -0
- db_storage_manager/premium_plugins/encrypted-export/plugin.py +349 -0
- db_storage_manager/premium_plugins/extension-inventory/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/extension-inventory/__pycache__/plugin.cpython-314.pyc +0 -0
- db_storage_manager/premium_plugins/extension-inventory/plugin.json +21 -0
- db_storage_manager/premium_plugins/extension-inventory/plugin.py +191 -0
- db_storage_manager/premium_plugins/maintenance-window-watch/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/maintenance-window-watch/__pycache__/plugin.cpython-314.pyc +0 -0
- db_storage_manager/premium_plugins/maintenance-window-watch/plugin.json +21 -0
- db_storage_manager/premium_plugins/maintenance-window-watch/plugin.py +380 -0
- db_storage_manager/premium_plugins/multi-cloud-discovery/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/multi-cloud-discovery/__pycache__/plugin.cpython-314.pyc +0 -0
- db_storage_manager/premium_plugins/multi-cloud-discovery/plugin.json +21 -0
- db_storage_manager/premium_plugins/multi-cloud-discovery/plugin.py +401 -0
- db_storage_manager/premium_plugins/pitr-verify/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/pitr-verify/plugin.json +21 -0
- db_storage_manager/premium_plugins/pitr-verify/plugin.py +1329 -0
- db_storage_manager/premium_plugins/pooler-health/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/pooler-health/plugin.json +21 -0
- db_storage_manager/premium_plugins/pooler-health/plugin.py +243 -0
- db_storage_manager/premium_plugins/rls-audit/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/rls-audit/plugin.json +21 -0
- db_storage_manager/premium_plugins/rls-audit/plugin.py +244 -0
- db_storage_manager/premium_plugins/schema-drift/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/schema-drift/plugin.json +21 -0
- db_storage_manager/premium_plugins/schema-drift/plugin.py +315 -0
- db_storage_manager/premium_plugins/trusted-sources-check/__pycache__/plugin.cpython-312.pyc +0 -0
- db_storage_manager/premium_plugins/trusted-sources-check/__pycache__/plugin.cpython-314.pyc +0 -0
- db_storage_manager/premium_plugins/trusted-sources-check/plugin.json +21 -0
- db_storage_manager/premium_plugins/trusted-sources-check/plugin.py +380 -0
- db_storage_manager/resources/__init__.py +60 -0
- db_storage_manager/resources/__pycache__/__init__.cpython-312.pyc +0 -0
- db_storage_manager/resources/icons/db-storage-manager-128.png +0 -0
- db_storage_manager/resources/icons/db-storage-manager-16.png +0 -0
- db_storage_manager/resources/icons/db-storage-manager-256.png +0 -0
- db_storage_manager/resources/icons/db-storage-manager-32.png +0 -0
- db_storage_manager/resources/icons/db-storage-manager-48.png +0 -0
- db_storage_manager/resources/icons/db-storage-manager-512.png +0 -0
- db_storage_manager/resources/icons/db-storage-manager-64.png +0 -0
- db_storage_manager/resources/icons/db-storage-manager.ico +0 -0
- db_storage_manager/resources/icons/db-storage-manager.png +0 -0
- db_storage_manager/resources/icons/db-storage-manager.svg +39 -0
- db_storage_manager/security/__init__.py +21 -0
- db_storage_manager/security/auth.py +800 -0
- db_storage_manager/security/bootstrap.py +114 -0
- db_storage_manager/security/mfa_qr.py +70 -0
- db_storage_manager/security/pairing.py +200 -0
- db_storage_manager/security/store.py +316 -0
- db_storage_manager/ssh/__init__.py +25 -0
- db_storage_manager/ssh/tunnel.py +481 -0
- db_storage_manager/themes/__init__.py +8 -0
- db_storage_manager/themes/manager.py +113 -0
- db_storage_manager/themes/themes.py +130 -0
- db_storage_manager/updates.py +205 -0
- db_storage_manager-1.0.8.dist-info/METADATA +261 -0
- db_storage_manager-1.0.8.dist-info/RECORD +200 -0
- db_storage_manager-1.0.8.dist-info/WHEEL +5 -0
- db_storage_manager-1.0.8.dist-info/entry_points.txt +6 -0
- db_storage_manager-1.0.8.dist-info/licenses/LICENSE +674 -0
- db_storage_manager-1.0.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Advanced database analysis features
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .performance import QueryPerformanceAnalyzer
|
|
6
|
+
from .optimization import IndexOptimizer
|
|
7
|
+
from .prediction import StorageGrowthPredictor
|
|
8
|
+
from .capacity import CapacityPlanner
|
|
9
|
+
from .query_suggestions import (
|
|
10
|
+
QuerySuggestion,
|
|
11
|
+
analyze_explain_plan,
|
|
12
|
+
analyze_sql_heuristics,
|
|
13
|
+
fetch_explain_plan,
|
|
14
|
+
suggest_for_query,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"QueryPerformanceAnalyzer",
|
|
19
|
+
"IndexOptimizer",
|
|
20
|
+
"StorageGrowthPredictor",
|
|
21
|
+
"CapacityPlanner",
|
|
22
|
+
"QuerySuggestion",
|
|
23
|
+
"analyze_sql_heuristics",
|
|
24
|
+
"analyze_explain_plan",
|
|
25
|
+
"fetch_explain_plan",
|
|
26
|
+
"suggest_for_query",
|
|
27
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Capacity planning tools
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Any, Optional
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from ..db.base import DatabaseConnection, StorageAnalysis
|
|
8
|
+
from .prediction import StorageGrowthPredictor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CapacityPlanner:
|
|
12
|
+
"""Capacity planning and forecasting"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, connection: DatabaseConnection):
|
|
15
|
+
self.connection = connection
|
|
16
|
+
self.predictor = StorageGrowthPredictor(connection)
|
|
17
|
+
|
|
18
|
+
def add_analysis(self, analysis: StorageAnalysis) -> None:
|
|
19
|
+
"""Add analysis for capacity planning"""
|
|
20
|
+
self.predictor.add_analysis(analysis)
|
|
21
|
+
|
|
22
|
+
def calculate_capacity_utilization(
|
|
23
|
+
self, analysis: StorageAnalysis, total_capacity: int
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
"""Calculate current capacity utilization"""
|
|
26
|
+
current_size = analysis.get("totalSize", 0)
|
|
27
|
+
utilization_percent = (
|
|
28
|
+
(current_size / total_capacity * 100) if total_capacity > 0 else 0
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
status = (
|
|
32
|
+
"critical"
|
|
33
|
+
if utilization_percent > 90
|
|
34
|
+
else "warning" if utilization_percent > 75 else "healthy"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"currentSize": current_size,
|
|
39
|
+
"totalCapacity": total_capacity,
|
|
40
|
+
"availableSpace": total_capacity - current_size,
|
|
41
|
+
"utilizationPercent": utilization_percent,
|
|
42
|
+
"status": status,
|
|
43
|
+
"timestamp": datetime.now().isoformat(),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def estimate_time_to_capacity(
|
|
47
|
+
self, total_capacity: int, threshold_percent: float = 90.0
|
|
48
|
+
) -> Dict[str, Any]:
|
|
49
|
+
"""Estimate time until capacity threshold is reached"""
|
|
50
|
+
if len(self.predictor.history) < 2:
|
|
51
|
+
return {
|
|
52
|
+
"estimatedDays": None,
|
|
53
|
+
"estimatedDate": None,
|
|
54
|
+
"confidence": "low",
|
|
55
|
+
"message": "Insufficient historical data",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
current_size = self.predictor.history[-1]["totalSize"]
|
|
59
|
+
threshold_size = total_capacity * (threshold_percent / 100)
|
|
60
|
+
|
|
61
|
+
if current_size >= threshold_size:
|
|
62
|
+
return {
|
|
63
|
+
"estimatedDays": 0,
|
|
64
|
+
"estimatedDate": datetime.now().isoformat(),
|
|
65
|
+
"status": "threshold_reached",
|
|
66
|
+
"message": f"Already at {threshold_percent}% capacity",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Predict growth
|
|
70
|
+
prediction = self.predictor.predict_growth(days=365)
|
|
71
|
+
growth_rate = prediction.get("growthRate", 0) / 100
|
|
72
|
+
|
|
73
|
+
if growth_rate <= 0:
|
|
74
|
+
return {
|
|
75
|
+
"estimatedDays": None,
|
|
76
|
+
"estimatedDate": None,
|
|
77
|
+
"status": "no_growth",
|
|
78
|
+
"message": "No growth detected, capacity threshold not expected",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Calculate days to threshold
|
|
82
|
+
remaining_space = threshold_size - current_size
|
|
83
|
+
daily_growth = current_size * growth_rate
|
|
84
|
+
estimated_days = (
|
|
85
|
+
int(remaining_space / daily_growth) if daily_growth > 0 else None
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if estimated_days:
|
|
89
|
+
estimated_date = datetime.now() + timedelta(days=estimated_days)
|
|
90
|
+
return {
|
|
91
|
+
"estimatedDays": estimated_days,
|
|
92
|
+
"estimatedDate": estimated_date.isoformat(),
|
|
93
|
+
"thresholdPercent": threshold_percent,
|
|
94
|
+
"thresholdSize": int(threshold_size),
|
|
95
|
+
"currentSize": current_size,
|
|
96
|
+
"growthRate": growth_rate * 100,
|
|
97
|
+
"confidence": prediction.get("confidence", "low"),
|
|
98
|
+
}
|
|
99
|
+
else:
|
|
100
|
+
return {
|
|
101
|
+
"estimatedDays": None,
|
|
102
|
+
"estimatedDate": None,
|
|
103
|
+
"status": "unable_to_calculate",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def get_capacity_recommendations(
|
|
107
|
+
self, analysis: StorageAnalysis, total_capacity: int
|
|
108
|
+
) -> List[Dict[str, Any]]:
|
|
109
|
+
"""Get capacity planning recommendations"""
|
|
110
|
+
recommendations = []
|
|
111
|
+
utilization = self.calculate_capacity_utilization(analysis, total_capacity)
|
|
112
|
+
|
|
113
|
+
if utilization["utilizationPercent"] > 90:
|
|
114
|
+
recommendations.append(
|
|
115
|
+
{
|
|
116
|
+
"type": "urgent",
|
|
117
|
+
"severity": "critical",
|
|
118
|
+
"message": "Database is at critical capacity (>90%)",
|
|
119
|
+
"recommendation": "Immediate action required: Consider archiving old data or increasing capacity",
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
elif utilization["utilizationPercent"] > 75:
|
|
123
|
+
recommendations.append(
|
|
124
|
+
{
|
|
125
|
+
"type": "warning",
|
|
126
|
+
"severity": "high",
|
|
127
|
+
"message": "Database is approaching capacity (>75%)",
|
|
128
|
+
"recommendation": "Plan for capacity increase or data archiving in the near future",
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Check growth trends
|
|
133
|
+
trends = self.predictor.get_growth_trends()
|
|
134
|
+
if trends.get("trend") == "rapid_growth":
|
|
135
|
+
recommendations.append(
|
|
136
|
+
{
|
|
137
|
+
"type": "growth_alert",
|
|
138
|
+
"severity": "medium",
|
|
139
|
+
"message": "Rapid growth detected",
|
|
140
|
+
"recommendation": "Monitor growth closely and plan capacity expansion",
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Time to capacity estimate
|
|
145
|
+
time_estimate = self.estimate_time_to_capacity(total_capacity)
|
|
146
|
+
if time_estimate.get("estimatedDays") and time_estimate["estimatedDays"] < 90:
|
|
147
|
+
recommendations.append(
|
|
148
|
+
{
|
|
149
|
+
"type": "capacity_forecast",
|
|
150
|
+
"severity": "high",
|
|
151
|
+
"message": f"Capacity threshold may be reached in {time_estimate['estimatedDays']} days",
|
|
152
|
+
"recommendation": "Plan capacity increase or data management strategy",
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return recommendations
|
|
157
|
+
|
|
158
|
+
def generate_capacity_report(
|
|
159
|
+
self, analysis: StorageAnalysis, total_capacity: int
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""Generate comprehensive capacity planning report"""
|
|
162
|
+
utilization = self.calculate_capacity_utilization(analysis, total_capacity)
|
|
163
|
+
trends = self.predictor.get_growth_trends()
|
|
164
|
+
time_estimate = self.estimate_time_to_capacity(total_capacity)
|
|
165
|
+
recommendations = self.get_capacity_recommendations(analysis, total_capacity)
|
|
166
|
+
|
|
167
|
+
# Predictions for different timeframes
|
|
168
|
+
predictions = {
|
|
169
|
+
"30days": self.predictor.predict_growth(30),
|
|
170
|
+
"90days": self.predictor.predict_growth(90),
|
|
171
|
+
"365days": self.predictor.predict_growth(365),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"currentState": utilization,
|
|
176
|
+
"growthTrends": trends,
|
|
177
|
+
"timeToCapacity": time_estimate,
|
|
178
|
+
"predictions": predictions,
|
|
179
|
+
"recommendations": recommendations,
|
|
180
|
+
"reportDate": datetime.now().isoformat(),
|
|
181
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Index optimization suggestions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Any
|
|
6
|
+
from ..db.base import DatabaseConnection, StorageAnalysis
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IndexOptimizer:
|
|
10
|
+
"""Provide index optimization suggestions"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, connection: DatabaseConnection):
|
|
13
|
+
self.connection = connection
|
|
14
|
+
|
|
15
|
+
async def analyze_indexes(self, analysis: StorageAnalysis) -> List[Dict[str, Any]]:
|
|
16
|
+
"""Analyze indexes and provide optimization suggestions"""
|
|
17
|
+
suggestions = []
|
|
18
|
+
db_type = self.connection.config.type.lower()
|
|
19
|
+
|
|
20
|
+
if db_type in ["postgresql", "postgres"]:
|
|
21
|
+
suggestions = await self._analyze_postgres_indexes(analysis)
|
|
22
|
+
elif db_type in ["mysql", "mariadb"]:
|
|
23
|
+
suggestions = await self._analyze_mysql_indexes(analysis)
|
|
24
|
+
elif db_type == "sqlserver" or db_type == "mssql":
|
|
25
|
+
suggestions = await self._analyze_sqlserver_indexes(analysis)
|
|
26
|
+
|
|
27
|
+
return suggestions
|
|
28
|
+
|
|
29
|
+
async def _analyze_postgres_indexes(
|
|
30
|
+
self, analysis: StorageAnalysis
|
|
31
|
+
) -> List[Dict[str, Any]]:
|
|
32
|
+
"""Analyze PostgreSQL indexes"""
|
|
33
|
+
suggestions = []
|
|
34
|
+
|
|
35
|
+
# Check for tables without indexes
|
|
36
|
+
for table in analysis.get("tables", []):
|
|
37
|
+
table_indexes = [
|
|
38
|
+
idx
|
|
39
|
+
for idx in analysis.get("indexes", [])
|
|
40
|
+
if idx.get("tableName") == table.get("name")
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
if not table_indexes and table.get("rowCount", 0) > 1000:
|
|
44
|
+
suggestions.append(
|
|
45
|
+
{
|
|
46
|
+
"type": "missing_index",
|
|
47
|
+
"severity": "medium",
|
|
48
|
+
"table": table.get("name"),
|
|
49
|
+
"message": f"Table {table.get('name')} has {table.get('rowCount')} rows but no indexes",
|
|
50
|
+
"recommendation": f"Consider adding indexes on frequently queried columns in {table.get('name')}",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Check for high bloat indexes
|
|
55
|
+
for idx in table_indexes:
|
|
56
|
+
if idx.get("bloat", 0) > 30.0:
|
|
57
|
+
suggestions.append(
|
|
58
|
+
{
|
|
59
|
+
"type": "index_bloat",
|
|
60
|
+
"severity": "high",
|
|
61
|
+
"index": idx.get("name"),
|
|
62
|
+
"table": idx.get("tableName"),
|
|
63
|
+
"bloat": idx.get("bloat"),
|
|
64
|
+
"message": f"Index {idx.get('name')} has {idx.get('bloat'):.2f}% bloat",
|
|
65
|
+
"recommendation": f"Consider rebuilding index {idx.get('name')} to reduce bloat",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return suggestions
|
|
70
|
+
|
|
71
|
+
async def _analyze_mysql_indexes(
|
|
72
|
+
self, analysis: StorageAnalysis
|
|
73
|
+
) -> List[Dict[str, Any]]:
|
|
74
|
+
"""Analyze MySQL indexes"""
|
|
75
|
+
suggestions = []
|
|
76
|
+
|
|
77
|
+
# Check for large tables without indexes
|
|
78
|
+
for table in analysis.get("tables", []):
|
|
79
|
+
table_indexes = [
|
|
80
|
+
idx
|
|
81
|
+
for idx in analysis.get("indexes", [])
|
|
82
|
+
if idx.get("tableName") == table.get("name")
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if not table_indexes and table.get("rowCount", 0) > 1000:
|
|
86
|
+
suggestions.append(
|
|
87
|
+
{
|
|
88
|
+
"type": "missing_index",
|
|
89
|
+
"severity": "medium",
|
|
90
|
+
"table": table.get("name"),
|
|
91
|
+
"message": f"Table {table.get('name')} has {table.get('rowCount')} rows but no indexes",
|
|
92
|
+
"recommendation": f"Add indexes on foreign keys and frequently queried columns",
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return suggestions
|
|
97
|
+
|
|
98
|
+
async def _analyze_sqlserver_indexes(
|
|
99
|
+
self, analysis: StorageAnalysis
|
|
100
|
+
) -> List[Dict[str, Any]]:
|
|
101
|
+
"""Analyze SQL Server indexes"""
|
|
102
|
+
suggestions = []
|
|
103
|
+
|
|
104
|
+
# Check for missing indexes (SQL Server specific)
|
|
105
|
+
if self.connection.connected:
|
|
106
|
+
try:
|
|
107
|
+
result = await self.connection.execute_query(
|
|
108
|
+
"""
|
|
109
|
+
SELECT
|
|
110
|
+
OBJECT_NAME(d.object_id) AS table_name,
|
|
111
|
+
d.equality_columns,
|
|
112
|
+
d.inequality_columns,
|
|
113
|
+
d.included_columns,
|
|
114
|
+
s.avg_user_impact
|
|
115
|
+
FROM sys.dm_db_missing_index_details d
|
|
116
|
+
INNER JOIN sys.dm_db_missing_index_groups g ON d.index_handle = g.index_handle
|
|
117
|
+
INNER JOIN sys.dm_db_missing_index_group_stats s ON g.index_group_handle = s.group_handle
|
|
118
|
+
WHERE s.avg_user_impact > 50
|
|
119
|
+
ORDER BY s.avg_user_impact DESC
|
|
120
|
+
""",
|
|
121
|
+
safe_mode=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
for row in result.get("rows", []):
|
|
125
|
+
suggestions.append(
|
|
126
|
+
{
|
|
127
|
+
"type": "missing_index",
|
|
128
|
+
"severity": "high",
|
|
129
|
+
"table": row.get("table_name"),
|
|
130
|
+
"message": f"Missing index on {row.get('table_name')} could improve performance by {row.get('avg_user_impact')}%",
|
|
131
|
+
"recommendation": f"Create index on {row.get('equality_columns')} for table {row.get('table_name')}",
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
return suggestions
|
|
138
|
+
|
|
139
|
+
async def get_optimization_recommendations(
|
|
140
|
+
self, analysis: StorageAnalysis
|
|
141
|
+
) -> Dict[str, Any]:
|
|
142
|
+
"""Get comprehensive optimization recommendations"""
|
|
143
|
+
suggestions = await self.analyze_indexes(analysis)
|
|
144
|
+
|
|
145
|
+
high_priority = [s for s in suggestions if s.get("severity") == "high"]
|
|
146
|
+
medium_priority = [s for s in suggestions if s.get("severity") == "medium"]
|
|
147
|
+
low_priority = [s for s in suggestions if s.get("severity") == "low"]
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"totalSuggestions": len(suggestions),
|
|
151
|
+
"highPriority": high_priority,
|
|
152
|
+
"mediumPriority": medium_priority,
|
|
153
|
+
"lowPriority": low_priority,
|
|
154
|
+
"suggestions": suggestions,
|
|
155
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query performance analysis
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Dict, List, Any, Optional
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from ..db.base import DatabaseConnection, QueryResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class QueryPerformanceAnalyzer:
|
|
12
|
+
"""Analyze query performance"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, connection: DatabaseConnection):
|
|
15
|
+
self.connection = connection
|
|
16
|
+
self.query_history: List[Dict[str, Any]] = []
|
|
17
|
+
|
|
18
|
+
async def analyze_query(self, query: str) -> Dict[str, Any]:
|
|
19
|
+
"""Analyze a query's performance"""
|
|
20
|
+
start_time = time.time()
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
result = await self.connection.execute_query(query, safe_mode=True)
|
|
24
|
+
execution_time = (
|
|
25
|
+
time.time() - start_time
|
|
26
|
+
) * 1000 # Convert to milliseconds
|
|
27
|
+
|
|
28
|
+
analysis = {
|
|
29
|
+
"query": query,
|
|
30
|
+
"executionTime": execution_time,
|
|
31
|
+
"rowCount": result.get("rowCount", 0),
|
|
32
|
+
"columns": result.get("columns", []),
|
|
33
|
+
"timestamp": datetime.now().isoformat(),
|
|
34
|
+
"status": "success",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Get explain plan if available
|
|
38
|
+
try:
|
|
39
|
+
explain_plan = await self._get_explain_plan(query)
|
|
40
|
+
analysis["explainPlan"] = explain_plan
|
|
41
|
+
except Exception:
|
|
42
|
+
analysis["explainPlan"] = None
|
|
43
|
+
|
|
44
|
+
self.query_history.append(analysis)
|
|
45
|
+
return analysis
|
|
46
|
+
|
|
47
|
+
except Exception as e:
|
|
48
|
+
execution_time = (time.time() - start_time) * 1000
|
|
49
|
+
analysis = {
|
|
50
|
+
"query": query,
|
|
51
|
+
"executionTime": execution_time,
|
|
52
|
+
"rowCount": 0,
|
|
53
|
+
"columns": [],
|
|
54
|
+
"timestamp": datetime.now().isoformat(),
|
|
55
|
+
"status": "error",
|
|
56
|
+
"error": str(e),
|
|
57
|
+
}
|
|
58
|
+
self.query_history.append(analysis)
|
|
59
|
+
return analysis
|
|
60
|
+
|
|
61
|
+
async def _get_explain_plan(self, query: str) -> Optional[Dict[str, Any]]:
|
|
62
|
+
"""Get query explain plan"""
|
|
63
|
+
db_type = self.connection.config.type.lower()
|
|
64
|
+
|
|
65
|
+
if db_type in ["postgresql", "postgres"]:
|
|
66
|
+
explain_query = f"EXPLAIN ANALYZE {query}"
|
|
67
|
+
elif db_type in ["mysql", "mariadb"]:
|
|
68
|
+
explain_query = f"EXPLAIN {query}"
|
|
69
|
+
elif db_type == "sqlite":
|
|
70
|
+
explain_query = f"EXPLAIN QUERY PLAN {query}"
|
|
71
|
+
else:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
result = await self.connection.execute_query(explain_query, safe_mode=True)
|
|
76
|
+
return {
|
|
77
|
+
"plan": result.get("rows", []),
|
|
78
|
+
"columns": result.get("columns", []),
|
|
79
|
+
}
|
|
80
|
+
except Exception:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def get_slow_queries(self, threshold_ms: float = 1000.0) -> List[Dict[str, Any]]:
|
|
84
|
+
"""Get queries that took longer than threshold"""
|
|
85
|
+
return [
|
|
86
|
+
q
|
|
87
|
+
for q in self.query_history
|
|
88
|
+
if q.get("executionTime", 0) > threshold_ms and q.get("status") == "success"
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
def get_query_statistics(self) -> Dict[str, Any]:
|
|
92
|
+
"""Get query performance statistics"""
|
|
93
|
+
if not self.query_history:
|
|
94
|
+
return {
|
|
95
|
+
"totalQueries": 0,
|
|
96
|
+
"averageExecutionTime": 0,
|
|
97
|
+
"slowQueries": 0,
|
|
98
|
+
"errorQueries": 0,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
successful_queries = [
|
|
102
|
+
q for q in self.query_history if q.get("status") == "success"
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
if successful_queries:
|
|
106
|
+
from ..native import aggregate_f64
|
|
107
|
+
|
|
108
|
+
times = [float(q.get("executionTime", 0) or 0) for q in successful_queries]
|
|
109
|
+
_count, _total, min_time, max_time, avg_time = aggregate_f64(times)
|
|
110
|
+
slow_queries = len(self.get_slow_queries())
|
|
111
|
+
else:
|
|
112
|
+
avg_time = 0
|
|
113
|
+
min_time = 0
|
|
114
|
+
max_time = 0
|
|
115
|
+
slow_queries = 0
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"totalQueries": len(self.query_history),
|
|
119
|
+
"averageExecutionTime": avg_time,
|
|
120
|
+
"minExecutionTime": min_time,
|
|
121
|
+
"maxExecutionTime": max_time,
|
|
122
|
+
"slowQueries": slow_queries,
|
|
123
|
+
"errorQueries": len(
|
|
124
|
+
[q for q in self.query_history if q.get("status") == "error"]
|
|
125
|
+
),
|
|
126
|
+
"successfulQueries": len(successful_queries),
|
|
127
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Storage growth predictions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Any, Optional
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
import statistics
|
|
8
|
+
from ..db.base import DatabaseConnection, StorageAnalysis
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StorageGrowthPredictor:
|
|
12
|
+
"""Predict storage growth based on historical data"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, connection: DatabaseConnection):
|
|
15
|
+
self.connection = connection
|
|
16
|
+
self.history: List[Dict[str, Any]] = []
|
|
17
|
+
|
|
18
|
+
def add_analysis(self, analysis: StorageAnalysis) -> None:
|
|
19
|
+
"""Add analysis data point to history"""
|
|
20
|
+
self.history.append(
|
|
21
|
+
{
|
|
22
|
+
"timestamp": datetime.now().isoformat(),
|
|
23
|
+
"totalSize": analysis.get("totalSize", 0),
|
|
24
|
+
"tableCount": analysis.get("tableCount", 0),
|
|
25
|
+
"analysis": analysis,
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def predict_growth(self, days: int = 30) -> Dict[str, Any]:
|
|
30
|
+
"""Predict storage growth for the next N days"""
|
|
31
|
+
if len(self.history) < 2:
|
|
32
|
+
return {
|
|
33
|
+
"predictedSize": 0,
|
|
34
|
+
"growthRate": 0,
|
|
35
|
+
"confidence": "low",
|
|
36
|
+
"message": "Insufficient historical data for prediction",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Calculate growth rate
|
|
40
|
+
sizes = [h["totalSize"] for h in self.history]
|
|
41
|
+
growth_rates = []
|
|
42
|
+
|
|
43
|
+
for i in range(1, len(sizes)):
|
|
44
|
+
if sizes[i - 1] > 0:
|
|
45
|
+
rate = (sizes[i] - sizes[i - 1]) / sizes[i - 1]
|
|
46
|
+
growth_rates.append(rate)
|
|
47
|
+
|
|
48
|
+
if not growth_rates:
|
|
49
|
+
return {
|
|
50
|
+
"predictedSize": sizes[-1] if sizes else 0,
|
|
51
|
+
"growthRate": 0,
|
|
52
|
+
"confidence": "low",
|
|
53
|
+
"message": "No growth detected",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
avg_growth_rate = statistics.mean(growth_rates)
|
|
57
|
+
current_size = sizes[-1]
|
|
58
|
+
|
|
59
|
+
# Predict future size
|
|
60
|
+
predicted_size = current_size * (1 + avg_growth_rate) ** days
|
|
61
|
+
|
|
62
|
+
# Calculate confidence based on data points
|
|
63
|
+
confidence = (
|
|
64
|
+
"high"
|
|
65
|
+
if len(self.history) >= 10
|
|
66
|
+
else "medium" if len(self.history) >= 5 else "low"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"predictedSize": int(predicted_size),
|
|
71
|
+
"currentSize": current_size,
|
|
72
|
+
"growthRate": avg_growth_rate * 100, # Percentage
|
|
73
|
+
"growthAmount": int(predicted_size - current_size),
|
|
74
|
+
"days": days,
|
|
75
|
+
"confidence": confidence,
|
|
76
|
+
"predictedDate": (datetime.now() + timedelta(days=days)).isoformat(),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def predict_table_growth(self, table_name: str, days: int = 30) -> Dict[str, Any]:
|
|
80
|
+
"""Predict growth for a specific table"""
|
|
81
|
+
table_history = []
|
|
82
|
+
|
|
83
|
+
for h in self.history:
|
|
84
|
+
analysis = h.get("analysis", {})
|
|
85
|
+
for table in analysis.get("tables", []):
|
|
86
|
+
if table.get("name") == table_name:
|
|
87
|
+
table_history.append(
|
|
88
|
+
{
|
|
89
|
+
"timestamp": h["timestamp"],
|
|
90
|
+
"size": table.get("size", 0),
|
|
91
|
+
"rowCount": table.get("rowCount", 0),
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if len(table_history) < 2:
|
|
96
|
+
return {
|
|
97
|
+
"predictedSize": 0,
|
|
98
|
+
"growthRate": 0,
|
|
99
|
+
"confidence": "low",
|
|
100
|
+
"message": f"Insufficient data for table {table_name}",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
sizes = [h["size"] for h in table_history]
|
|
104
|
+
growth_rates = []
|
|
105
|
+
|
|
106
|
+
for i in range(1, len(sizes)):
|
|
107
|
+
if sizes[i - 1] > 0:
|
|
108
|
+
rate = (sizes[i] - sizes[i - 1]) / sizes[i - 1]
|
|
109
|
+
growth_rates.append(rate)
|
|
110
|
+
|
|
111
|
+
if not growth_rates:
|
|
112
|
+
return {
|
|
113
|
+
"predictedSize": sizes[-1] if sizes else 0,
|
|
114
|
+
"growthRate": 0,
|
|
115
|
+
"confidence": "low",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
avg_growth_rate = statistics.mean(growth_rates)
|
|
119
|
+
current_size = sizes[-1]
|
|
120
|
+
predicted_size = current_size * (1 + avg_growth_rate) ** days
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"tableName": table_name,
|
|
124
|
+
"predictedSize": int(predicted_size),
|
|
125
|
+
"currentSize": current_size,
|
|
126
|
+
"growthRate": avg_growth_rate * 100,
|
|
127
|
+
"growthAmount": int(predicted_size - current_size),
|
|
128
|
+
"days": days,
|
|
129
|
+
"confidence": "high" if len(table_history) >= 10 else "medium",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
def get_growth_trends(self) -> Dict[str, Any]:
|
|
133
|
+
"""Get overall growth trends"""
|
|
134
|
+
if len(self.history) < 2:
|
|
135
|
+
return {
|
|
136
|
+
"trend": "insufficient_data",
|
|
137
|
+
"message": "Need at least 2 data points",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
sizes = [h["totalSize"] for h in self.history]
|
|
141
|
+
growth_rates = []
|
|
142
|
+
|
|
143
|
+
for i in range(1, len(sizes)):
|
|
144
|
+
if sizes[i - 1] > 0:
|
|
145
|
+
rate = (sizes[i] - sizes[i - 1]) / sizes[i - 1]
|
|
146
|
+
growth_rates.append(rate)
|
|
147
|
+
|
|
148
|
+
if not growth_rates:
|
|
149
|
+
return {"trend": "stable", "growthRate": 0}
|
|
150
|
+
|
|
151
|
+
avg_rate = statistics.mean(growth_rates)
|
|
152
|
+
|
|
153
|
+
if avg_rate > 0.1:
|
|
154
|
+
trend = "rapid_growth"
|
|
155
|
+
elif avg_rate > 0.01:
|
|
156
|
+
trend = "moderate_growth"
|
|
157
|
+
elif avg_rate > 0:
|
|
158
|
+
trend = "slow_growth"
|
|
159
|
+
elif avg_rate == 0:
|
|
160
|
+
trend = "stable"
|
|
161
|
+
else:
|
|
162
|
+
trend = "declining"
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"trend": trend,
|
|
166
|
+
"growthRate": avg_rate * 100,
|
|
167
|
+
"dataPoints": len(self.history),
|
|
168
|
+
"totalGrowth": sizes[-1] - sizes[0] if sizes else 0,
|
|
169
|
+
}
|