mysql_genius 0.3.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cac820a4fce8c58c24e922c9d1794b5e643c83c36c087dbf19ec5f34b41dd7ff
4
- data.tar.gz: 382817839d37e85caa3265ef4bedff6073d1f417d5d33c3295881e1b8d745044
3
+ metadata.gz: e54ecfd5bfe23f9c1e97caee0dd7d76499375488a37c2bbf2f58951b2b37dd85
4
+ data.tar.gz: d0fd0253422d4fab85dbc40497ad60d4c8ed4ed699de435a8210fbed1981734d
5
5
  SHA512:
6
- metadata.gz: 8676b5b731e2df5df4392a46533c395c72dde5b00323f965f23429f8598b68f64d40a974ee31dcfa52bc6ab5810ad6f2fc2f8ad7f07a69dea75df63d6b128564
7
- data.tar.gz: fe0708d066c4aa9779fc8eaaf849c03ef0943031c4871201be840a3f34bebf481b3f89e02f477700e2c32c4611a0fead1b2cd8f462cd17a928a4f66a8c38435b
6
+ metadata.gz: eb7a5cf0dbda1f1c0b5df59b543735d3a83a902219d54e354f06434a85101d858a7c80407ecf4e492f4bdceaf9b26d2e8d854af3396cad6e4cf6ef503781f500
7
+ data.tar.gz: a910f441a3a234e91c95b338ae74427fd19c68184ad8b5a7b4019adc4f6220e9440381f646c0f2daeaacfec7cb73a3204c1aa1efc0e4dc960ce5b7633a8b46b0
@@ -0,0 +1,32 @@
1
+ name: Publish to RubyGems
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: 3.3
16
+ bundler-cache: true
17
+ - run: bundle exec rspec
18
+
19
+ publish:
20
+ needs: test
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: ruby/setup-ruby@v1
25
+ with:
26
+ ruby-version: 3.3
27
+ - name: Build gem
28
+ run: gem build mysql_genius.gemspec
29
+ - name: Publish to RubyGems
30
+ run: gem push mysql_genius-*.gem
31
+ env:
32
+ GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
data/.gitignore CHANGED
@@ -12,3 +12,4 @@
12
12
  /vendor
13
13
  CLAUDE.md
14
14
  Gemfile.lock
15
+ *.gem
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Added
6
+ - **Sortable columns** -- click any column header to sort ascending/descending on all data tables
7
+ - **Automated RubyGems publishing** -- GitHub Actions workflow publishes gem on tag push
8
+
9
+ ### Fixed
10
+ - **Query stats noise** -- MySQLGenius internal queries (information_schema, performance_schema, SHOW, etc.) are now excluded from the Query Stats tab
11
+
3
12
  ## 0.3.0
4
13
 
5
14
  ### Improved
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MySQLGenius
2
2
 
3
- A AI powered MySQL dashboard and build to help you optimize your database for maximum performance, inspired by [PgHero](https://github.com/ankane/pghero).
3
+ An AI-powered MySQL dashboard for Rails to help you optimize your database for maximum performance, inspired by [PgHero](https://github.com/ankane/pghero).
4
4
 
5
5
  ## Screenshots
6
6
 
@@ -10,13 +10,9 @@ At-a-glance server health, top slow queries, most expensive queries, and index a
10
10
 
11
11
  ![Dashboard](docs/screenshots/dashboard.png)
12
12
 
13
- ### Slow Queries
14
-
15
- SELECT queries exceeding the configured threshold, captured via ActiveSupport notifications and Redis.
16
-
17
13
  ### Query Stats
18
14
 
19
- Top queries from `performance_schema` sorted by total time, with call counts, avg/max time, and rows examined.
15
+ Top queries from `performance_schema` sorted by total time, with SQL syntax highlighting and color-coded durations.
20
16
 
21
17
  ![Query Stats](docs/screenshots/query_stats.png)
22
18
 
@@ -26,9 +22,9 @@ Server health: version, connections, InnoDB buffer pool, and query activity with
26
22
 
27
23
  ![Server](docs/screenshots/server.png)
28
24
 
29
- ### Table Sizes
25
+ ### Tables
30
26
 
31
- View row counts, data size, index size, fragmentation, and a visual size chart for every table.
27
+ Row counts, data size, index size, engine, fragmentation, and optimize suggestions for every table.
32
28
 
33
29
  ![Table Sizes](docs/screenshots/table_sizes.png)
34
30
 
@@ -46,232 +42,69 @@ Build queries visually or write raw SQL. Optional AI assistant generates queries
46
42
 
47
43
  ### AI Tools
48
44
 
49
- Schema review that finds anti-patterns -- missing primary keys, nullable foreign keys, inappropriate column types, and more.
45
+ Schema review, query optimization, index advisor, anomaly detection, root cause analysis, and migration risk assessment.
50
46
 
51
47
  ![AI Tools](docs/screenshots/ai_tools.png)
52
48
 
53
49
  ## Features
54
50
 
55
- - **Visual Query Builder** -- point-and-click query construction with column selection, type-aware filters, and ordering
56
- - **Safe SQL Execution** -- read-only enforcement, blocked tables, masked sensitive columns, row limits, query timeouts
51
+ - **Dashboard** -- server health, slow queries, expensive queries, index alerts at a glance
52
+ - **Query Explorer** -- visual builder + raw SQL editor with AI assistant
53
+ - **SQL Syntax Highlighting** -- dark-themed code blocks with color-coded keywords, functions, strings
54
+ - **Safe SQL Execution** -- read-only enforcement, blocked tables, masked columns, row limits, timeouts
57
55
  - **EXPLAIN Analysis** -- run EXPLAIN on any query and view the execution plan
58
- - **AI Query Suggestions** -- describe what you want in plain English, get SQL back (optional, any OpenAI-compatible API)
59
- - **AI Query Optimization** -- get actionable optimization suggestions from EXPLAIN output (optional)
60
- - **Slow Query Monitoring** -- captures slow SELECT queries via ActiveSupport notifications and Redis
61
- - **Duplicate Index Detection** -- finds redundant indexes whose columns are a left-prefix of another index
62
- - **Table Size Dashboard** -- view row counts, data size, index size, and fragmentation for all tables
63
- - **Audit Logging** -- logs all query executions, rejections, and errors
56
+ - **9 AI Tools** -- suggestions, optimization, schema review, query rewrite, index advisor, anomaly detection, root cause analysis, migration risk ([details](https://github.com/antarr/mysql_genius/wiki/AI-Features))
57
+ - **Slow Query Monitoring** -- captures slow queries via ActiveSupport notifications and Redis ([details](https://github.com/antarr/mysql_genius/wiki/Slow-Query-Monitoring))
58
+ - **Index Analysis** -- duplicate index detection, unused index detection with DROP statements
59
+ - **Dark Theme** -- auto-detects system preference with manual toggle ([details](https://github.com/antarr/mysql_genius/wiki/Dark-Theme))
64
60
  - **MariaDB Support** -- automatically detects MariaDB and uses appropriate timeout syntax
65
- - **Self-contained UI** -- no external CSS/JS dependencies, works with any Rails layout
66
- - **Zero jQuery** -- pure vanilla JavaScript frontend
67
-
68
- ## Requirements
69
-
70
- - Rails 5.2+
71
- - Ruby 2.6+
72
- - MySQL or MariaDB
73
- - Redis (optional, for slow query monitoring)
61
+ - **Self-contained UI** -- no external CSS/JS dependencies, no jQuery, works with any Rails layout
74
62
 
75
- ## Installation
76
-
77
- Add to your Gemfile:
63
+ ## Quick Start
78
64
 
79
65
  ```ruby
66
+ # Gemfile
80
67
  gem "mysql_genius"
81
68
  ```
82
69
 
83
- Or from GitHub:
84
-
85
- ```ruby
86
- gem "mysql_genius", github: "antarr/mysql_genius"
87
- ```
88
-
89
- Then run:
90
-
91
70
  ```bash
92
71
  bundle install
72
+ rails generate mysql_genius:install
93
73
  ```
94
74
 
95
- ## Setup
75
+ Visit `/mysql_genius` in your browser.
96
76
 
97
- ### 1. Mount the engine
77
+ For detailed setup, see the [Installation guide](https://github.com/antarr/mysql_genius/wiki/Installation).
98
78
 
99
- In `config/routes.rb`:
100
-
101
- ```ruby
102
- Rails.application.routes.draw do
103
- mount MysqlGenius::Engine, at: "/mysql_genius"
104
- end
105
- ```
106
-
107
- To restrict access at the route level:
108
-
109
- ```ruby
110
- # Using a session constraint
111
- constraints ->(req) { req.session[:admin] } do
112
- mount MysqlGenius::Engine, at: "/mysql_genius"
113
- end
114
-
115
- # Or using Devise
116
- authenticate :user, ->(u) { u.admin? } do
117
- mount MysqlGenius::Engine, at: "/mysql_genius"
118
- end
119
- ```
120
-
121
- ### 2. Configure
122
-
123
- Create `config/initializers/mysql_genius.rb`:
79
+ ## Configuration
124
80
 
125
81
  ```ruby
126
82
  MysqlGenius.configure do |config|
127
- # --- Authentication ---
128
- # Lambda that receives the controller instance. Return true to allow access.
129
- # Default: allows everyone. Use route constraints for most cases.
130
- config.authenticate = ->(controller) { true }
131
-
132
- # To use current_user or other app helpers, inherit from ApplicationController:
133
- # config.base_controller = "ApplicationController"
134
- # config.authenticate = ->(controller) { controller.current_user&.admin? }
135
-
136
- # --- Tables ---
137
- # Tables featured at the top of the visual builder dropdown (optional)
138
- config.featured_tables = %w[users posts comments]
139
-
140
- # Tables blocked from querying (defaults: sessions, schema_migrations, ar_internal_metadata)
83
+ config.base_controller = "ApplicationController"
84
+ config.authenticate = ->(controller) { controller.current_user&.admin? }
141
85
  config.blocked_tables += %w[oauth_tokens api_keys]
142
-
143
- # Column patterns to redact with [REDACTED] in results (case-insensitive substring match)
144
- config.masked_column_patterns = %w[password secret digest token ssn]
145
-
146
- # Default columns checked in the visual builder per table (optional).
147
- # When empty for a table, all columns are checked by default.
148
- config.default_columns = {
149
- "users" => %w[id name email created_at],
150
- "posts" => %w[id title user_id published_at]
151
- }
152
-
153
- # --- Query Safety ---
154
- config.max_row_limit = 1000 # Hard cap on rows returned
155
- config.default_row_limit = 25 # Default when no limit specified
156
- config.query_timeout_ms = 30_000 # 30 second timeout (uses MariaDB or MySQL hints)
157
-
158
- # --- Slow Query Monitoring ---
159
- # Requires Redis. Set to nil to disable.
160
- config.redis_url = ENV["REDIS_URL"].presence || "redis://127.0.0.1:6379/0"
161
- config.slow_query_threshold_ms = 250
162
-
163
- # --- Audit Logging ---
164
- # Set to nil to disable. Logs query executions, rejections, and errors.
165
- config.audit_logger = Logger.new(Rails.root.join("log", "mysql_genius.log"))
166
86
  end
167
87
  ```
168
88
 
169
- ### 3. AI Features (optional)
89
+ For full configuration options, see the [Configuration guide](https://github.com/antarr/mysql_genius/wiki/Configuration).
170
90
 
171
- MySQLGenius supports AI-powered query suggestions and optimization via any OpenAI-compatible API, including OpenAI, Azure OpenAI, Ollama Cloud, and local Ollama instances.
91
+ ## AI Features (optional)
92
+
93
+ Works with OpenAI, Azure OpenAI, Ollama Cloud, local Ollama, or any OpenAI-compatible API.
172
94
 
173
95
  ```ruby
174
96
  MysqlGenius.configure do |config|
175
- # --- Option A: OpenAI ---
176
97
  config.ai_endpoint = "https://api.openai.com/v1/chat/completions"
177
98
  config.ai_api_key = ENV["OPENAI_API_KEY"]
178
99
  config.ai_model = "gpt-4o"
179
100
  config.ai_auth_style = :bearer
180
-
181
- # --- Option B: Azure OpenAI ---
182
- config.ai_endpoint = ENV["AZURE_OPENAI_ENDPOINT"] # Your deployment URL
183
- config.ai_api_key = ENV["AZURE_OPENAI_API_KEY"]
184
- config.ai_auth_style = :api_key # Default, uses api-key header
185
-
186
- # --- Option C: Ollama Cloud ---
187
- config.ai_endpoint = "https://api.ollama.com/v1/chat/completions"
188
- config.ai_api_key = ENV["OLLAMA_API_KEY"]
189
- config.ai_model = "gemma3:27b"
190
- config.ai_auth_style = :bearer
191
-
192
- # --- Option D: Local Ollama ---
193
- config.ai_endpoint = "http://localhost:11434/v1/chat/completions"
194
- config.ai_api_key = "ollama" # Any non-empty string
195
- config.ai_model = "llama3"
196
- config.ai_auth_style = :bearer
197
-
198
- # --- Option E: Custom client ---
199
- # Any callable that accepts messages: and temperature: kwargs
200
- # and returns an OpenAI-compatible response hash.
201
- config.ai_client = ->(messages:, temperature:) {
202
- MyAiService.chat(messages, temperature: temperature)
203
- }
204
-
205
- # --- Domain Context ---
206
- # Helps the AI understand your schema and generate better queries.
207
- config.ai_system_context = <<~CONTEXT
208
- This is an e-commerce database.
209
- - `users` stores customer accounts. Primary key is `id`.
210
- - `orders` tracks purchases. Linked to users via `user_id`.
211
- - `products` contains the product catalog.
212
- - Soft-deleted records have `deleted_at IS NOT NULL`.
213
- CONTEXT
214
101
  end
215
102
  ```
216
103
 
217
- | Option | `ai_auth_style` | `ai_model` | Notes |
218
- |--------|-----------------|------------|-------|
219
- | OpenAI | `:bearer` | Required (e.g. `gpt-4o`) | |
220
- | Azure OpenAI | `:api_key` (default) | Optional (uses deployment default) | |
221
- | Ollama Cloud | `:bearer` | Required (e.g. `gemma3:27b`) | Follows redirects automatically |
222
- | Local Ollama | `:bearer` | Required | No API key validation |
223
- | Custom client | N/A | N/A | You handle everything |
224
-
225
- When AI is not configured, the AI Assistant panel and optimization buttons are hidden automatically.
226
-
227
- ## Usage
228
-
229
- Visit `/mysql_genius` in your browser. The dashboard loads automatically with an overview of your database health.
230
-
231
- ### Dashboard
232
-
233
- The default landing page shows server health cards, top 5 slow queries, top 5 most expensive queries (from performance_schema), and index alert badges for duplicate and unused indexes. Each section links to its detailed tab.
234
-
235
- ### Query Explorer
236
-
237
- Combines the visual query builder and raw SQL editor in one tab. Toggle between Visual mode (point-and-click with column selection, filters, and ordering) and SQL mode (raw SQL with optional AI assistant). Generated SQL syncs between modes.
238
-
239
- ### Monitoring Tabs
240
-
241
- - **Slow Queries** -- slow SELECT queries captured from your application in real time, with Explain and Optimize actions
242
- - **Query Stats** -- top queries from `performance_schema` sorted by total time, avg time, calls, or rows examined
243
- - **Server** -- connections, InnoDB buffer pool, query activity, with AI-powered diagnostics
244
- - **Table Sizes** -- row counts, data size, index size, fragmentation for all tables
245
- - **Unused Indexes** -- indexes with zero reads since server restart
246
- - **Duplicate Indexes** -- redundant indexes with ready-to-run DROP statements
247
-
248
- ## Configuration Reference
249
-
250
- | Option | Type | Default | Description |
251
- |--------|------|---------|-------------|
252
- | `authenticate` | Proc | `->(_) { true }` | Authorization check |
253
- | `base_controller` | String | `"ActionController::Base"` | Parent controller class |
254
- | `featured_tables` | Array | `[]` | Tables shown in Featured group |
255
- | `blocked_tables` | Array | `[sessions, ...]` | Tables that cannot be queried |
256
- | `masked_column_patterns` | Array | `[password, secret, ...]` | Column patterns to redact |
257
- | `default_columns` | Hash | `{}` | Default checked columns per table |
258
- | `max_row_limit` | Integer | `1000` | Maximum rows returned |
259
- | `default_row_limit` | Integer | `25` | Default row limit |
260
- | `query_timeout_ms` | Integer | `30000` | Query timeout in ms |
261
- | `redis_url` | String | `nil` | Redis URL for slow query monitoring |
262
- | `slow_query_threshold_ms` | Integer | `250` | Slow query threshold |
263
- | `audit_logger` | Logger | `nil` | Logger for query audit trail |
264
- | `ai_endpoint` | String | `nil` | AI API endpoint URL |
265
- | `ai_api_key` | String | `nil` | AI API key |
266
- | `ai_model` | String | `nil` | AI model name |
267
- | `ai_auth_style` | Symbol | `:api_key` | `:bearer` or `:api_key` |
268
- | `ai_client` | Proc | `nil` | Custom AI client callable |
269
- | `ai_system_context` | String | `nil` | Domain context for AI prompts |
104
+ For all provider examples, see the [AI Features guide](https://github.com/antarr/mysql_genius/wiki/AI-Features).
270
105
 
271
106
  ## Compatibility
272
107
 
273
- Tested against:
274
-
275
108
  | Rails | Ruby |
276
109
  |-------|------|
277
110
  | 5.2 | 2.7, 3.0 |
@@ -283,6 +116,10 @@ Tested against:
283
116
  | 8.0 | 3.2, 3.3, 3.4 |
284
117
  | 8.1 | 3.2, 3.3, 3.4 |
285
118
 
119
+ ## Documentation
120
+
121
+ Full documentation is available on the [Wiki](https://github.com/antarr/mysql_genius/wiki).
122
+
286
123
  ## Development
287
124
 
288
125
  ```bash
@@ -292,12 +129,6 @@ bin/setup
292
129
  bundle exec rspec
293
130
  ```
294
131
 
295
- To test against a specific Rails version:
296
-
297
- ```bash
298
- RAILS_VERSION=6.1 bundle update && bundle exec rspec
299
- ```
300
-
301
132
  ## Contributing
302
133
 
303
134
  Bug reports and pull requests are welcome on GitHub at https://github.com/antarr/mysql_genius.
@@ -128,6 +128,14 @@ module MysqlGenius
128
128
  WHERE SCHEMA_NAME = #{connection.quote(connection.current_database)}
129
129
  AND DIGEST_TEXT IS NOT NULL
130
130
  AND DIGEST_TEXT NOT LIKE 'EXPLAIN%'
131
+ AND DIGEST_TEXT NOT LIKE '%`information_schema`%'
132
+ AND DIGEST_TEXT NOT LIKE '%`performance_schema`%'
133
+ AND DIGEST_TEXT NOT LIKE '%information_schema.%'
134
+ AND DIGEST_TEXT NOT LIKE '%performance_schema.%'
135
+ AND DIGEST_TEXT NOT LIKE 'SHOW %'
136
+ AND DIGEST_TEXT NOT LIKE 'SET STATEMENT %'
137
+ AND DIGEST_TEXT NOT LIKE 'SELECT VERSION ( )%'
138
+ AND DIGEST_TEXT NOT LIKE 'SELECT @@%'
131
139
  ORDER BY #{order_clause}
132
140
  LIMIT #{limit}
133
141
  SQL
@@ -79,7 +79,12 @@
79
79
  /* Table */
80
80
  .mg-table-wrap { overflow-x: auto; }
81
81
  table.mg-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; }
82
- .mg-table th { padding: 10px 12px; text-align: left; white-space: nowrap; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #5a6770; background: #f1f3f5; border-bottom: 2px solid #d0d7de; }
82
+ .mg-table th { padding: 10px 12px; text-align: left; white-space: nowrap; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #5a6770; background: #f1f3f5; border-bottom: 2px solid #d0d7de; user-select: none; }
83
+ .mg-table th.mg-sortable { cursor: pointer; position: relative; padding-right: 20px; }
84
+ .mg-table th.mg-sortable:hover { color: #333; }
85
+ .mg-table th.mg-sortable::after { content: '\2195'; position: absolute; right: 6px; opacity: 0.3; font-size: 10px; }
86
+ .mg-table th.mg-sort-asc::after { content: '\2191'; opacity: 0.8; }
87
+ .mg-table th.mg-sort-desc::after { content: '\2193'; opacity: 0.8; }
83
88
  .mg-table th:first-child { border-top-left-radius: 6px; }
84
89
  .mg-table th:last-child { border-top-right-radius: 6px; }
85
90
  .mg-table td { padding: 8px 12px; border-bottom: 1px solid #eaecef; vertical-align: top; }
@@ -200,6 +205,7 @@
200
205
 
201
206
  /* Tables */
202
207
  [data-theme="dark"] .mg-table th { background: #161b22; color: #8b949e; border-bottom-color: #30363d; }
208
+ [data-theme="dark"] .mg-table th.mg-sortable:hover { color: #c9d1d9; }
203
209
  [data-theme="dark"] .mg-table td { border-bottom-color: #21262d; }
204
210
  [data-theme="dark"] .mg-table tbody tr:hover { background: #1c2128; }
205
211
  [data-theme="dark"] .mg-table tbody tr:nth-child(even) { background: #0d1117; }
@@ -2,7 +2,7 @@
2
2
  <div class="mg-tab-content" id="tab-qstats">
3
3
  <div class="mg-row" style="justify-content:space-between;align-items:center;margin-bottom:12px;">
4
4
  <div class="mg-text-muted">Top queries from <code>performance_schema.events_statements_summary_by_digest</code>. <span id="qstats-count" class="mg-badge mg-badge-secondary"></span></div>
5
- <div>
5
+ <!--div>
6
6
  <label style="display:inline;font-size:12px;margin-right:4px;">Sort by:</label>
7
7
  <select id="qstats-sort" style="width:auto;display:inline-block;padding:3px 6px;font-size:12px;">
8
8
  <option value="total_time">Total Time</option>
@@ -11,7 +11,7 @@
11
11
  <option value="rows_examined">Rows Examined</option>
12
12
  </select>
13
13
  <button id="qstats-refresh" class="mg-btn mg-btn-outline-secondary mg-btn-sm" style="margin-left:4px;">&#8635; Refresh</button>
14
- </div>
14
+ </div-->
15
15
  </div>
16
16
  <div id="qstats-loading" class="mg-text-center mg-hidden"><span class="mg-spinner"></span> Loading...</div>
17
17
  <div id="qstats-error" class="mg-hidden"></div>
@@ -23,7 +23,7 @@
23
23
  <th style="text-align:right">Total</th>
24
24
  <th style="text-align:right">Fragmented</th>
25
25
  <th>Updated</th>
26
- <th style="width:200px"></th>
26
+ <th style="width:200px" data-no-sort></th>
27
27
  </tr>
28
28
  </thead>
29
29
  <tbody id="sizes-tbody"></tbody>
@@ -76,6 +76,60 @@
76
76
  function hide(e) { e.classList.add('mg-hidden'); }
77
77
  function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
78
78
 
79
+ // --- Table Sorting ---
80
+
81
+ function parseSortValue(text) {
82
+ var m = text.match(/([\d.]+)\s*(s|ms|KB|MB|GB)/i);
83
+ if (m) {
84
+ var n = parseFloat(m[1]);
85
+ var unit = m[2].toLowerCase();
86
+ if (unit === 's') return n * 1000;
87
+ if (unit === 'gb') return n * 1024;
88
+ if (unit === 'mb') return n;
89
+ if (unit === 'kb') return n / 1024;
90
+ return n;
91
+ }
92
+ return parseFloat(text.replace(/[^0-9.\-]/g, '')) || 0;
93
+ }
94
+
95
+ function makeSortable(table) {
96
+ if (table.dataset.sortable) return;
97
+ table.dataset.sortable = '1';
98
+
99
+ var headers = Array.from(table.querySelectorAll('th'));
100
+ headers.forEach(function(th, colIdx) {
101
+ if (th.dataset.noSort || th.textContent.trim() === '' || th.textContent.trim() === 'Actions') return;
102
+ th.classList.add('mg-sortable');
103
+ th.addEventListener('click', function() {
104
+ var tbody = table.querySelector('tbody');
105
+ if (!tbody) return;
106
+ var rows = Array.from(tbody.querySelectorAll('tr'));
107
+ if (rows.length === 0) return;
108
+
109
+ var asc = !th.classList.contains('mg-sort-asc');
110
+ headers.forEach(function(h) { h.classList.remove('mg-sort-asc', 'mg-sort-desc'); });
111
+ th.classList.add(asc ? 'mg-sort-asc' : 'mg-sort-desc');
112
+
113
+ var isNum = th.style.textAlign === 'right' || th.classList.contains('mg-num');
114
+
115
+ rows.sort(function(a, b) {
116
+ var cellA = a.children[colIdx];
117
+ var cellB = b.children[colIdx];
118
+ if (!cellA || !cellB) return 0;
119
+ var valA = cellA.textContent.trim();
120
+ var valB = cellB.textContent.trim();
121
+
122
+ if (isNum) {
123
+ return asc ? parseSortValue(valA) - parseSortValue(valB) : parseSortValue(valB) - parseSortValue(valA);
124
+ }
125
+ return asc ? valA.localeCompare(valB) : valB.localeCompare(valA);
126
+ });
127
+
128
+ rows.forEach(function(row) { tbody.appendChild(row); });
129
+ });
130
+ });
131
+ }
132
+
79
133
  // --- SQL Syntax Highlighting (single-pass tokenizer) ---
80
134
 
81
135
  var SQL_KW_SET = {};
@@ -793,6 +847,7 @@
793
847
  '<button class="mg-btn mg-btn-outline-secondary mg-btn-sm slow-use-btn" data-sql="' + escHtml(q.sql).replace(/"/g, '&quot;') + '">Use</button></td></tr>';
794
848
  }).join('');
795
849
  show(el('slow-table-wrapper'));
850
+ makeSortable(qs('#slow-table-wrapper .mg-table'));
796
851
  }, function() {
797
852
  hide(el('slow-loading'));
798
853
  el('slow-empty').textContent = 'Failed to load slow queries.';
@@ -841,6 +896,7 @@
841
896
  '</tr>';
842
897
  }).join('');
843
898
  show(el('dup-table-wrapper'));
899
+ makeSortable(qs('#dup-table-wrapper .mg-table'));
844
900
 
845
901
  // Generate migration
846
902
  var ts = migrationTimestamp();
@@ -965,6 +1021,7 @@
965
1021
  '</tr>';
966
1022
  }).join('');
967
1023
  show(el('sizes-table-wrapper'));
1024
+ makeSortable(qs('#sizes-table-wrapper .mg-table'));
968
1025
  }, function() {
969
1026
  hide(el('sizes-loading'));
970
1027
  el('sizes-total').textContent = 'Failed to load';
@@ -1003,6 +1060,7 @@
1003
1060
  '</tr>';
1004
1061
  }).join('');
1005
1062
  show(el('qstats-table-wrapper'));
1063
+ makeSortable(qs('#qstats-table-wrapper .mg-table'));
1006
1064
  }, function(json) {
1007
1065
  hide(el('qstats-loading'));
1008
1066
  el('qstats-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load query stats.') + '</div>';
@@ -1045,6 +1103,7 @@
1045
1103
  '</tr>';
1046
1104
  }).join('');
1047
1105
  show(el('unused-table-wrapper'));
1106
+ makeSortable(qs('#unused-table-wrapper .mg-table'));
1048
1107
 
1049
1108
  // Generate migration
1050
1109
  var ts = migrationTimestamp();
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MysqlGenius
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mysql_genius
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antarr Byrd
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-04-10 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activerecord
@@ -61,6 +62,7 @@ extra_rdoc_files: []
61
62
  files:
62
63
  - ".github/FUNDING.yml"
63
64
  - ".github/workflows/ci.yml"
65
+ - ".github/workflows/publish.yml"
64
66
  - ".gitignore"
65
67
  - ".rspec"
66
68
  - ".rubocop.yml"
@@ -117,6 +119,7 @@ metadata:
117
119
  homepage_uri: https://github.com/antarr/mysql_genius
118
120
  source_code_uri: https://github.com/antarr/mysql_genius
119
121
  changelog_uri: https://github.com/antarr/mysql_genius/blob/main/CHANGELOG.md
122
+ post_install_message:
120
123
  rdoc_options: []
121
124
  require_paths:
122
125
  - lib
@@ -131,7 +134,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
134
  - !ruby/object:Gem::Version
132
135
  version: '0'
133
136
  requirements: []
134
- rubygems_version: 4.0.4
137
+ rubygems_version: 3.5.22
138
+ signing_key:
135
139
  specification_version: 4
136
140
  summary: A MySQL performance dashboard and query explorer for Rails — like PgHero,
137
141
  but for MySQL.