mysql_genius 0.7.1 → 0.8.0
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +22 -0
- data/app/controllers/concerns/mysql_genius/ai_features.rb +55 -0
- data/app/views/layouts/mysql_genius/application.html.erb +27 -0
- data/config/routes.rb +6 -0
- data/docs/guides/ai-features.md +115 -0
- data/docs/guides/getting-started-rails.md +118 -0
- data/docs/guides/ssh-tunnel-connections.md +151 -0
- data/lib/mysql_genius/version.rb +1 -1
- data/mysql_genius.gemspec +1 -1
- data/ralph/prd.json +64 -121
- data/ralph/progress.txt +97 -119
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15b1fc129dbade000c7a7467601b927804f11f73892a0f450f92f6ffc1d4b313
|
|
4
|
+
data.tar.gz: d01c43dd9e9e06212c6fa85cad829fe940c982b1a1cf412ae02336de69bde376
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b9e69fee6f8b6c79e36bdccca6c838876f10fb79b333464496f37aa14db50d42b2507a4dfd18e0162c2b612af4279bf4ea731cd765da83068a23f3e303c340e
|
|
7
|
+
data.tar.gz: cf0730102bb3ce854f669dcf77343c3f5f0704996e1fa5bbe0c08c6a02f34110c59dfa53d9a303044f5c8ac6417f9e6f2863d3cc1d2535e84f108922b3379d0e
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **6 new AI analysis features:**
|
|
7
|
+
- **Variable Config Reviewer** — reviews my.cnf settings against observed workload
|
|
8
|
+
- **Connection Pressure Advisor** — diagnoses connection pool health
|
|
9
|
+
- **Workload Digest** — executive summary of the entire query workload
|
|
10
|
+
- **InnoDB Health Interpreter** — plain English translation of `SHOW ENGINE INNODB STATUS`
|
|
11
|
+
- **Index Consolidation Planner** — holistic drop/merge/add index plan across tables
|
|
12
|
+
- **Slow Query Pattern Grouper** — groups slow queries by shared root cause
|
|
13
|
+
- AI buttons added to Server tab, Query Stats tab, and Indexes tabs
|
|
14
|
+
- `mysql_genius` now declares runtime dependency on `mysql_genius-core ~> 0.8.0`
|
|
15
|
+
|
|
16
|
+
## 0.7.2
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Anthropic Messages API support** — `x-api-key` auth style with `anthropic-version` header, top-level `system` parameter, `content[0].text` response parsing.
|
|
20
|
+
- **Configurable `max_tokens`** — new field on `Core::Ai::Config` (default 4096), sent to both OpenAI and Anthropic APIs.
|
|
21
|
+
- **Copy response button** on all AI result sections (schema review, migration risk, optimization, describe query, rewrite, index advisor, root cause, anomaly detection).
|
|
22
|
+
- **Dark mode contrast fixes** for AI result sections — proper CSS classes with dark-mode variants replace hardcoded light-mode inline styles.
|
|
23
|
+
- **`capability?(:standalone_header)`** guard hides the dashboard header when rendered inside a layout that already provides one.
|
|
24
|
+
|
|
3
25
|
## 0.7.1
|
|
4
26
|
|
|
5
27
|
Lockstep version bump with `mysql_genius-core 0.7.1` which fixes missing ERB templates in the gem package.
|
|
@@ -232,6 +232,61 @@ module MysqlGenius
|
|
|
232
232
|
render(json: { error: "Migration risk assessment failed: #{e.message}" }, status: :unprocessable_entity)
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
+
def variable_review
|
|
236
|
+
return ai_not_configured unless mysql_genius_config.ai_enabled?
|
|
237
|
+
|
|
238
|
+
result = MysqlGenius::Core::Ai::VariableReviewer.new(ai_client, ai_config_for_core, rails_connection).call
|
|
239
|
+
render(json: result)
|
|
240
|
+
rescue StandardError => e
|
|
241
|
+
render(json: { error: "Variable review failed: #{e.message}" }, status: :unprocessable_entity)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def connection_advisor
|
|
245
|
+
return ai_not_configured unless mysql_genius_config.ai_enabled?
|
|
246
|
+
|
|
247
|
+
result = MysqlGenius::Core::Ai::ConnectionAdvisor.new(ai_client, ai_config_for_core, rails_connection).call
|
|
248
|
+
render(json: result)
|
|
249
|
+
rescue StandardError => e
|
|
250
|
+
render(json: { error: "Connection advisor failed: #{e.message}" }, status: :unprocessable_entity)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def workload_digest
|
|
254
|
+
return ai_not_configured unless mysql_genius_config.ai_enabled?
|
|
255
|
+
|
|
256
|
+
result = MysqlGenius::Core::Ai::WorkloadDigest.new(rails_connection, ai_client, ai_config_for_core).call
|
|
257
|
+
render(json: result)
|
|
258
|
+
rescue StandardError => e
|
|
259
|
+
render(json: { error: "Workload digest failed: #{e.message}" }, status: :unprocessable_entity)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def innodb_health
|
|
263
|
+
return ai_not_configured unless mysql_genius_config.ai_enabled?
|
|
264
|
+
|
|
265
|
+
result = MysqlGenius::Core::Ai::InnodbInterpreter.new(ai_client, ai_config_for_core, rails_connection).call
|
|
266
|
+
render(json: result)
|
|
267
|
+
rescue StandardError => e
|
|
268
|
+
render(json: { error: "InnoDB health analysis failed: #{e.message}" }, status: :unprocessable_entity)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def index_planner
|
|
272
|
+
return ai_not_configured unless mysql_genius_config.ai_enabled?
|
|
273
|
+
|
|
274
|
+
tables = params[:tables].present? ? Array(params[:tables]) : nil
|
|
275
|
+
result = MysqlGenius::Core::Ai::IndexPlanner.new(ai_client, ai_config_for_core, rails_connection).call(tables)
|
|
276
|
+
render(json: result)
|
|
277
|
+
rescue StandardError => e
|
|
278
|
+
render(json: { error: "Index planner failed: #{e.message}" }, status: :unprocessable_entity)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def pattern_grouper
|
|
282
|
+
return ai_not_configured unless mysql_genius_config.ai_enabled?
|
|
283
|
+
|
|
284
|
+
result = MysqlGenius::Core::Ai::PatternGrouper.new(rails_connection, ai_client, ai_config_for_core).call
|
|
285
|
+
render(json: result)
|
|
286
|
+
rescue StandardError => e
|
|
287
|
+
render(json: { error: "Pattern grouper failed: #{e.message}" }, status: :unprocessable_entity)
|
|
288
|
+
end
|
|
289
|
+
|
|
235
290
|
private
|
|
236
291
|
|
|
237
292
|
RAILS_DOMAIN_CONTEXT = <<~CTX
|
|
@@ -145,6 +145,17 @@
|
|
|
145
145
|
code { font-size: 12px; word-break: break-all; background: #f0f1f3; padding: 2px 6px; border-radius: 3px; color: #24292f; }
|
|
146
146
|
pre.mg-pre { background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
|
|
147
147
|
|
|
148
|
+
/* AI result sections */
|
|
149
|
+
.mg-ai-section { border-radius: 4px; border: 1px solid #dee2e6; border-left-width: 4px; margin-bottom: 12px; }
|
|
150
|
+
.mg-ai-section-header { padding: 8px 12px; border-bottom: 1px solid #dee2e6; font-size: 13px; }
|
|
151
|
+
.mg-ai-section-body { padding: 12px; font-size: 13px; }
|
|
152
|
+
.mg-ai-danger { border-left-color: #dc3545; background: #fff5f5; }
|
|
153
|
+
.mg-ai-danger .mg-ai-section-header { border-bottom-color: #f5c6cb; }
|
|
154
|
+
.mg-ai-warning { border-left-color: #ffc107; background: #fffbeb; }
|
|
155
|
+
.mg-ai-warning .mg-ai-section-header { border-bottom-color: #ffeeba; }
|
|
156
|
+
.mg-ai-info { border-left-color: #17a2b8; background: #f0f9ff; }
|
|
157
|
+
.mg-ai-info .mg-ai-section-header { border-bottom-color: #bee5eb; }
|
|
158
|
+
|
|
148
159
|
/* Theme toggle */
|
|
149
160
|
.mg-theme-toggle { background: none; border: 1px solid #ced4da; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 14px; line-height: 1; color: inherit; }
|
|
150
161
|
.mg-theme-toggle:hover { background: #e9ecef; }
|
|
@@ -220,7 +231,13 @@
|
|
|
220
231
|
|
|
221
232
|
/* Inline code */
|
|
222
233
|
[data-theme="dark"] code { background: #21262d; color: #c9d1d9; }
|
|
234
|
+
[data-theme="dark"] pre { background: #161b22; color: #c9d1d9; border-color: #30363d; }
|
|
223
235
|
[data-theme="dark"] pre.mg-pre { background: #161b22; color: #c9d1d9; }
|
|
236
|
+
[data-theme="dark"] .mg-card-body { color: #c9d1d9; }
|
|
237
|
+
[data-theme="dark"] .mg-card-body h1, [data-theme="dark"] .mg-card-body h2, [data-theme="dark"] .mg-card-body h3, [data-theme="dark"] .mg-card-body h4 { color: #c9d1d9; }
|
|
238
|
+
[data-theme="dark"] .mg-card-body p, [data-theme="dark"] .mg-card-body li, [data-theme="dark"] .mg-card-body td { color: #c9d1d9; }
|
|
239
|
+
[data-theme="dark"] .mg-card-body strong { color: #e6edf3; }
|
|
240
|
+
[data-theme="dark"] .mg-card-body a { color: #58a6ff; }
|
|
224
241
|
|
|
225
242
|
/* Links */
|
|
226
243
|
[data-theme="dark"] .mg-link { color: #58a6ff; }
|
|
@@ -234,6 +251,16 @@
|
|
|
234
251
|
/* Checkboxes */
|
|
235
252
|
[data-theme="dark"] .mg-check .type-hint { color: #484f58; }
|
|
236
253
|
|
|
254
|
+
/* AI result sections (dark) */
|
|
255
|
+
[data-theme="dark"] .mg-ai-section { border-color: #30363d; }
|
|
256
|
+
[data-theme="dark"] .mg-ai-danger { background: #2d0d0d; border-left-color: #f85149; }
|
|
257
|
+
[data-theme="dark"] .mg-ai-danger .mg-ai-section-header { border-bottom-color: #3d1414; color: #f85149; }
|
|
258
|
+
[data-theme="dark"] .mg-ai-warning { background: #2d2000; border-left-color: #d29922; }
|
|
259
|
+
[data-theme="dark"] .mg-ai-warning .mg-ai-section-header { border-bottom-color: #3d2e00; color: #d29922; }
|
|
260
|
+
[data-theme="dark"] .mg-ai-info { background: #0d2a3a; border-left-color: #58a6ff; }
|
|
261
|
+
[data-theme="dark"] .mg-ai-info .mg-ai-section-header { border-bottom-color: #0d3a5a; color: #58a6ff; }
|
|
262
|
+
[data-theme="dark"] .mg-ai-section-body { color: #c9d1d9; }
|
|
263
|
+
|
|
237
264
|
/* Theme toggle (dark) */
|
|
238
265
|
[data-theme="dark"] .mg-theme-toggle { border-color: #30363d; color: #c9d1d9; }
|
|
239
266
|
[data-theme="dark"] .mg-theme-toggle:hover { background: #21262d; }
|
data/config/routes.rb
CHANGED
|
@@ -25,4 +25,10 @@ MysqlGenius::Engine.routes.draw do
|
|
|
25
25
|
post "anomaly_detection", to: "queries#anomaly_detection"
|
|
26
26
|
post "root_cause", to: "queries#root_cause"
|
|
27
27
|
post "migration_risk", to: "queries#migration_risk"
|
|
28
|
+
post "variable_review", to: "queries#variable_review"
|
|
29
|
+
post "connection_advisor", to: "queries#connection_advisor"
|
|
30
|
+
post "workload_digest", to: "queries#workload_digest"
|
|
31
|
+
post "innodb_health", to: "queries#innodb_health"
|
|
32
|
+
post "index_planner", to: "queries#index_planner"
|
|
33
|
+
post "pattern_grouper", to: "queries#pattern_grouper"
|
|
28
34
|
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# AI Features Guide
|
|
2
|
+
|
|
3
|
+
MysqlGenius integrates with OpenAI-compatible LLM APIs to provide AI-powered database analysis. All AI features are optional — the dashboard works fully without them.
|
|
4
|
+
|
|
5
|
+
## Supported providers
|
|
6
|
+
|
|
7
|
+
| Provider | Endpoint | Auth Style |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| OpenAI | `https://api.openai.com/v1/chat/completions` | Bearer |
|
|
10
|
+
| Anthropic | `https://api.anthropic.com/v1/messages` | x-api-key |
|
|
11
|
+
| Google Gemini | `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` | Bearer |
|
|
12
|
+
| Azure OpenAI | `https://YOUR-RESOURCE.openai.azure.com/...` | api-key |
|
|
13
|
+
| DeepSeek | `https://api.deepseek.com/chat/completions` | Bearer |
|
|
14
|
+
| Groq | `https://api.groq.com/openai/v1/chat/completions` | Bearer |
|
|
15
|
+
| Ollama (local) | `http://localhost:11434/v1/chat/completions` | Bearer |
|
|
16
|
+
| OpenRouter | `https://openrouter.ai/api/v1/chat/completions` | Bearer |
|
|
17
|
+
| Perplexity | `https://api.perplexity.ai/chat/completions` | Bearer |
|
|
18
|
+
| Any OpenAI-compatible | Your endpoint URL | Bearer or api-key |
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
In your Rails initializer (`config/initializers/mysql_genius.rb`):
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
MysqlGenius.configure do |config|
|
|
26
|
+
config.ai_endpoint = "https://api.openai.com/v1/chat/completions"
|
|
27
|
+
config.ai_api_key = ENV["OPENAI_API_KEY"]
|
|
28
|
+
config.ai_model = "gpt-4o-mini"
|
|
29
|
+
config.ai_auth_style = :bearer # or :api_key for Azure, :x_api_key for Anthropic
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Available features
|
|
34
|
+
|
|
35
|
+
### Query Explorer AI
|
|
36
|
+
|
|
37
|
+
| Feature | What it does | Where |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| **AI Suggest** | Generate SQL from natural language | Query Explorer tab |
|
|
40
|
+
| **AI Optimization** | Analyze EXPLAIN output, suggest improvements | After running EXPLAIN |
|
|
41
|
+
| **Index Advisor** | Recommend indexes for a specific query | After running EXPLAIN |
|
|
42
|
+
| **Describe Query** | Explain what a SQL query does in plain English | Query Explorer tab |
|
|
43
|
+
| **Rewrite Query** | Suggest an optimized version of the SQL | Query Explorer tab |
|
|
44
|
+
|
|
45
|
+
### Schema & Migration
|
|
46
|
+
|
|
47
|
+
| Feature | What it does | Where |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| **Schema Review** | Find anti-patterns across your schema | AI Tools tab |
|
|
50
|
+
| **Migration Risk** | Assess safety of a DDL/migration before deploying | AI Tools tab |
|
|
51
|
+
| **AI Optimize** | Review a specific table's schema (appears on fragmented tables) | Tables tab |
|
|
52
|
+
|
|
53
|
+
### Server Analysis
|
|
54
|
+
|
|
55
|
+
| Feature | What it does | Where |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| **Variable Config Review** | Review my.cnf settings against your workload | Server tab |
|
|
58
|
+
| **Connection Advisor** | Diagnose connection pool issues | Server tab |
|
|
59
|
+
| **InnoDB Health** | Interpret SHOW ENGINE INNODB STATUS | Server tab |
|
|
60
|
+
|
|
61
|
+
### Workload Analysis
|
|
62
|
+
|
|
63
|
+
| Feature | What it does | Where |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| **Workload Digest** | Executive summary of your query workload | Query Stats tab |
|
|
66
|
+
| **Pattern Grouper** | Group slow queries by shared root cause | Query Stats tab |
|
|
67
|
+
| **Index Planner** | Holistic index optimization plan | Indexes tabs |
|
|
68
|
+
|
|
69
|
+
## Settings
|
|
70
|
+
|
|
71
|
+
### Max Tokens
|
|
72
|
+
|
|
73
|
+
Controls the maximum response length from the LLM. Default: 4096. Adjust with the slider in AI Configuration.
|
|
74
|
+
|
|
75
|
+
- **Lower (256-1024)** — faster responses, may truncate complex analyses
|
|
76
|
+
- **Higher (4096-16384)** — complete analyses, slower and more expensive
|
|
77
|
+
|
|
78
|
+
### System Prompt
|
|
79
|
+
|
|
80
|
+
Optional context injected into every AI request. Use it to describe your application:
|
|
81
|
+
|
|
82
|
+
> "This is an e-commerce platform with 50M orders. The database handles 5000 QPS during peak hours."
|
|
83
|
+
|
|
84
|
+
### Domain Prompt
|
|
85
|
+
|
|
86
|
+
Optional instructions for the AI's recommendations:
|
|
87
|
+
|
|
88
|
+
> "Prefer window functions over correlated subqueries. Don't recommend foreign keys — we handle referential integrity in the application layer."
|
|
89
|
+
|
|
90
|
+
## Using with Ollama (free, local)
|
|
91
|
+
|
|
92
|
+
1. Install Ollama: `brew install ollama`
|
|
93
|
+
2. Pull a model: `ollama pull llama3.2`
|
|
94
|
+
3. Start Ollama: `ollama serve`
|
|
95
|
+
4. In MysqlGenius, select **Ollama (local)** as the provider
|
|
96
|
+
5. The endpoint and model auto-fill — just click **Save**
|
|
97
|
+
|
|
98
|
+
No API key needed. All data stays on your machine.
|
|
99
|
+
|
|
100
|
+
## Copying AI responses
|
|
101
|
+
|
|
102
|
+
Every AI response has a **Copy response** button at the bottom right. Click it to copy the plain text to your clipboard — useful for sharing with team members or pasting into tickets.
|
|
103
|
+
|
|
104
|
+
## Cost considerations
|
|
105
|
+
|
|
106
|
+
Each AI feature makes one API call per invocation. Typical costs with OpenAI gpt-4o-mini:
|
|
107
|
+
|
|
108
|
+
| Feature | ~Input tokens | ~Output tokens | ~Cost |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| Schema Review (all tables) | 2000-5000 | 500-2000 | $0.001-0.005 |
|
|
111
|
+
| Migration Risk | 500-1000 | 500-1000 | $0.001 |
|
|
112
|
+
| Query Optimization | 1000-2000 | 500-1000 | $0.001 |
|
|
113
|
+
| Workload Digest | 3000-5000 | 1000-2000 | $0.003 |
|
|
114
|
+
|
|
115
|
+
Using Ollama or other local models eliminates API costs entirely.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Getting Started with MysqlGenius (Rails)
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
Add to your Gemfile:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem "mysql_genius"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Mount the engine
|
|
16
|
+
|
|
17
|
+
In `config/routes.rb`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
Rails.application.routes.draw do
|
|
21
|
+
mount MysqlGenius::Engine, at: "/mysql_genius"
|
|
22
|
+
# ... your other routes
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
Create an initializer at `config/initializers/mysql_genius.rb`:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
MysqlGenius.configure do |config|
|
|
32
|
+
# Authentication (required in production)
|
|
33
|
+
config.authenticate = ->(controller) {
|
|
34
|
+
# Example: restrict to admin users
|
|
35
|
+
controller.current_user&.admin?
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Tables to hide from the dashboard
|
|
39
|
+
config.blocked_tables = %w[
|
|
40
|
+
schema_migrations
|
|
41
|
+
ar_internal_metadata
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Columns to mask in query results
|
|
45
|
+
config.masked_column_patterns = %w[
|
|
46
|
+
password
|
|
47
|
+
token
|
|
48
|
+
secret
|
|
49
|
+
ssn
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# Query limits
|
|
53
|
+
config.default_row_limit = 100
|
|
54
|
+
config.max_row_limit = 10_000
|
|
55
|
+
config.query_timeout_ms = 10_000
|
|
56
|
+
|
|
57
|
+
# AI features (optional)
|
|
58
|
+
config.ai_endpoint = ENV["MYSQL_GENIUS_AI_ENDPOINT"]
|
|
59
|
+
config.ai_api_key = ENV["MYSQL_GENIUS_AI_KEY"]
|
|
60
|
+
config.ai_model = "gpt-4o-mini"
|
|
61
|
+
config.ai_auth_style = :bearer
|
|
62
|
+
|
|
63
|
+
# Stats collection (background thread, default: true)
|
|
64
|
+
config.stats_collection = true
|
|
65
|
+
|
|
66
|
+
# Slow query monitoring via Redis (optional)
|
|
67
|
+
# config.redis_url = ENV["REDIS_URL"]
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Visit the dashboard
|
|
72
|
+
|
|
73
|
+
Start your Rails server and navigate to:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
http://localhost:3000/mysql_genius
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Features
|
|
80
|
+
|
|
81
|
+
### Dashboard
|
|
82
|
+
Overview of server health, top slow queries, expensive queries, duplicate/unused index counts.
|
|
83
|
+
|
|
84
|
+
### Query Explorer
|
|
85
|
+
Run SELECT queries against your database with syntax highlighting, EXPLAIN output, and AI-powered suggestions.
|
|
86
|
+
|
|
87
|
+
### Query Stats
|
|
88
|
+
Top queries from `performance_schema.events_statements_summary_by_digest`. Click any query to see a detail page with time-series performance charts.
|
|
89
|
+
|
|
90
|
+
### Server
|
|
91
|
+
Server status, connections, InnoDB buffer pool, query activity.
|
|
92
|
+
|
|
93
|
+
### Tables
|
|
94
|
+
Table sizes with fragmentation detection. Tables needing optimization get an "AI Optimize" button.
|
|
95
|
+
|
|
96
|
+
### Indexes
|
|
97
|
+
Duplicate and unused index detection.
|
|
98
|
+
|
|
99
|
+
### AI Tools (when configured)
|
|
100
|
+
- **Schema Review** — find anti-patterns in your schema
|
|
101
|
+
- **Migration Risk** — assess DDL safety before deploying
|
|
102
|
+
- **Query Description** — explain what a query does in plain English
|
|
103
|
+
- **Query Rewrite** — suggest optimized versions of slow queries
|
|
104
|
+
- **Index Advisor** — recommend indexes for specific queries
|
|
105
|
+
|
|
106
|
+
## Security
|
|
107
|
+
|
|
108
|
+
**Always configure authentication in production.** Without it, anyone who can reach the mounted path can view your database schema and run SELECT queries.
|
|
109
|
+
|
|
110
|
+
The `blocked_tables` and `masked_column_patterns` settings provide defense-in-depth but are not a substitute for authentication.
|
|
111
|
+
|
|
112
|
+
## Supported databases
|
|
113
|
+
|
|
114
|
+
- MySQL 5.7+
|
|
115
|
+
- MySQL 8.0+
|
|
116
|
+
- MariaDB 10.3+
|
|
117
|
+
|
|
118
|
+
Some features (query stats, unused indexes) require `performance_schema` to be enabled.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Connecting to MySQL Through an SSH Tunnel
|
|
2
|
+
|
|
3
|
+
If your MySQL server is behind a firewall, on a private network, or only accessible via a bastion/jump host, you can use an SSH tunnel to connect MysqlGenius to it.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
An SSH tunnel forwards a local port on your machine to the MySQL port on the remote server. MysqlGenius connects to `localhost:<local_port>` and the tunnel transparently routes traffic to the actual database server.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Your Machine (MysqlGenius) → SSH Tunnel → Bastion Host → MySQL Server
|
|
11
|
+
localhost:3307 db.internal:3306
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Step 1: Open the SSH tunnel
|
|
15
|
+
|
|
16
|
+
In a terminal, run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
ssh -L 3307:db.internal:3306 user@bastion-host.example.com -N
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Flags explained:**
|
|
23
|
+
- `-L 3307:db.internal:3306` — forward local port `3307` to `db.internal:3306` through the tunnel
|
|
24
|
+
- `user@bastion-host.example.com` — your SSH login on the bastion/jump host
|
|
25
|
+
- `-N` — don't open a shell, just forward the port
|
|
26
|
+
|
|
27
|
+
**With an SSH key:**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
ssh -L 3307:db.internal:3306 user@bastion-host.example.com -N -i ~/.ssh/my_key
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Keep it running in the background:**
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
ssh -L 3307:db.internal:3306 user@bastion-host.example.com -N -f
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The `-f` flag sends SSH to the background after connecting.
|
|
40
|
+
|
|
41
|
+
## Step 2: Configure your Rails app
|
|
42
|
+
|
|
43
|
+
MysqlGenius uses your app's `ActiveRecord::Base.connection`, which reads from `database.yml`. Point it at the tunnel:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
# config/database.yml
|
|
47
|
+
production:
|
|
48
|
+
adapter: mysql2
|
|
49
|
+
host: 127.0.0.1
|
|
50
|
+
port: 3307
|
|
51
|
+
username: readonly
|
|
52
|
+
password: <%= ENV["DB_PASSWORD"] %>
|
|
53
|
+
database: app_production
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
No special MysqlGenius configuration needed — it automatically uses the same connection as your Rails app.
|
|
57
|
+
|
|
58
|
+
## Step 3: Verify the connection
|
|
59
|
+
|
|
60
|
+
Start your Rails server and visit `/mysql_genius`. If the tunnel is running, the dashboard loads normally. If not, you'll see a connection error.
|
|
61
|
+
|
|
62
|
+
## Common issues
|
|
63
|
+
|
|
64
|
+
### "Connection refused"
|
|
65
|
+
|
|
66
|
+
The SSH tunnel is not running. Start it first:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
ssh -L 3307:db.internal:3306 user@bastion -N
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### "Access denied"
|
|
73
|
+
|
|
74
|
+
The tunnel is working but the MySQL credentials are wrong. Verify your username/password can connect to the database directly from the bastion host.
|
|
75
|
+
|
|
76
|
+
### "Lost connection to MySQL server during query"
|
|
77
|
+
|
|
78
|
+
The SSH tunnel dropped. This happens if the tunnel is idle for too long. Add keep-alive settings:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
ssh -L 3307:db.internal:3306 user@bastion -N -o ServerAliveInterval=60 -o ServerAliveCountMax=3
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Port already in use
|
|
85
|
+
|
|
86
|
+
Another process is using port 3307. Pick a different local port:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
ssh -L 3308:db.internal:3306 user@bastion -N
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then update your MysqlGenius config to use port `3308`.
|
|
93
|
+
|
|
94
|
+
## Multiple databases through one bastion
|
|
95
|
+
|
|
96
|
+
You can tunnel to multiple MySQL servers through the same bastion:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Production on local port 3307
|
|
100
|
+
ssh -L 3307:db-prod.internal:3306 user@bastion -N -f
|
|
101
|
+
|
|
102
|
+
# Staging on local port 3308
|
|
103
|
+
ssh -L 3308:db-staging.internal:3306 user@bastion -N -f
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Then create separate MysqlGenius profiles for each:
|
|
107
|
+
- **Production**: `127.0.0.1:3307`
|
|
108
|
+
- **Staging**: `127.0.0.1:3308`
|
|
109
|
+
|
|
110
|
+
## Automating the tunnel
|
|
111
|
+
|
|
112
|
+
### macOS: Launch Agent
|
|
113
|
+
|
|
114
|
+
Create `~/Library/LaunchAgents/com.mysqlgenius.tunnel.plist`:
|
|
115
|
+
|
|
116
|
+
```xml
|
|
117
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
118
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
119
|
+
<plist version="1.0">
|
|
120
|
+
<dict>
|
|
121
|
+
<key>Label</key>
|
|
122
|
+
<string>com.mysqlgenius.tunnel</string>
|
|
123
|
+
<key>ProgramArguments</key>
|
|
124
|
+
<array>
|
|
125
|
+
<string>ssh</string>
|
|
126
|
+
<string>-L</string>
|
|
127
|
+
<string>3307:db.internal:3306</string>
|
|
128
|
+
<string>user@bastion</string>
|
|
129
|
+
<string>-N</string>
|
|
130
|
+
<string>-o</string>
|
|
131
|
+
<string>ServerAliveInterval=60</string>
|
|
132
|
+
</array>
|
|
133
|
+
<key>KeepAlive</key>
|
|
134
|
+
<true/>
|
|
135
|
+
<key>RunAtLoad</key>
|
|
136
|
+
<true/>
|
|
137
|
+
</dict>
|
|
138
|
+
</plist>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Load it:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
launchctl load ~/Library/LaunchAgents/com.mysqlgenius.tunnel.plist
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The tunnel will start automatically on login and restart if it drops.
|
|
148
|
+
|
|
149
|
+
## Future: Built-in SSH tunnel support
|
|
150
|
+
|
|
151
|
+
Built-in SSH tunnel support is planned for a future release. When available, you'll be able to configure the SSH connection directly in the MysqlGenius profile form without needing a separate terminal.
|
data/lib/mysql_genius/version.rb
CHANGED
data/mysql_genius.gemspec
CHANGED
|
@@ -30,6 +30,6 @@ Gem::Specification.new do |spec|
|
|
|
30
30
|
spec.require_paths = ["lib"]
|
|
31
31
|
|
|
32
32
|
spec.add_dependency("activerecord", ">= 6.0", "< 9")
|
|
33
|
-
spec.add_dependency("mysql_genius-core", "~> 0.
|
|
33
|
+
spec.add_dependency("mysql_genius-core", "~> 0.8.0")
|
|
34
34
|
spec.add_dependency("railties", ">= 6.0", "< 9")
|
|
35
35
|
end
|
data/ralph/prd.json
CHANGED
|
@@ -1,174 +1,117 @@
|
|
|
1
1
|
{
|
|
2
2
|
"project": "MysqlGenius",
|
|
3
|
-
"branchName": "ralph/
|
|
4
|
-
"description": "
|
|
3
|
+
"branchName": "ralph/ssh-tunnel-support",
|
|
4
|
+
"description": "Add built-in SSH tunnel support to the desktop sidecar so users can connect to MySQL databases behind firewalls without manually running ssh -L",
|
|
5
5
|
"userStories": [
|
|
6
6
|
{
|
|
7
7
|
"id": "US-001",
|
|
8
|
-
"title": "Add
|
|
9
|
-
"description": "As a developer, I need a
|
|
8
|
+
"title": "Add net-ssh dependency and SshTunnel class",
|
|
9
|
+
"description": "As a developer, I need a class that opens an SSH tunnel forwarding a local port to a remote MySQL server.",
|
|
10
10
|
"acceptanceCriteria": [
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"Create spec at gems/mysql_genius-
|
|
22
|
-
"
|
|
11
|
+
"Add net-ssh ~> 7.0 to gems/mysql_genius-desktop/mysql_genius-desktop.gemspec as runtime dependency",
|
|
12
|
+
"Run bundle install in gems/mysql_genius-desktop",
|
|
13
|
+
"Create gems/mysql_genius-desktop/lib/mysql_genius/desktop/ssh_tunnel.rb",
|
|
14
|
+
"SshTunnel.new(ssh_host:, ssh_port: 22, ssh_user:, ssh_key_path: nil, ssh_password: nil, remote_host:, remote_port: 3306, local_port: 0)",
|
|
15
|
+
"start method: opens SSH connection, sets up port forwarding, returns the allocated local port",
|
|
16
|
+
"stop method: closes the SSH connection and port forwarding",
|
|
17
|
+
"running? method: returns boolean",
|
|
18
|
+
"The tunnel runs in a background thread so it doesn't block the main thread",
|
|
19
|
+
"If ssh_key_path is provided, use key-based auth. If ssh_password, use password auth. If neither, use ssh-agent",
|
|
20
|
+
"Raises SshTunnel::ConnectionError with a clear message on failure",
|
|
21
|
+
"Create spec at gems/mysql_genius-desktop/spec/mysql_genius/desktop/ssh_tunnel_spec.rb",
|
|
22
|
+
"Desktop gem suite passes",
|
|
23
23
|
"Typecheck passes"
|
|
24
24
|
],
|
|
25
25
|
"priority": 1,
|
|
26
26
|
"passes": true,
|
|
27
|
-
"notes": "
|
|
27
|
+
"notes": "Use Net::SSH.start for the connection and Net::SSH::Gateway or manual forwarding via Net::SSH::Service::Forward. For local_port: 0, bind to an ephemeral port and read the assigned port from the socket. The background thread should keep the SSH connection alive with keepalive packets. Spec can mock Net::SSH.start since we can't open real SSH connections in CI."
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"id": "US-002",
|
|
31
|
-
"title": "Add
|
|
32
|
-
"description": "As a developer, I need
|
|
31
|
+
"title": "Add SSH fields to the Database profiles schema",
|
|
32
|
+
"description": "As a developer, I need the SQLite profiles table to store SSH tunnel configuration.",
|
|
33
33
|
"acceptanceCriteria": [
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"Computes deltas: delta_calls = current - previous, delta_total_time = current - previous",
|
|
41
|
-
"Records delta snapshot into the StatsHistory instance",
|
|
42
|
-
"Negative deltas (server restart) recorded as 0",
|
|
43
|
-
"If performance_schema is unavailable, logs warning and stops (no crash loop)",
|
|
44
|
-
"Add require to gems/mysql_genius-core/lib/mysql_genius/core.rb",
|
|
45
|
-
"Create spec at gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_collector_spec.rb",
|
|
46
|
-
"Core gem suite passes",
|
|
34
|
+
"Add columns to the profiles table in Database class: ssh_enabled BOOLEAN DEFAULT 0, ssh_host TEXT, ssh_port INTEGER DEFAULT 22, ssh_user TEXT, ssh_key_path TEXT, ssh_password TEXT, remote_host TEXT, remote_port INTEGER DEFAULT 3306",
|
|
35
|
+
"The schema migration must be backward-compatible: use ALTER TABLE ADD COLUMN IF NOT EXISTS (or check column existence before adding)",
|
|
36
|
+
"Database#add_profile and #update_profile accept the new SSH fields",
|
|
37
|
+
"Database#find_profile and #list_profiles return the new fields",
|
|
38
|
+
"Update database_spec.rb to test SSH fields round-trip",
|
|
39
|
+
"Desktop gem suite passes",
|
|
47
40
|
"Typecheck passes"
|
|
48
41
|
],
|
|
49
42
|
"priority": 2,
|
|
50
43
|
"passes": true,
|
|
51
|
-
"notes": "
|
|
44
|
+
"notes": "SQLite doesn't support ADD COLUMN IF NOT EXISTS directly. Use a rescue on the ALTER TABLE or check pragma_table_info for column existence before adding. The ssh_password field stores the SSH password (not the MySQL password). For Tauri app, the SSH password could later move to Keychain — for now store in SQLite alongside the MySQL password."
|
|
52
45
|
},
|
|
53
46
|
{
|
|
54
47
|
"id": "US-003",
|
|
55
|
-
"title": "
|
|
56
|
-
"description": "As a
|
|
48
|
+
"title": "Wire SSH tunnel into ActiveSession connection flow",
|
|
49
|
+
"description": "As a user, I want the sidecar to automatically open an SSH tunnel before connecting to MySQL when SSH is enabled on a profile.",
|
|
57
50
|
"acceptanceCriteria": [
|
|
58
|
-
"Modify
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
51
|
+
"Modify ActiveSession to check if the profile has SSH enabled",
|
|
52
|
+
"If ssh_enabled: open SshTunnel first, get the local forwarded port, then connect Trilogy to 127.0.0.1:local_port instead of the profile's host:port",
|
|
53
|
+
"ActiveSession stores a reference to the tunnel and closes it on session.close",
|
|
54
|
+
"On connection retry (Trilogy::ConnectionResetError), also restart the SSH tunnel",
|
|
55
|
+
"The Launcher reads SSH fields from the Database and passes them through the Config/profile to ActiveSession",
|
|
56
|
+
"If SSH connection fails, raise ConnectError with a clear message including the SSH host",
|
|
57
|
+
"Desktop gem suite passes",
|
|
64
58
|
"Typecheck passes"
|
|
65
59
|
],
|
|
66
60
|
"priority": 3,
|
|
67
61
|
"passes": true,
|
|
68
|
-
"notes": "
|
|
62
|
+
"notes": "The flow is: Launcher reads profile from DB -> if ssh_enabled, ActiveSession opens SshTunnel(ssh_host, ..., remote_host=profile.host, remote_port=profile.port) -> tunnel returns local_port -> Trilogy connects to 127.0.0.1:local_port. The profile's host/port become the REMOTE host/port (what the tunnel forwards to), not the direct Trilogy target. This is the key architectural insight."
|
|
69
63
|
},
|
|
70
64
|
{
|
|
71
65
|
"id": "US-004",
|
|
72
|
-
"title": "Add
|
|
73
|
-
"description": "As a user, I want to
|
|
66
|
+
"title": "Add SSH fields to the connections page UI",
|
|
67
|
+
"description": "As a user, I want to configure SSH tunnel settings when adding or editing a profile.",
|
|
74
68
|
"acceptanceCriteria": [
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"Page loads data via fetch to GET /api/query_history/:digest on page load",
|
|
86
|
-
"All JS is inline in the template (no external dependencies)",
|
|
69
|
+
"Add an 'Enable SSH Tunnel' checkbox to the profile form on /connections",
|
|
70
|
+
"When checked, show additional fields: SSH Host, SSH Port (default 22), SSH User, SSH Key Path, SSH Password",
|
|
71
|
+
"SSH Key Path field has a placeholder: ~/.ssh/id_rsa",
|
|
72
|
+
"SSH Password field uses type=password with the existing eye toggle",
|
|
73
|
+
"When unchecked, hide the SSH fields",
|
|
74
|
+
"The form sends ssh_enabled, ssh_host, ssh_port, ssh_user, ssh_key_path, ssh_password in the profile POST/PUT request",
|
|
75
|
+
"The existing Test Connection button tests through the tunnel when SSH is enabled",
|
|
76
|
+
"Edit mode populates the SSH fields from the saved profile",
|
|
77
|
+
"Dark mode works correctly on all new fields",
|
|
78
|
+
"Desktop gem suite passes",
|
|
87
79
|
"Typecheck passes"
|
|
88
80
|
],
|
|
89
81
|
"priority": 4,
|
|
90
82
|
"passes": true,
|
|
91
|
-
"notes": "The
|
|
83
|
+
"notes": "The checkbox should use a simple <input type='checkbox' id='mg-f-ssh-enabled'>. When it changes, toggle visibility of a div containing the SSH fields. The Test Connection flow: if SSH enabled, first open tunnel (POST /api/test_ssh_tunnel), get local port, then test MySQL connection against 127.0.0.1:local_port. Or simpler: the existing /api/test_connection route handles it server-side (check ssh_enabled in the payload, open tunnel internally, test MySQL, close tunnel)."
|
|
92
84
|
},
|
|
93
85
|
{
|
|
94
86
|
"id": "US-005",
|
|
95
|
-
"title": "
|
|
96
|
-
"description": "As a user, I want
|
|
87
|
+
"title": "Update profile switching to handle SSH tunnels",
|
|
88
|
+
"description": "As a user, I want SSH tunnels to be properly managed when switching between profiles.",
|
|
97
89
|
"acceptanceCriteria": [
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"Rails adapter suite passes: bundle exec rspec",
|
|
90
|
+
"SessionSwapper closes the old SSH tunnel (if any) when switching profiles",
|
|
91
|
+
"SessionSwapper opens a new SSH tunnel if the new profile has SSH enabled",
|
|
92
|
+
"The Launcher opens the initial SSH tunnel on boot if the default profile has SSH enabled",
|
|
93
|
+
"On sidecar shutdown (at_exit), both the MySQL session and SSH tunnel are closed",
|
|
94
|
+
"Desktop gem suite passes",
|
|
104
95
|
"Typecheck passes"
|
|
105
96
|
],
|
|
106
97
|
"priority": 5,
|
|
107
98
|
"passes": true,
|
|
108
|
-
"notes": "
|
|
99
|
+
"notes": "ActiveSession already has a close method that the SessionSwapper calls. If ActiveSession holds a reference to the tunnel, session.close should also close the tunnel. The new profile's tunnel opens as part of ActiveSession.new in the swapper."
|
|
109
100
|
},
|
|
110
101
|
{
|
|
111
102
|
"id": "US-006",
|
|
112
|
-
"title": "
|
|
113
|
-
"description": "As a
|
|
103
|
+
"title": "Full green sweep",
|
|
104
|
+
"description": "As a developer, I need all test suites passing.",
|
|
114
105
|
"acceptanceCriteria": [
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"Registers at_exit to stop the collector",
|
|
120
|
-
"Add two routes to config/routes.rb: get 'queries/:digest' and get 'api/query_history/:digest'",
|
|
121
|
-
"Add query_detail action to QueriesController that renders the shared template",
|
|
122
|
-
"Add query_history action that returns JSON with current stats + history",
|
|
123
|
-
"query_history looks up the digest in performance_schema and gets history from MysqlGenius.stats_history",
|
|
124
|
-
"Add request spec for GET /queries/:digest (returns 200)",
|
|
125
|
-
"Add request spec for GET /api/query_history/:digest (returns JSON with query and history keys)",
|
|
126
|
-
"Rails adapter suite passes",
|
|
106
|
+
"Rails adapter rspec passes (94+ examples)",
|
|
107
|
+
"Core gem rspec passes (248+ examples)",
|
|
108
|
+
"Desktop gem rspec passes (202+ examples)",
|
|
109
|
+
"All rubocops clean",
|
|
127
110
|
"Typecheck passes"
|
|
128
111
|
],
|
|
129
112
|
"priority": 6,
|
|
130
113
|
"passes": true,
|
|
131
|
-
"notes": "
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
"id": "US-007",
|
|
135
|
-
"title": "Wire stats collector and detail routes into desktop sidecar",
|
|
136
|
-
"description": "As a sidecar user, I want the stats collector running and the query detail page accessible.",
|
|
137
|
-
"acceptanceCriteria": [
|
|
138
|
-
"Update gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb to create StatsHistory + StatsCollector and set on App",
|
|
139
|
-
"Add App settings: :stats_history, :stats_collector",
|
|
140
|
-
"Collector uses connection_provider that checks out from the active session",
|
|
141
|
-
"Register at_exit to stop collector",
|
|
142
|
-
"Update SessionSwapper to stop old collector, clear history, start new collector on profile switch",
|
|
143
|
-
"Add GET /queries/:digest route to App (renders query_detail template through layout)",
|
|
144
|
-
"Add GET /api/query_history/:digest route to App (returns JSON)",
|
|
145
|
-
"Both new routes are under session-token auth",
|
|
146
|
-
"Add request spec for the two new routes",
|
|
147
|
-
"Desktop gem suite passes: (cd gems/mysql_genius-desktop && bundle exec rspec)",
|
|
148
|
-
"Typecheck passes"
|
|
149
|
-
],
|
|
150
|
-
"priority": 7,
|
|
151
|
-
"passes": true,
|
|
152
|
-
"notes": "The connection_provider for the sidecar wraps session.checkout. On profile switch, SessionSwapper should: App.settings.stats_collector&.stop, App.settings.stats_history&.clear, then after swapping the session create a new collector with the new session and start it. The render_query_detail method follows the same pattern as render_dashboard (Tilt through layout)."
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
"id": "US-008",
|
|
156
|
-
"title": "Full green sweep across all suites",
|
|
157
|
-
"description": "As a developer, I need all test suites and linters passing before the PR.",
|
|
158
|
-
"acceptanceCriteria": [
|
|
159
|
-
"Rails adapter rspec passes (78+ examples)",
|
|
160
|
-
"Core gem rspec passes (194+ examples plus new StatsHistory/StatsCollector/QueryStats specs)",
|
|
161
|
-
"Desktop gem rspec passes (154+ examples plus new route specs)",
|
|
162
|
-
"Rails adapter rubocop clean",
|
|
163
|
-
"Core gem rubocop clean",
|
|
164
|
-
"Desktop gem rubocop clean",
|
|
165
|
-
"git diff main -- .github/workflows/publish.yml is empty (untouched)",
|
|
166
|
-
"No version bumps committed (version bump is a separate release task)",
|
|
167
|
-
"Typecheck passes"
|
|
168
|
-
],
|
|
169
|
-
"priority": 8,
|
|
170
|
-
"passes": false,
|
|
171
|
-
"notes": "Run all six commands in order. If any fail, fix before proceeding. This is a verification step, not an implementation step."
|
|
114
|
+
"notes": ""
|
|
172
115
|
}
|
|
173
116
|
]
|
|
174
117
|
}
|
data/ralph/progress.txt
CHANGED
|
@@ -1,141 +1,119 @@
|
|
|
1
1
|
# Ralph Progress Log
|
|
2
|
-
Started:
|
|
2
|
+
Started: Mon Apr 13 06:43:11 CDT 2026
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
## Codebase Patterns
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- Routes that need a dynamic segment (digest) use base-path + value pattern in desktop PATHS hash; Rails uses named route helpers with keyword arg
|
|
24
|
-
|
|
6
|
+
- Desktop gem specs use RSpec mocking (allow/receive), NOT Mocha - Mocha is only for the Rails adapter
|
|
7
|
+
- Desktop gem uses `instance_double` for test doubles (VerifiedDoubles cop is disabled)
|
|
8
|
+
- Config objects are built in tests via `Config.allocate` + `instance_variable_set` pattern
|
|
9
|
+
- RSpec/MultipleMemoizedHelpers max is 5 - inline simple constants to stay under the limit
|
|
10
|
+
- Net::SSH is mocked at the class level: `allow(Net::SSH).to(receive(:start).and_return(ssh_session))`
|
|
11
|
+
- SshTunnel event loop runs in a background Thread - test thread existence, not `session.loop` (race condition)
|
|
12
|
+
- SQLite migration: use `PRAGMA table_info(table)` to check column existence before `ALTER TABLE ADD COLUMN`
|
|
13
|
+
- Database profiles use string keys throughout (from `results_as_hash = true`); `ssh_enabled` is INTEGER 0/1
|
|
14
|
+
- ActiveSession.open_adapter_for accepts `tunnel_port:` kwarg — pass it from `session.tunnel_port` in conn_proc lambdas
|
|
15
|
+
- MysqlConfig#ssh_enabled? coerces INTEGER/boolean/string to boolean (handles DB and YAML sources)
|
|
16
|
+
- When SSH enabled, profile.host/port become SshTunnel remote_host/remote_port; Trilogy connects to 127.0.0.1:tunnel_port
|
|
17
|
+
- Instance doubles of ActiveSession must include `tunnel_port: nil` in specs that trigger conn_proc creation
|
|
18
|
+
- Launcher `import_profiles` must include all profile fields — keep in sync when adding new columns to Database schema
|
|
19
|
+
- Use `stub_boot(session:, db_name:)` helper in launcher_spec.rb for full boot mock setup
|
|
20
|
+
- Eye toggle: `.mg-eye-toggle` button with `data-target="input-id"` toggles password/text input type
|
|
21
|
+
- input[type="password"] must be included in both light-mode and dark-mode CSS selectors alongside text/number
|
|
22
|
+
- Layout/MultilineHashKeyLineBreaks cop: each key on its own line in multiline hashes (use rubocop -A to auto-fix)
|
|
25
23
|
---
|
|
26
24
|
|
|
27
|
-
## 2026-04-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- RuboCop: clean
|
|
25
|
+
## 2026-04-13 - US-001
|
|
26
|
+
- What was implemented: SshTunnel class with start/stop/running? methods, net-ssh dependency, 22 specs
|
|
27
|
+
- Files changed:
|
|
28
|
+
- gems/mysql_genius-desktop/mysql_genius-desktop.gemspec (added net-ssh ~> 7.0)
|
|
29
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop.rb (added require for ssh_tunnel)
|
|
30
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/ssh_tunnel.rb (new - SshTunnel class)
|
|
31
|
+
- gems/mysql_genius-desktop/spec/mysql_genius/desktop/ssh_tunnel_spec.rb (new - 22 examples)
|
|
35
32
|
- **Learnings for future iterations:**
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
33
|
+
- SshTunnel uses Net::SSH::Service::Forward for port forwarding, NOT Net::SSH::Gateway
|
|
34
|
+
- Ephemeral port allocation: bind TCPServer to port 0, read assigned port, close immediately
|
|
35
|
+
- Auth priority: ssh_key_path > ssh_password > ssh-agent (no auth options = agent fallback)
|
|
36
|
+
- The event loop thread runs `@session.loop(0.5) { @running }` - the 0.5 is the poll interval in seconds
|
|
37
|
+
- ConnectionError wraps all SSH failures with descriptive messages including host:port
|
|
39
38
|
---
|
|
40
39
|
|
|
41
|
-
## 2026-04-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
- connection_provider callable pattern: adapters supply their own connection strategy
|
|
47
|
-
- Added require to `gems/mysql_genius-core/lib/mysql_genius/core.rb`
|
|
48
|
-
- Created spec at `gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_collector_spec.rb` (12 examples)
|
|
49
|
-
- Files changed: stats_collector.rb (new), core.rb (require added), stats_collector_spec.rb (new)
|
|
50
|
-
- Core gem suite: 215 examples, 0 failures
|
|
51
|
-
- RuboCop: clean
|
|
40
|
+
## 2026-04-13 - US-002
|
|
41
|
+
- What was implemented: Added SSH tunnel columns to the profiles SQLite table with backward-compatible migration
|
|
42
|
+
- Files changed:
|
|
43
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/database.rb (schema + migration + add_profile + update_profile)
|
|
44
|
+
- gems/mysql_genius-desktop/spec/mysql_genius/desktop/database_spec.rb (6 new SSH field specs + legacy migration test)
|
|
52
45
|
- **Learnings for future iterations:**
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
46
|
+
- SQLite doesn't support `ADD COLUMN IF NOT EXISTS` — use `PRAGMA table_info(table)` to check column existence before ALTER TABLE
|
|
47
|
+
- The `migrate_ssh_columns` method runs after `create_schema` so it handles both fresh and legacy databases
|
|
48
|
+
- `ssh_enabled` is stored as INTEGER (0/1) since SQLite has no native boolean type — use `.to_i` when writing
|
|
49
|
+
- `find_profile` and `list_profiles` use `SELECT *` so they automatically return new columns without code changes
|
|
50
|
+
- RSpec/ExampleLength max is 25 lines — extract DB setup into helper methods for longer integration tests
|
|
51
|
+
- SSH fields use string keys throughout (matching SQLite3 `results_as_hash = true` output)
|
|
57
52
|
---
|
|
58
53
|
|
|
59
|
-
## 2026-04-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
54
|
+
## 2026-04-13 - US-003
|
|
55
|
+
- What was implemented: Wired SSH tunnel into ActiveSession connection flow with tunnel lifecycle management
|
|
56
|
+
- Files changed:
|
|
57
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/config/mysql_config.rb (added SSH fields + ssh_enabled? method)
|
|
58
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/active_session.rb (tunnel start/stop/restart, open_adapter_for tunnel_port kwarg)
|
|
59
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/session_swapper.rb (SSH fields in mysql_hash_from_profile, tunnel_port in conn_proc)
|
|
60
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb (tunnel_port in conn_proc)
|
|
61
|
+
- gems/mysql_genius-desktop/spec/mysql_genius/desktop/active_session_spec.rb (7 new SSH tunnel specs)
|
|
62
|
+
- gems/mysql_genius-desktop/spec/mysql_genius/desktop/launcher_spec.rb (added tunnel_port to instance_double)
|
|
63
|
+
- gems/mysql_genius-desktop/spec/mysql_genius/desktop/session_swapper_spec.rb (added tunnel_port to instance_double)
|
|
64
|
+
- gems/mysql_genius-desktop/spec/requests/profiles_api_spec.rb (added tunnel_port to instance_double)
|
|
66
65
|
- **Learnings for future iterations:**
|
|
67
|
-
-
|
|
68
|
-
-
|
|
66
|
+
- ActiveSession manages SSH tunnel lifecycle: start in initialize, stop in close, restart on retry
|
|
67
|
+
- open_adapter_for(config, tunnel_port: nil) — when tunnel_port is set, connect to 127.0.0.1:tunnel_port instead of config host/port
|
|
68
|
+
- Profile host/port become SshTunnel remote_host/remote_port — this is the key architectural pattern
|
|
69
|
+
- SshTunnel::ConnectionError is caught and re-raised as ConnectError with SSH host context
|
|
70
|
+
- Tunnel restart on Trilogy::ConnectionResetError creates a fresh SshTunnel object (stop old, start new)
|
|
71
|
+
- StatsCollector conn_proc captures tunnel_port via closure so it routes through the existing tunnel
|
|
72
|
+
- Any instance_double(ActiveSession) in specs that trigger conn_proc must stub tunnel_port
|
|
69
73
|
---
|
|
70
74
|
|
|
71
|
-
## 2026-04-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
- Data loaded via fetch to GET /api/query_history/:digest, EXPLAIN via POST /explain
|
|
79
|
-
- All JS is inline (no external dependencies), replicates helper functions from dashboard (highlightSql, formatDuration, ajax, etc.)
|
|
80
|
-
- Files changed: `gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/query_detail.html.erb` (new)
|
|
81
|
-
- Core gem suite: 215 examples, 0 failures
|
|
82
|
-
- Rails adapter suite: 78 examples, 0 failures
|
|
83
|
-
- RuboCop: clean
|
|
75
|
+
## 2026-04-13 - US-004
|
|
76
|
+
- What was implemented: Added SSH tunnel UI fields to the connections page profile form
|
|
77
|
+
- Files changed:
|
|
78
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/connections.html.erb (SSH checkbox, conditional fields, eye toggle, JS updates)
|
|
79
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb (format_profile, profile_attrs_from_request, update_attrs_from_request, mysql_hash_from_profile, test_connection route with SSH tunnel support)
|
|
80
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/layout.html.erb (input[type="password"] in base + dark mode CSS)
|
|
81
|
+
- gems/mysql_genius-desktop/spec/requests/profiles_api_spec.rb (3 new specs: SSH create, SSH update, SSH test_connection)
|
|
84
82
|
- **Learnings for future iterations:**
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
83
|
+
- Password field was type="text" — changed to type="password" with eye toggle using data-target attribute
|
|
84
|
+
- Eye toggle pattern: button with class `mg-eye-toggle` and `data-target="input-id"`, toggles input.type between password/text
|
|
85
|
+
- SSH fields are conditionally shown via `toggleSshFields()` — checkbox change listener toggles `.mg-hidden` on `#mg-ssh-fields` div
|
|
86
|
+
- `getFormData()` only includes SSH fields when checkbox is checked (sends ssh_enabled: 0 when unchecked)
|
|
87
|
+
- `fillForm()` must call `toggleSshFields()` after setting checkbox state to show/hide fields on edit
|
|
88
|
+
- test_connection route opens/closes SshTunnel inline when ssh_enabled — tunnel.stop in both success and rescue paths
|
|
89
|
+
- input[type="password"] was not in the original CSS selectors — must be added to both light and dark mode rules
|
|
90
|
+
- Layout/MultilineHashKeyLineBreaks cop requires each hash key on its own line in multiline hashes
|
|
90
91
|
---
|
|
91
92
|
|
|
92
|
-
## 2026-04-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
93
|
+
## 2026-04-13 - US-005
|
|
94
|
+
- What was implemented: SSH tunnel lifecycle management during profile switching, boot, and shutdown
|
|
95
|
+
- Bug fix: `import_profiles` in Launcher was not importing SSH fields from YAML config to SQLite database
|
|
96
|
+
- Added SessionSwapper specs: SSH fields pass-through, old session/tunnel closure, tunnel_port capture for conn_proc
|
|
97
|
+
- Added Launcher specs: SSH field import, tunnel_port capture for stats collector, shutdown registration
|
|
98
|
+
- Extracted `stub_boot` helper in launcher_spec.rb to keep tests under RSpec/ExampleLength limit
|
|
99
|
+
- Files changed:
|
|
100
|
+
- gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb (import_profiles now includes SSH fields)
|
|
101
|
+
- gems/mysql_genius-desktop/spec/mysql_genius/desktop/session_swapper_spec.rb (3 new SSH switching specs)
|
|
102
|
+
- gems/mysql_genius-desktop/spec/mysql_genius/desktop/launcher_spec.rb (4 new specs + stub_boot helper)
|
|
99
103
|
- **Learnings for future iterations:**
|
|
100
|
-
-
|
|
101
|
-
-
|
|
104
|
+
- Most SSH tunnel lifecycle was already implemented in US-003 via ActiveSession — US-005 was about verifying integration points
|
|
105
|
+
- import_profiles must be kept in sync with any new profile fields — it's easy to miss when adding columns
|
|
106
|
+
- Launcher `register_shutdown` uses at_exit — test by capturing the arguments passed to it, not by invoking at_exit
|
|
107
|
+
- Use `stub_boot` helper in launcher_spec.rb for tests that need the full boot mock setup
|
|
108
|
+
- StatsCollector conn_proc captures tunnel_port via closure — test by capturing the proc from StatsCollector.new kwargs
|
|
102
109
|
---
|
|
103
110
|
|
|
104
|
-
## 2026-04-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
- Added request specs in `spec/requests/mysql_genius/query_detail_spec.rb` (4 examples)
|
|
113
|
-
- Files changed: configuration.rb, lib/mysql_genius.rb, engine.rb, config/routes.rb, queries_controller.rb, shared_view_helpers.rb, query_detail_spec.rb (new)
|
|
114
|
-
- Rails adapter suite: 82 examples, 0 failures
|
|
115
|
-
- RuboCop: clean
|
|
116
|
-
- **Learnings for future iterations:**
|
|
117
|
-
- `ActiveRecord::Result` `instance_double` — use `to_a:` returning hash arrays; use `result.to_a.first` not `result.first` (which needs Enumerable)
|
|
118
|
-
- Engine's `config.after_initialize` creates the collector in test env too, causing a benign "StatsCollector stopped" warning in tests — this is expected
|
|
119
|
-
- `path_for` needs to know about digest routes: use an allowlist `%i[query_detail query_history]` and conditionally pass `digest:` param
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## 2026-04-11 - US-007
|
|
123
|
-
- Updated `Launcher#call` to create StatsHistory + StatsCollector, set on App, register collector in at_exit
|
|
124
|
-
- Added `set :stats_history, nil` and `set :stats_collector, nil` to App settings
|
|
125
|
-
- Updated App `path_for` helper to append `@digest` for `:query_detail` and `:query_history` routes
|
|
126
|
-
- Added `GET /queries/:digest` and `GET /api/query_history/:digest` routes to App (under session-token auth)
|
|
127
|
-
- Added `render_query_detail`, `fetch_query_history_current`, `fetch_query_history_series` private methods
|
|
128
|
-
- Updated SessionSwapper to stop old collector, clear history, create new collector after session swap
|
|
129
|
-
- Added `:root`, `:query_detail`, `:query_history` to desktop PATHS hash
|
|
130
|
-
- Updated `rack_helper.rb` to reset stats settings and stub `StatsCollector.new` globally to prevent background threads in specs
|
|
131
|
-
- Updated `session_swapper_spec.rb` to mock StatsCollector/StatsHistory and handle new `set` keys
|
|
132
|
-
- Added `spec/requests/query_detail_spec.rb` for desktop (4 examples)
|
|
133
|
-
- Files changed: app.rb, paths.rb, launcher.rb, session_swapper.rb, rack_helper.rb, session_swapper_spec.rb, query_detail_spec.rb (new)
|
|
134
|
-
- Desktop suite: 158 examples, 0 failures (was 154)
|
|
135
|
-
- RuboCop: clean
|
|
136
|
-
- **Learnings for future iterations:**
|
|
137
|
-
- Desktop gem has NO ActiveSupport — never use `.squish`; use explicit string concatenation
|
|
138
|
-
- `Core::Result#to_a` returns raw row arrays (not hashes) — use `to_hashes` for hash access
|
|
139
|
-
- When new `SessionSwapper#switch_to` logic creates real background objects, existing integration tests can fail — mock the constructor in rack_helper to prevent threads
|
|
140
|
-
- Add `# rubocop:disable RSpec/InstanceVariable` at file top + `enable` at bottom when using `@fake_adapter` directly in specs
|
|
111
|
+
## 2026-04-13 - US-006
|
|
112
|
+
- What was verified: Full green sweep across all three gems
|
|
113
|
+
- Results:
|
|
114
|
+
- Rails adapter: 94 examples, 0 failures
|
|
115
|
+
- Core gem: 248 examples, 0 failures
|
|
116
|
+
- Desktop gem: 246 examples, 0 failures
|
|
117
|
+
- All rubocops clean (166 + 76 + 50 files inspected, no offenses)
|
|
118
|
+
- No code changes required — all suites passed after US-005 commit
|
|
141
119
|
---
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mysql_genius
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antarr Byrd
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -36,14 +36,14 @@ dependencies:
|
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.8.0
|
|
40
40
|
type: :runtime
|
|
41
41
|
prerelease: false
|
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.
|
|
46
|
+
version: 0.8.0
|
|
47
47
|
- !ruby/object:Gem::Dependency
|
|
48
48
|
name: railties
|
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -96,6 +96,9 @@ files:
|
|
|
96
96
|
- bin/console
|
|
97
97
|
- bin/setup
|
|
98
98
|
- config/routes.rb
|
|
99
|
+
- docs/guides/ai-features.md
|
|
100
|
+
- docs/guides/getting-started-rails.md
|
|
101
|
+
- docs/guides/ssh-tunnel-connections.md
|
|
99
102
|
- docs/screenshots/ai_tools.png
|
|
100
103
|
- docs/screenshots/dashboard.png
|
|
101
104
|
- docs/screenshots/duplicate_indexes.png
|