mysql_genius 0.4.1 → 0.5.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 +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +23 -0
- data/Gemfile +2 -0
- data/app/controllers/concerns/mysql_genius/ai_features.rb +16 -163
- data/app/controllers/concerns/mysql_genius/shared_view_helpers.rb +23 -0
- data/app/controllers/mysql_genius/queries_controller.rb +16 -20
- data/lib/mysql_genius/engine.rb +4 -0
- data/lib/mysql_genius/version.rb +1 -1
- data/mysql_genius.gemspec +1 -1
- metadata +5 -15
- data/app/views/mysql_genius/queries/_shared_results.html.erb +0 -56
- data/app/views/mysql_genius/queries/_tab_ai_tools.html.erb +0 -43
- data/app/views/mysql_genius/queries/_tab_dashboard.html.erb +0 -95
- data/app/views/mysql_genius/queries/_tab_duplicate_indexes.html.erb +0 -35
- data/app/views/mysql_genius/queries/_tab_query_explorer.html.erb +0 -110
- data/app/views/mysql_genius/queries/_tab_query_stats.html.erb +0 -26
- data/app/views/mysql_genius/queries/_tab_server.html.erb +0 -54
- data/app/views/mysql_genius/queries/_tab_slow_queries.html.erb +0 -17
- data/app/views/mysql_genius/queries/_tab_table_sizes.html.erb +0 -33
- data/app/views/mysql_genius/queries/_tab_unused_indexes.html.erb +0 -36
- data/app/views/mysql_genius/queries/index.html.erb +0 -1565
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48fbc36b280f63255eebb38587db28d24e38150b53faaff13efeddb810de8c70
|
|
4
|
+
data.tar.gz: a55a5115090bc136e99d854dc13c231670293a363f8d441951d98191c794d9e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d9de9d97d05814ff37b0ed88ee1846656ba3f1ff323f094ff776a3abb4be358d12204810e9068d2f6e99daea88d007f5a406f27681b7c1574a38395b0889c7e
|
|
7
|
+
data.tar.gz: cb5c725008371288dff5e8a32a66851914a39de71f0b647ade81bde743e01f7fe5f7ae2664715da9602b7e024b5609c3df53f37c1817faa73e7a4d801a26f292
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **ERB templates missing from `mysql_genius-core` gem package.** The gemspec glob only matched `*.rb` files, excluding the shared ERB templates. The dashboard crashed with `MissingTemplate` error when installed from RubyGems. Fixed by changing the glob to `*.{rb,erb}`.
|
|
7
|
+
|
|
8
|
+
## 0.5.0
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **ERB templates moved into `mysql_genius-core`.** All 11 view files (`dashboard.html.erb` and 10 partials) have been extracted from `app/views/mysql_genius/queries/` into `gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/`. The index template is renamed to `dashboard.html.erb`. The engine registers `MysqlGenius::Core.views_path` before `:add_view_paths` so Rails finds templates in both view roots. Non-Rails adapters (Phase 2b `mysql_genius-desktop` sidecar) can register this same path with their own view loader and implement `path_for`/`render_partial` to reuse the templates.
|
|
12
|
+
- **`QueriesController#index`** now sets `@framework_version_major` and `@framework_version_minor` instance variables (replacing direct `Rails::VERSION` references in the template) and explicitly renders `"mysql_genius/queries/dashboard"`.
|
|
13
|
+
- **`SharedViewHelpers`** — new concern providing `path_for(name)` and `render_partial(name)` as the 2-method contract the shared templates depend on. `render_partial` delegates to `view_context.render(partial: "mysql_genius/queries/#{name}")`.
|
|
14
|
+
- Extracted 5 AI prompt builders from the `AiFeatures` concern into `MysqlGenius::Core::Ai::{DescribeQuery, SchemaReview, RewriteQuery, IndexAdvisor, MigrationRisk}` plus a shared `Core::Ai::SchemaContextBuilder` helper. `anomaly_detection` and `root_cause` remain in the Rails concern because they depend on the Redis-backed `SlowQueryMonitor`.
|
|
15
|
+
- Extracted `QueriesController#columns` logic into `MysqlGenius::Core::Analysis::Columns` with a tagged-result struct. Retires the `masked_column?` helper added in the 0.4.1 hotfix.
|
|
16
|
+
- `MysqlGenius::Core::Ai::Config` gains a `domain_context:` field. The Rails adapter defaults it to a Rails-specific string; `mysql_genius-desktop` will default to empty.
|
|
17
|
+
- `mysql_genius` now declares runtime dependency on `mysql_genius-core ~> 0.5.0` (was `~> 0.4.0`).
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Integration test suite at `spec/dummy/` + `spec/rails_helper.rb` + `spec/requests/`. Boots a minimal Rails engine dummy app and dispatches real HTTP requests against the mounted engine via `Rack::Test`. Dedicated regression specs at `spec/regressions/` pin the two Phase 1b latent bugs (`Core::Connection::ActiveRecordAdapter` boot-order and `QueriesController#masked_column?` helper deletion) so they can never silently return.
|
|
21
|
+
- `CLAUDE.md` updated: the "no Rails boot in tests" rule is relaxed to a two-tier model (unit specs stub AR, integration specs boot Rails via `spec/dummy/`).
|
|
22
|
+
|
|
23
|
+
### Internal
|
|
24
|
+
- `MysqlGenius::Core.views_path` — new public module method returning the absolute path to the shared ERB template directory.
|
|
25
|
+
|
|
3
26
|
## 0.4.1
|
|
4
27
|
|
|
5
28
|
### Fixed
|
data/Gemfile
CHANGED
|
@@ -47,20 +47,7 @@ module MysqlGenius
|
|
|
47
47
|
sql = params[:sql].to_s.strip
|
|
48
48
|
return render(json: { error: "SQL is required." }, status: :unprocessable_entity) if sql.blank?
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
{ role: "system", content: <<~PROMPT },
|
|
52
|
-
You are a MySQL query explainer. Given a SQL query, explain in plain English:
|
|
53
|
-
1. What the query does (tables involved, joins, filters, aggregations)
|
|
54
|
-
2. How data flows through the query
|
|
55
|
-
3. Any subtle behaviors (implicit type casts, NULL handling in NOT IN, DISTINCT effects, etc.)
|
|
56
|
-
4. Potential performance concerns visible from the SQL structure alone
|
|
57
|
-
#{ai_domain_context}
|
|
58
|
-
Respond with JSON: {"explanation": "your plain-English explanation using markdown formatting"}
|
|
59
|
-
PROMPT
|
|
60
|
-
{ role: "user", content: sql },
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
result = ai_client.chat(messages: messages)
|
|
50
|
+
result = MysqlGenius::Core::Ai::DescribeQuery.new(ai_client, ai_config_for_core).call(sql)
|
|
64
51
|
render(json: result)
|
|
65
52
|
rescue StandardError => e
|
|
66
53
|
render(json: { error: "Explanation failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -69,42 +56,7 @@ module MysqlGenius
|
|
|
69
56
|
def schema_review
|
|
70
57
|
return ai_not_configured unless mysql_genius_config.ai_enabled?
|
|
71
58
|
|
|
72
|
-
|
|
73
|
-
connection = ActiveRecord::Base.connection
|
|
74
|
-
|
|
75
|
-
tables_to_review = table.present? ? [table] : queryable_tables.first(20)
|
|
76
|
-
schema_desc = tables_to_review.map do |t|
|
|
77
|
-
next unless connection.tables.include?(t)
|
|
78
|
-
|
|
79
|
-
cols = connection.columns(t).map { |c| "#{c.name} #{c.sql_type}#{" NOT NULL" unless c.null}#{" DEFAULT #{c.default}" if c.default}" }
|
|
80
|
-
pk = connection.primary_key(t)
|
|
81
|
-
indexes = connection.indexes(t).map { |idx| "#{"UNIQUE " if idx.unique}INDEX #{idx.name} (#{idx.columns.join(", ")})" }
|
|
82
|
-
row_count = connection.exec_query("SELECT TABLE_ROWS FROM information_schema.tables WHERE table_schema = #{connection.quote(connection.current_database)} AND table_name = #{connection.quote(t)}").rows.first&.first
|
|
83
|
-
desc = "Table: #{t} (~#{row_count} rows)\n"
|
|
84
|
-
desc += "Primary Key: #{pk || "NONE"}\n"
|
|
85
|
-
desc += "Columns: #{cols.join(", ")}\n"
|
|
86
|
-
desc += "Indexes: #{indexes.any? ? indexes.join(", ") : "NONE"}"
|
|
87
|
-
desc
|
|
88
|
-
end.compact.join("\n\n")
|
|
89
|
-
|
|
90
|
-
messages = [
|
|
91
|
-
{ role: "system", content: <<~PROMPT },
|
|
92
|
-
You are a MySQL schema reviewer for a Ruby on Rails application. Analyze the following schema and identify anti-patterns and improvement opportunities. Look for:
|
|
93
|
-
- Inappropriate column types (VARCHAR(255) for short values, TEXT where VARCHAR suffices, INT for booleans)
|
|
94
|
-
- Missing indexes on foreign key columns or frequently filtered columns
|
|
95
|
-
- Missing NOT NULL constraints where NULLs are unlikely
|
|
96
|
-
- ENUM columns that should be lookup tables
|
|
97
|
-
- Missing created_at/updated_at timestamps
|
|
98
|
-
- Tables without a PRIMARY KEY
|
|
99
|
-
- Overly wide indexes or redundant indexes
|
|
100
|
-
- Column naming inconsistencies
|
|
101
|
-
#{ai_domain_context}
|
|
102
|
-
Respond with JSON: {"findings": "markdown-formatted findings organized by severity (Critical, Warning, Suggestion). Include specific ALTER TABLE statements where applicable."}
|
|
103
|
-
PROMPT
|
|
104
|
-
{ role: "user", content: schema_desc },
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
result = ai_client.chat(messages: messages)
|
|
59
|
+
result = MysqlGenius::Core::Ai::SchemaReview.new(ai_client, ai_config_for_core, rails_connection).call(params[:table].to_s.strip.presence)
|
|
108
60
|
render(json: result)
|
|
109
61
|
rescue StandardError => e
|
|
110
62
|
render(json: { error: "Schema review failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -116,31 +68,7 @@ module MysqlGenius
|
|
|
116
68
|
sql = params[:sql].to_s.strip
|
|
117
69
|
return render(json: { error: "SQL is required." }, status: :unprocessable_entity) if sql.blank?
|
|
118
70
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
messages = [
|
|
122
|
-
{ role: "system", content: <<~PROMPT },
|
|
123
|
-
You are a MySQL query rewrite expert. Analyze the SQL for anti-patterns and suggest a rewritten version. Look for:
|
|
124
|
-
- SELECT * when specific columns would suffice
|
|
125
|
-
- Correlated subqueries that could be JOINs
|
|
126
|
-
- OR conditions preventing index use (suggest UNION ALL)
|
|
127
|
-
- LIKE '%prefix' patterns (leading wildcard)
|
|
128
|
-
- Implicit type conversions in WHERE clauses
|
|
129
|
-
- NOT IN with NULLable columns (suggest NOT EXISTS)
|
|
130
|
-
- ORDER BY on non-indexed columns with LIMIT
|
|
131
|
-
- Unnecessary DISTINCT
|
|
132
|
-
- Functions on indexed columns in WHERE (e.g., DATE(created_at) instead of range)
|
|
133
|
-
|
|
134
|
-
Available schema:
|
|
135
|
-
#{schema}
|
|
136
|
-
#{ai_domain_context}
|
|
137
|
-
|
|
138
|
-
Respond with JSON: {"original": "the original SQL", "rewritten": "the improved SQL", "changes": "markdown list of each change and why it helps"}
|
|
139
|
-
PROMPT
|
|
140
|
-
{ role: "user", content: sql },
|
|
141
|
-
]
|
|
142
|
-
|
|
143
|
-
result = ai_client.chat(messages: messages)
|
|
71
|
+
result = MysqlGenius::Core::Ai::RewriteQuery.new(ai_client, ai_config_for_core, rails_connection).call(sql)
|
|
144
72
|
render(json: result)
|
|
145
73
|
rescue StandardError => e
|
|
146
74
|
render(json: { error: "Rewrite failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -153,33 +81,7 @@ module MysqlGenius
|
|
|
153
81
|
explain_rows = Array(params[:explain_rows]).map { |row| row.respond_to?(:values) ? row.values : Array(row) }
|
|
154
82
|
return render(json: { error: "SQL and EXPLAIN output are required." }, status: :unprocessable_entity) if sql.blank? || explain_rows.blank?
|
|
155
83
|
|
|
156
|
-
|
|
157
|
-
tables_in_query = MysqlGenius::Core::SqlValidator.extract_table_references(sql, connection)
|
|
158
|
-
|
|
159
|
-
index_detail = tables_in_query.map do |t|
|
|
160
|
-
indexes = connection.indexes(t).map { |idx| "#{"UNIQUE " if idx.unique}INDEX #{idx.name} (#{idx.columns.join(", ")})" }
|
|
161
|
-
stats = connection.exec_query("SELECT INDEX_NAME, COLUMN_NAME, CARDINALITY, SEQ_IN_INDEX FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = #{connection.quote(connection.current_database)} AND TABLE_NAME = #{connection.quote(t)} ORDER BY INDEX_NAME, SEQ_IN_INDEX")
|
|
162
|
-
cardinality = stats.rows.map { |r| "#{r[0]}.#{r[1]}: cardinality=#{r[2]}" }.join(", ")
|
|
163
|
-
row_count = connection.exec_query("SELECT TABLE_ROWS FROM information_schema.tables WHERE table_schema = #{connection.quote(connection.current_database)} AND table_name = #{connection.quote(t)}").rows.first&.first
|
|
164
|
-
"Table: #{t} (~#{row_count} rows)\nIndexes: #{indexes.any? ? indexes.join("; ") : "NONE"}\nCardinality: #{cardinality}"
|
|
165
|
-
end.join("\n\n")
|
|
166
|
-
|
|
167
|
-
messages = [
|
|
168
|
-
{ role: "system", content: <<~PROMPT },
|
|
169
|
-
You are a MySQL index advisor. Given a query, its EXPLAIN output, and current index/cardinality information, suggest optimal indexes. Consider:
|
|
170
|
-
- Composite index column ordering (most selective first, or matching query order)
|
|
171
|
-
- Covering indexes to avoid table lookups
|
|
172
|
-
- Partial indexes for long string columns
|
|
173
|
-
- Write-side costs (if this is a high-write table, note the INSERT/UPDATE overhead)
|
|
174
|
-
- Whether existing indexes could be extended rather than creating new ones
|
|
175
|
-
#{ai_domain_context}
|
|
176
|
-
|
|
177
|
-
Respond with JSON: {"indexes": "markdown-formatted recommendations with exact CREATE INDEX statements, rationale for column ordering, and estimated impact. Include any indexes that should be DROPPED as part of the change."}
|
|
178
|
-
PROMPT
|
|
179
|
-
{ role: "user", content: "Query:\n#{sql}\n\nEXPLAIN:\n#{explain_rows.map { |r| r.join(" | ") }.join("\n")}\n\nCurrent Indexes:\n#{index_detail}" },
|
|
180
|
-
]
|
|
181
|
-
|
|
182
|
-
result = ai_client.chat(messages: messages)
|
|
84
|
+
result = MysqlGenius::Core::Ai::IndexAdvisor.new(ai_client, ai_config_for_core, rails_connection).call(sql, explain_rows)
|
|
183
85
|
render(json: result)
|
|
184
86
|
rescue StandardError => e
|
|
185
87
|
render(json: { error: "Index advisor failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -323,54 +225,7 @@ module MysqlGenius
|
|
|
323
225
|
migration_sql = params[:migration].to_s.strip
|
|
324
226
|
return render(json: { error: "Migration SQL or Ruby code is required." }, status: :unprocessable_entity) if migration_sql.blank?
|
|
325
227
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# Try to identify tables mentioned in the migration
|
|
329
|
-
table_names = migration_sql.scan(/(?:create_table|add_column|remove_column|add_index|remove_index|rename_column|change_column|alter\s+table)\s+[:\"]?(\w+)/i).flatten.uniq
|
|
330
|
-
table_names += migration_sql.scan(/ALTER\s+TABLE\s+`?(\w+)`?/i).flatten
|
|
331
|
-
|
|
332
|
-
table_info = table_names.uniq.map do |t|
|
|
333
|
-
next unless connection.tables.include?(t)
|
|
334
|
-
|
|
335
|
-
row_count = connection.exec_query("SELECT TABLE_ROWS FROM information_schema.tables WHERE table_schema = #{connection.quote(connection.current_database)} AND table_name = #{connection.quote(t)}").rows.first&.first
|
|
336
|
-
indexes = connection.indexes(t).map { |idx| "#{idx.name} (#{idx.columns.join(", ")})" }
|
|
337
|
-
"Table: #{t} (~#{row_count} rows, #{indexes.size} indexes)"
|
|
338
|
-
end.compact.join("\n")
|
|
339
|
-
|
|
340
|
-
# Current active queries on those tables
|
|
341
|
-
active = ""
|
|
342
|
-
begin
|
|
343
|
-
results = connection.exec_query(<<~SQL)
|
|
344
|
-
SELECT DIGEST_TEXT, COUNT_STAR AS calls, ROUND(AVG_TIMER_WAIT / 1000000000, 1) AS avg_ms
|
|
345
|
-
FROM performance_schema.events_statements_summary_by_digest
|
|
346
|
-
WHERE SCHEMA_NAME = #{connection.quote(connection.current_database)}
|
|
347
|
-
AND DIGEST_TEXT IS NOT NULL
|
|
348
|
-
AND COUNT_STAR > 10
|
|
349
|
-
ORDER BY COUNT_STAR DESC LIMIT 20
|
|
350
|
-
SQL
|
|
351
|
-
matching = results.rows.select { |r| table_names.any? { |t| r[0].to_s.downcase.include?(t.downcase) } }
|
|
352
|
-
active = matching.map { |r| "calls=#{r[1]} avg=#{r[2]}ms: #{r[0].to_s.truncate(200)}" }.join("\n")
|
|
353
|
-
rescue ActiveRecord::StatementInvalid
|
|
354
|
-
# performance_schema may be unavailable
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
messages = [
|
|
358
|
-
{ role: "system", content: <<~PROMPT },
|
|
359
|
-
You are a MySQL migration risk assessor. Given a Rails migration or DDL, evaluate:
|
|
360
|
-
1. Will this lock the table? For how long given the row count?
|
|
361
|
-
2. Is this safe to run during traffic, or does it need a maintenance window?
|
|
362
|
-
3. Should pt-online-schema-change or gh-ost be used instead?
|
|
363
|
-
4. Will it break or degrade any of the active queries against this table?
|
|
364
|
-
5. Are there any data loss risks?
|
|
365
|
-
6. What is the recommended deployment strategy?
|
|
366
|
-
#{ai_domain_context}
|
|
367
|
-
|
|
368
|
-
Respond with JSON: {"risk_level": "low|medium|high|critical", "assessment": "markdown-formatted risk assessment with specific recommendations and estimated lock duration"}
|
|
369
|
-
PROMPT
|
|
370
|
-
{ role: "user", content: "Migration:\n#{migration_sql}\n\nAffected Tables:\n#{table_info.presence || "Could not determine"}\n\nActive Queries on These Tables:\n#{active.presence || "None found or performance_schema unavailable"}" },
|
|
371
|
-
]
|
|
372
|
-
|
|
373
|
-
result = ai_client.chat(messages: messages)
|
|
228
|
+
result = MysqlGenius::Core::Ai::MigrationRisk.new(ai_client, ai_config_for_core, rails_connection).call(migration_sql)
|
|
374
229
|
render(json: result)
|
|
375
230
|
rescue StandardError => e
|
|
376
231
|
render(json: { error: "Migration risk assessment failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -378,6 +233,10 @@ module MysqlGenius
|
|
|
378
233
|
|
|
379
234
|
private
|
|
380
235
|
|
|
236
|
+
RAILS_DOMAIN_CONTEXT = <<~CTX
|
|
237
|
+
This is a Ruby on Rails application. Do NOT recommend adding foreign key constraints (FOREIGN KEY / REFERENCES); Rails handles referential integrity at the application layer. DO recommend indexes on foreign key columns for join performance.
|
|
238
|
+
CTX
|
|
239
|
+
|
|
381
240
|
def ai_client
|
|
382
241
|
MysqlGenius::Core::Ai::Client.new(ai_config_for_core)
|
|
383
242
|
end
|
|
@@ -391,6 +250,7 @@ module MysqlGenius
|
|
|
391
250
|
model: cfg.ai_model,
|
|
392
251
|
auth_style: cfg.ai_auth_style,
|
|
393
252
|
system_context: cfg.ai_system_context,
|
|
253
|
+
domain_context: RAILS_DOMAIN_CONTEXT,
|
|
394
254
|
)
|
|
395
255
|
end
|
|
396
256
|
|
|
@@ -398,21 +258,14 @@ module MysqlGenius
|
|
|
398
258
|
render(json: { error: "AI features are not configured." }, status: :not_found)
|
|
399
259
|
end
|
|
400
260
|
|
|
401
|
-
def
|
|
402
|
-
|
|
403
|
-
parts << "This is a Ruby on Rails application. Do NOT recommend adding foreign key constraints (FOREIGN KEY / REFERENCES); Rails handles referential integrity at the application layer. DO recommend indexes on foreign key columns for join performance."
|
|
404
|
-
ctx = mysql_genius_config.ai_system_context
|
|
405
|
-
parts << "Domain context:\n#{ctx}" if ctx.present?
|
|
406
|
-
"\n" + parts.join("\n")
|
|
261
|
+
def rails_connection
|
|
262
|
+
MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
|
|
407
263
|
end
|
|
408
264
|
|
|
409
|
-
def
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
cols = connection.columns(t).map { |c| "#{c.name} (#{c.type})" }
|
|
414
|
-
"#{t}: #{cols.join(", ")}"
|
|
415
|
-
end.join("\n")
|
|
265
|
+
def ai_domain_context
|
|
266
|
+
cfg = mysql_genius_config
|
|
267
|
+
ctx = cfg.ai_system_context
|
|
268
|
+
ctx.present? ? "\nDomain context:\n#{ctx}" : ""
|
|
416
269
|
end
|
|
417
270
|
end
|
|
418
271
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MysqlGenius
|
|
4
|
+
module SharedViewHelpers
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
helper_method :path_for, :render_partial
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# URL path helper for shared templates.
|
|
12
|
+
# path_for(:execute) # => "/mysql_genius/execute" (from engine route helpers)
|
|
13
|
+
def path_for(name)
|
|
14
|
+
mysql_genius.public_send("#{name}_path")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Partial renderer for shared templates.
|
|
18
|
+
# render_partial(:tab_dashboard) # => view_context.render partial: "mysql_genius/queries/tab_dashboard"
|
|
19
|
+
def render_partial(name)
|
|
20
|
+
view_context.render(partial: "mysql_genius/queries/#{name}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -5,6 +5,7 @@ module MysqlGenius
|
|
|
5
5
|
include QueryExecution
|
|
6
6
|
include DatabaseAnalysis
|
|
7
7
|
include AiFeatures
|
|
8
|
+
include SharedViewHelpers
|
|
8
9
|
|
|
9
10
|
def index
|
|
10
11
|
@featured_tables = if mysql_genius_config.featured_tables.any?
|
|
@@ -14,23 +15,24 @@ module MysqlGenius
|
|
|
14
15
|
end
|
|
15
16
|
@all_tables = queryable_tables.sort
|
|
16
17
|
@ai_enabled = mysql_genius_config.ai_enabled?
|
|
18
|
+
@framework_version_major = Rails::VERSION::MAJOR
|
|
19
|
+
@framework_version_minor = Rails::VERSION::MINOR
|
|
20
|
+
render("mysql_genius/queries/dashboard")
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def columns
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return render(json: { error: "Table '#{table}' does not exist." }, status: :not_found)
|
|
27
|
-
end
|
|
24
|
+
result = MysqlGenius::Core::Analysis::Columns.new(
|
|
25
|
+
rails_connection,
|
|
26
|
+
blocked_tables: mysql_genius_config.blocked_tables,
|
|
27
|
+
masked_column_patterns: mysql_genius_config.masked_column_patterns,
|
|
28
|
+
default_columns: mysql_genius_config.default_columns,
|
|
29
|
+
).call(table: params[:table])
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
case result.status
|
|
32
|
+
when :ok then render(json: result.columns)
|
|
33
|
+
when :blocked then render(json: { error: result.error_message }, status: :forbidden)
|
|
34
|
+
when :not_found then render(json: { error: result.error_message }, status: :not_found)
|
|
32
35
|
end
|
|
33
|
-
render(json: cols)
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def slow_queries
|
|
@@ -58,14 +60,8 @@ module MysqlGenius
|
|
|
58
60
|
ActiveRecord::Base.connection.tables - mysql_genius_config.blocked_tables
|
|
59
61
|
end
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# reintroduces the 1-arg instance method the controller's `columns`
|
|
64
|
-
# action depends on. Without this helper, `columns` raises NoMethodError
|
|
65
|
-
# at runtime (Phase 1b regression — Core::SqlValidator.masked_column?
|
|
66
|
-
# became a 2-arg class method but the call site wasn't updated).
|
|
67
|
-
def masked_column?(name)
|
|
68
|
-
MysqlGenius::Core::SqlValidator.masked_column?(name, mysql_genius_config.masked_column_patterns)
|
|
63
|
+
def rails_connection
|
|
64
|
+
MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
|
|
69
65
|
end
|
|
70
66
|
end
|
|
71
67
|
end
|
data/lib/mysql_genius/engine.rb
CHANGED
|
@@ -4,6 +4,10 @@ module MysqlGenius
|
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
isolate_namespace MysqlGenius
|
|
6
6
|
|
|
7
|
+
initializer "mysql_genius.register_core_views", before: :add_view_paths do
|
|
8
|
+
paths["app/views"] << MysqlGenius::Core.views_path
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
config.after_initialize do
|
|
8
12
|
if MysqlGenius.configuration.redis_url.present?
|
|
9
13
|
require "mysql_genius/slow_query_monitor"
|
data/lib/mysql_genius/version.rb
CHANGED
data/mysql_genius.gemspec
CHANGED
|
@@ -31,6 +31,6 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
spec.require_paths = ["lib"]
|
|
32
32
|
|
|
33
33
|
spec.add_dependency("activerecord", ">= 5.2", "< 9")
|
|
34
|
-
spec.add_dependency("mysql_genius-core", "~> 0.
|
|
34
|
+
spec.add_dependency("mysql_genius-core", "~> 0.5.0")
|
|
35
35
|
spec.add_dependency("railties", ">= 5.2", "< 9")
|
|
36
36
|
end
|
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.5.1
|
|
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.5.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.5.0
|
|
47
47
|
- !ruby/object:Gem::Dependency
|
|
48
48
|
name: railties
|
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -88,20 +88,10 @@ files:
|
|
|
88
88
|
- app/controllers/concerns/mysql_genius/ai_features.rb
|
|
89
89
|
- app/controllers/concerns/mysql_genius/database_analysis.rb
|
|
90
90
|
- app/controllers/concerns/mysql_genius/query_execution.rb
|
|
91
|
+
- app/controllers/concerns/mysql_genius/shared_view_helpers.rb
|
|
91
92
|
- app/controllers/mysql_genius/base_controller.rb
|
|
92
93
|
- app/controllers/mysql_genius/queries_controller.rb
|
|
93
94
|
- app/views/layouts/mysql_genius/application.html.erb
|
|
94
|
-
- app/views/mysql_genius/queries/_shared_results.html.erb
|
|
95
|
-
- app/views/mysql_genius/queries/_tab_ai_tools.html.erb
|
|
96
|
-
- app/views/mysql_genius/queries/_tab_dashboard.html.erb
|
|
97
|
-
- app/views/mysql_genius/queries/_tab_duplicate_indexes.html.erb
|
|
98
|
-
- app/views/mysql_genius/queries/_tab_query_explorer.html.erb
|
|
99
|
-
- app/views/mysql_genius/queries/_tab_query_stats.html.erb
|
|
100
|
-
- app/views/mysql_genius/queries/_tab_server.html.erb
|
|
101
|
-
- app/views/mysql_genius/queries/_tab_slow_queries.html.erb
|
|
102
|
-
- app/views/mysql_genius/queries/_tab_table_sizes.html.erb
|
|
103
|
-
- app/views/mysql_genius/queries/_tab_unused_indexes.html.erb
|
|
104
|
-
- app/views/mysql_genius/queries/index.html.erb
|
|
105
95
|
- bin/console
|
|
106
96
|
- bin/setup
|
|
107
97
|
- config/routes.rb
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
<!-- Explain Results -->
|
|
2
|
-
<div id="explain-results" class="mg-mt mg-hidden">
|
|
3
|
-
<div class="mg-card">
|
|
4
|
-
<div class="mg-card-header">
|
|
5
|
-
<span><strong>🔎 EXPLAIN Output</strong></span>
|
|
6
|
-
<div>
|
|
7
|
-
<% if @ai_enabled %>
|
|
8
|
-
<button id="explain-optimize" class="mg-btn mg-btn-outline mg-btn-sm">⚡ AI Optimization</button>
|
|
9
|
-
<button id="explain-index-advisor" class="mg-btn mg-btn-outline mg-btn-sm">⚡ Index Advisor</button>
|
|
10
|
-
<% end %>
|
|
11
|
-
<button id="explain-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm">✕ Close</button>
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
<div class="mg-card-body">
|
|
15
|
-
<div class="mg-table-wrap">
|
|
16
|
-
<table class="mg-table">
|
|
17
|
-
<thead id="explain-thead"></thead>
|
|
18
|
-
<tbody id="explain-tbody"></tbody>
|
|
19
|
-
</table>
|
|
20
|
-
</div>
|
|
21
|
-
<div id="optimize-results" class="mg-hidden mg-mt">
|
|
22
|
-
<div id="optimize-content" class="mg-alert mg-alert-info"></div>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<!-- Results Area -->
|
|
29
|
-
<div id="query-results" class="mg-mt">
|
|
30
|
-
<div id="results-alert" class="mg-hidden"></div>
|
|
31
|
-
<div id="results-stats" class="mg-mb mg-hidden">
|
|
32
|
-
<span id="results-row-count" class="mg-badge mg-badge-info"></span>
|
|
33
|
-
<span id="results-time" class="mg-badge mg-badge-secondary"></span>
|
|
34
|
-
<span id="results-truncated" class="mg-badge mg-badge-warning mg-hidden">Results truncated</span>
|
|
35
|
-
</div>
|
|
36
|
-
<div id="results-table-wrapper" class="mg-table-wrap mg-hidden">
|
|
37
|
-
<table class="mg-table">
|
|
38
|
-
<thead id="results-thead"></thead>
|
|
39
|
-
<tbody id="results-tbody"></tbody>
|
|
40
|
-
</table>
|
|
41
|
-
</div>
|
|
42
|
-
<div id="results-empty" class="mg-text-center mg-text-muted mg-hidden">No rows returned.</div>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<!-- AI Query Analysis Results -->
|
|
46
|
-
<div id="ai-query-result" class="mg-mt mg-hidden">
|
|
47
|
-
<div class="mg-card">
|
|
48
|
-
<div class="mg-card-header">
|
|
49
|
-
<span id="ai-query-title"><strong>⚡ AI Analysis</strong></span>
|
|
50
|
-
<button id="ai-query-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm">✕</button>
|
|
51
|
-
</div>
|
|
52
|
-
<div class="mg-card-body">
|
|
53
|
-
<div id="ai-query-content" style="font-size:13px;"></div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
<!-- AI Tools Tab -->
|
|
2
|
-
<div class="mg-tab-content" id="tab-aitools">
|
|
3
|
-
<!-- Schema Review -->
|
|
4
|
-
<div class="mg-card mg-mb">
|
|
5
|
-
<div class="mg-card-header"><strong>⚡ Schema Review</strong></div>
|
|
6
|
-
<div class="mg-card-body">
|
|
7
|
-
<div class="mg-text-muted mg-mb" style="font-size:12px;">Analyze your schema for anti-patterns: inappropriate column types, missing indexes, naming inconsistencies, and more.</div>
|
|
8
|
-
<div class="mg-row" style="align-items:flex-end;">
|
|
9
|
-
<div class="mg-col-4 mg-field">
|
|
10
|
-
<label for="schema-table">Table (leave blank for all)</label>
|
|
11
|
-
<select id="schema-table">
|
|
12
|
-
<option value="">All tables (top 20)</option>
|
|
13
|
-
<% @all_tables.each do |table| %>
|
|
14
|
-
<option value="<%= table %>"><%= table %></option>
|
|
15
|
-
<% end %>
|
|
16
|
-
</select>
|
|
17
|
-
</div>
|
|
18
|
-
<div class="mg-field">
|
|
19
|
-
<button id="schema-review-btn" class="mg-btn mg-btn-primary mg-btn-sm">⚡ Analyze Schema</button>
|
|
20
|
-
</div>
|
|
21
|
-
</div>
|
|
22
|
-
<div id="schema-result" class="mg-mt mg-hidden">
|
|
23
|
-
<div id="schema-result-content" style="font-size:13px;"></div>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<!-- Migration Risk Assessment -->
|
|
29
|
-
<div class="mg-card">
|
|
30
|
-
<div class="mg-card-header"><strong>⚡ Migration Risk Assessment</strong></div>
|
|
31
|
-
<div class="mg-card-body">
|
|
32
|
-
<div class="mg-text-muted mg-mb" style="font-size:12px;">Paste a Rails migration or DDL and get a risk assessment: lock duration, impact on active queries, deployment strategy.</div>
|
|
33
|
-
<div class="mg-field">
|
|
34
|
-
<textarea id="migration-input" rows="8" placeholder="class AddIndexToUsers < ActiveRecord::Migration[7.0] def change add_index :users, :email, unique: true end end"></textarea>
|
|
35
|
-
</div>
|
|
36
|
-
<button id="migration-assess-btn" class="mg-btn mg-btn-primary mg-btn-sm">⚡ Assess Risk</button>
|
|
37
|
-
<div id="migration-result" class="mg-mt mg-hidden">
|
|
38
|
-
<div id="migration-risk-badge" style="margin-bottom:8px;"></div>
|
|
39
|
-
<div id="migration-result-content" style="font-size:13px;"></div>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|