mysql_genius 0.4.1 → 0.6.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/.github/workflows/ci.yml +2 -11
- data/.github/workflows/publish.yml +2 -2
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +38 -0
- data/Gemfile +2 -0
- data/README.md +2 -1
- data/app/controllers/concerns/mysql_genius/ai_features.rb +17 -173
- data/app/controllers/concerns/mysql_genius/database_analysis.rb +5 -10
- data/app/controllers/concerns/mysql_genius/query_execution.rb +2 -4
- data/app/controllers/concerns/mysql_genius/shared_view_helpers.rb +23 -0
- data/app/controllers/mysql_genius/base_controller.rb +9 -0
- data/app/controllers/mysql_genius/queries_controller.rb +15 -23
- data/lib/mysql_genius/engine.rb +4 -0
- data/lib/mysql_genius/version.rb +1 -1
- data/mysql_genius.gemspec +3 -4
- metadata +9 -20
- 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: a35fb751622eaa5cd7bc6fad555020fb5dbf2a9a2f50d50d27ee9b3da6440818
|
|
4
|
+
data.tar.gz: 1423b66748df1f05b0a9e9ff3868d35905953218443823e9cf2b0a554e88c4db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fbd465309eb1068b2c78cc47e56e93ac98badd97f8df834a306cbda5c114f903f410c5a7e927c6e9079556d2a240e44521290a28b959e9af91805f13b1fe22d
|
|
7
|
+
data.tar.gz: 2f3fe565bd970da7f1a1ac0f331bc435a59cc5508b1094fdc91003ef01f5fd3fb7782618cc7d55e24e5aec243f597895b5d4bd1238e90b150d5d92ba2cf611ef
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -14,17 +14,8 @@ jobs:
|
|
|
14
14
|
fail-fast: false
|
|
15
15
|
matrix:
|
|
16
16
|
ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"]
|
|
17
|
-
rails: ["
|
|
17
|
+
rails: ["6.0", "6.1", "7.0", "7.1", "7.2", "8.0", "8.1"]
|
|
18
18
|
exclude:
|
|
19
|
-
# Rails 5.2 doesn't support Ruby 3.1+
|
|
20
|
-
- ruby: "3.1"
|
|
21
|
-
rails: "5.2"
|
|
22
|
-
- ruby: "3.2"
|
|
23
|
-
rails: "5.2"
|
|
24
|
-
- ruby: "3.3"
|
|
25
|
-
rails: "5.2"
|
|
26
|
-
- ruby: "3.4"
|
|
27
|
-
rails: "5.2"
|
|
28
19
|
# Rails 6.0 doesn't support Ruby 3.2+
|
|
29
20
|
- ruby: "3.2"
|
|
30
21
|
rails: "6.0"
|
|
@@ -61,7 +52,7 @@ jobs:
|
|
|
61
52
|
RAILS_VERSION: ${{ matrix.rails }}
|
|
62
53
|
|
|
63
54
|
steps:
|
|
64
|
-
- uses: actions/checkout@
|
|
55
|
+
- uses: actions/checkout@v5
|
|
65
56
|
|
|
66
57
|
- name: Set up Ruby ${{ matrix.ruby }}
|
|
67
58
|
uses: ruby/setup-ruby@v1
|
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
test:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
12
|
+
- uses: actions/checkout@v5
|
|
13
13
|
- uses: ruby/setup-ruby@v1
|
|
14
14
|
with:
|
|
15
15
|
ruby-version: 3.3
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
needs: test
|
|
26
26
|
runs-on: ubuntu-latest
|
|
27
27
|
steps:
|
|
28
|
-
- uses: actions/checkout@
|
|
28
|
+
- uses: actions/checkout@v5
|
|
29
29
|
- uses: ruby/setup-ruby@v1
|
|
30
30
|
with:
|
|
31
31
|
ruby-version: 3.3
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Dropped Rails 5.2 support.** The gemspec floor is now Rails 6.0 (`activerecord`/`railties` constraint is `">= 6.0", "< 9"`). Rails 5.2 has been end-of-life since June 2022 and its incompatibilities with modern Rack (`ActionDispatch::Static#initialize` arity mismatch, `MiddlewareStack#operations` removal) started surfacing as CI failures once Phase 2a's integration specs booted Rails in test. Pin `mysql_genius 0.5.0` (`gem "mysql_genius", "~> 0.5.0"`) if you can't upgrade Rails yet.
|
|
7
|
+
- `mysql_genius` now declares runtime dependency on `mysql_genius-core ~> 0.6.0` (was `~> 0.5.0`).
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **CI matrix: Ruby 3.x + Rails 5.2/6.0/6.1 compatibility.** `spec/rails_helper.rb` and `spec/dummy/config/application.rb` now explicitly `require "logger"` before loading Rails. Works around a `Logger::Severity` reference inside `ActiveSupport::LoggerThreadSafeLevel` that fails on Ruby 3.x + older Rails because Logger is no longer autoloaded in modern Ruby. Only affects the test suite; no runtime impact on host apps.
|
|
11
|
+
|
|
12
|
+
### Internal
|
|
13
|
+
- **`rails_connection` consolidated into `BaseController`.** Nine inline `MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)` call sites in the three controller concerns, plus two separate private helper definitions in `QueriesController` and `AiFeatures`, collapse to one private method on `BaseController`. Shared across all concerns via Ruby's standard method lookup.
|
|
14
|
+
- **`ai_domain_context` helper inlined and deleted.** Its two remaining callers (`anomaly_detection` and `root_cause`) now compute a local `domain_ctx` string before building their message array.
|
|
15
|
+
- **`fake_result(columns:, rows:, to_a:)` test helper extracted** into `spec/support/fake_connection.rb` alongside `fake_column`. Four duplicated `instance_double("ActiveRecord::Result", columns:, rows:, to_a:)` call sites in request specs refactored.
|
|
16
|
+
- **`Core::Analysis::Columns` spec** covers the `default: false` branch (6th column outside `default_columns`).
|
|
17
|
+
- **`actions/checkout@v4` bumped to `@v5`** in both `.github/workflows/ci.yml` and `.github/workflows/publish.yml`. Prepares for GitHub's June 2026 Node 20 deprecation.
|
|
18
|
+
- **Gemspec `source_code_uri` duplicate dropped** from both gemspecs (was equal to `homepage_uri` and triggered a RubyGems build warning on every release).
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
- README Compatibility table no longer lists Rails 5.2. The note explaining the drop and pinning instructions stays in place.
|
|
22
|
+
|
|
23
|
+
## 0.5.0
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **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.
|
|
27
|
+
- **`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"`.
|
|
28
|
+
- **`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}")`.
|
|
29
|
+
- 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`.
|
|
30
|
+
- 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.
|
|
31
|
+
- `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.
|
|
32
|
+
- `mysql_genius` now declares runtime dependency on `mysql_genius-core ~> 0.5.0` (was `~> 0.4.0`).
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- 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.
|
|
36
|
+
- `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/`).
|
|
37
|
+
|
|
38
|
+
### Internal
|
|
39
|
+
- `MysqlGenius::Core.views_path` — new public module method returning the absolute path to the shared ERB template directory.
|
|
40
|
+
|
|
3
41
|
## 0.4.1
|
|
4
42
|
|
|
5
43
|
### Fixed
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -134,7 +134,6 @@ This helps if the problem is a stale trust store, but does **not** help if the u
|
|
|
134
134
|
|
|
135
135
|
| Rails | Ruby |
|
|
136
136
|
|-------|------|
|
|
137
|
-
| 5.2 | 2.7, 3.0 |
|
|
138
137
|
| 6.0 | 2.7, 3.0, 3.1 |
|
|
139
138
|
| 6.1 | 2.7, 3.0, 3.1, 3.2, 3.3 |
|
|
140
139
|
| 7.0 | 2.7, 3.0, 3.1, 3.2, 3.3 |
|
|
@@ -143,6 +142,8 @@ This helps if the problem is a stale trust store, but does **not** help if the u
|
|
|
143
142
|
| 8.0 | 3.2, 3.3, 3.4 |
|
|
144
143
|
| 8.1 | 3.2, 3.3, 3.4 |
|
|
145
144
|
|
|
145
|
+
> **Rails 5.2:** dropped in `mysql_genius 0.6.0`. `mysql_genius 0.5.0` is the last version to support Rails 5.2 — pin it (`gem "mysql_genius", "~> 0.5.0"`) if you can't upgrade Rails yet. Rails 5.2 has been end-of-life since June 2022, and its incompatibilities with modern Rack (`ActionDispatch::Static#initialize` arity mismatch, `MiddlewareStack#operations` removal) surfaced as CI failures once Phase 2a's integration specs booted Rails in test.
|
|
146
|
+
|
|
146
147
|
## Documentation
|
|
147
148
|
|
|
148
149
|
Full documentation is available on the [Wiki](https://github.com/antarr/mysql_genius/wiki).
|
|
@@ -12,8 +12,7 @@ module MysqlGenius
|
|
|
12
12
|
prompt = params[:prompt].to_s.strip
|
|
13
13
|
return render(json: { error: "Please describe what you want to query." }, status: :unprocessable_entity) if prompt.blank?
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
service = MysqlGenius::Core::Ai::Suggestion.new(connection, ai_client, ai_config_for_core)
|
|
15
|
+
service = MysqlGenius::Core::Ai::Suggestion.new(rails_connection, ai_client, ai_config_for_core)
|
|
17
16
|
result = service.call(prompt, queryable_tables)
|
|
18
17
|
sql = sanitize_ai_sql(result["sql"].to_s)
|
|
19
18
|
render(json: { sql: sql, explanation: result["explanation"] })
|
|
@@ -33,8 +32,7 @@ module MysqlGenius
|
|
|
33
32
|
return render(json: { error: "SQL and EXPLAIN output are required." }, status: :unprocessable_entity)
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
service = MysqlGenius::Core::Ai::Optimization.new(connection, ai_client, ai_config_for_core)
|
|
35
|
+
service = MysqlGenius::Core::Ai::Optimization.new(rails_connection, ai_client, ai_config_for_core)
|
|
38
36
|
result = service.call(sql, explain_rows, queryable_tables)
|
|
39
37
|
render(json: result)
|
|
40
38
|
rescue StandardError => e
|
|
@@ -47,20 +45,7 @@ module MysqlGenius
|
|
|
47
45
|
sql = params[:sql].to_s.strip
|
|
48
46
|
return render(json: { error: "SQL is required." }, status: :unprocessable_entity) if sql.blank?
|
|
49
47
|
|
|
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)
|
|
48
|
+
result = MysqlGenius::Core::Ai::DescribeQuery.new(ai_client, ai_config_for_core).call(sql)
|
|
64
49
|
render(json: result)
|
|
65
50
|
rescue StandardError => e
|
|
66
51
|
render(json: { error: "Explanation failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -69,42 +54,7 @@ module MysqlGenius
|
|
|
69
54
|
def schema_review
|
|
70
55
|
return ai_not_configured unless mysql_genius_config.ai_enabled?
|
|
71
56
|
|
|
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)
|
|
57
|
+
result = MysqlGenius::Core::Ai::SchemaReview.new(ai_client, ai_config_for_core, rails_connection).call(params[:table].to_s.strip.presence)
|
|
108
58
|
render(json: result)
|
|
109
59
|
rescue StandardError => e
|
|
110
60
|
render(json: { error: "Schema review failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -116,31 +66,7 @@ module MysqlGenius
|
|
|
116
66
|
sql = params[:sql].to_s.strip
|
|
117
67
|
return render(json: { error: "SQL is required." }, status: :unprocessable_entity) if sql.blank?
|
|
118
68
|
|
|
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)
|
|
69
|
+
result = MysqlGenius::Core::Ai::RewriteQuery.new(ai_client, ai_config_for_core, rails_connection).call(sql)
|
|
144
70
|
render(json: result)
|
|
145
71
|
rescue StandardError => e
|
|
146
72
|
render(json: { error: "Rewrite failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -153,33 +79,7 @@ module MysqlGenius
|
|
|
153
79
|
explain_rows = Array(params[:explain_rows]).map { |row| row.respond_to?(:values) ? row.values : Array(row) }
|
|
154
80
|
return render(json: { error: "SQL and EXPLAIN output are required." }, status: :unprocessable_entity) if sql.blank? || explain_rows.blank?
|
|
155
81
|
|
|
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)
|
|
82
|
+
result = MysqlGenius::Core::Ai::IndexAdvisor.new(ai_client, ai_config_for_core, rails_connection).call(sql, explain_rows)
|
|
183
83
|
render(json: result)
|
|
184
84
|
rescue StandardError => e
|
|
185
85
|
render(json: { error: "Index advisor failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -223,6 +123,7 @@ module MysqlGenius
|
|
|
223
123
|
|
|
224
124
|
slow_summary = slow_data.first(50).map { |q| "#{q["duration_ms"]}ms @ #{q["timestamp"]}: #{q["sql"].to_s.truncate(150)}" }.join("\n")
|
|
225
125
|
stats_summary = stats.map { |q| "calls=#{q[:calls]} avg=#{q[:avg_ms]}ms total=#{q[:total_ms]}ms exam=#{q[:rows_examined]} sent=#{q[:rows_sent]}: #{q[:sql]}" }.join("\n")
|
|
126
|
+
domain_ctx = mysql_genius_config.ai_system_context.present? ? "\nDomain context:\n#{mysql_genius_config.ai_system_context}" : ""
|
|
226
127
|
|
|
227
128
|
messages = [
|
|
228
129
|
{ role: "system", content: <<~PROMPT },
|
|
@@ -232,7 +133,7 @@ module MysqlGenius
|
|
|
232
133
|
3. Full table scans (rows_examined >> rows_sent)
|
|
233
134
|
4. Sudden new query patterns that may indicate code changes
|
|
234
135
|
5. Queries creating excessive temp tables or sorts
|
|
235
|
-
#{
|
|
136
|
+
#{domain_ctx}
|
|
236
137
|
|
|
237
138
|
Respond with JSON: {"report": "markdown-formatted health report organized by severity. For each finding, explain the issue, affected query, and recommended fix."}
|
|
238
139
|
PROMPT
|
|
@@ -294,6 +195,8 @@ module MysqlGenius
|
|
|
294
195
|
slow_summary = slows.map { |q| "#{q["duration_ms"]}ms: #{q["sql"].to_s.truncate(150)}" }.join("\n")
|
|
295
196
|
end
|
|
296
197
|
|
|
198
|
+
domain_ctx = mysql_genius_config.ai_system_context.present? ? "\nDomain context:\n#{mysql_genius_config.ai_system_context}" : ""
|
|
199
|
+
|
|
297
200
|
messages = [
|
|
298
201
|
{ role: "system", content: <<~PROMPT },
|
|
299
202
|
You are a MySQL incident responder. The user is asking "why is the database slow right now?" Analyze the provided data and give a root cause diagnosis. Consider:
|
|
@@ -304,7 +207,7 @@ module MysqlGenius
|
|
|
304
207
|
- Disk I/O saturation
|
|
305
208
|
- Replication lag
|
|
306
209
|
- Unusual query patterns
|
|
307
|
-
#{
|
|
210
|
+
#{domain_ctx}
|
|
308
211
|
|
|
309
212
|
Respond with JSON: {"diagnosis": "markdown-formatted root cause analysis. Start with a 1-2 sentence summary, then detailed findings. Include specific actionable steps to resolve the issue."}
|
|
310
213
|
PROMPT
|
|
@@ -323,54 +226,7 @@ module MysqlGenius
|
|
|
323
226
|
migration_sql = params[:migration].to_s.strip
|
|
324
227
|
return render(json: { error: "Migration SQL or Ruby code is required." }, status: :unprocessable_entity) if migration_sql.blank?
|
|
325
228
|
|
|
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)
|
|
229
|
+
result = MysqlGenius::Core::Ai::MigrationRisk.new(ai_client, ai_config_for_core, rails_connection).call(migration_sql)
|
|
374
230
|
render(json: result)
|
|
375
231
|
rescue StandardError => e
|
|
376
232
|
render(json: { error: "Migration risk assessment failed: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -378,6 +234,10 @@ module MysqlGenius
|
|
|
378
234
|
|
|
379
235
|
private
|
|
380
236
|
|
|
237
|
+
RAILS_DOMAIN_CONTEXT = <<~CTX
|
|
238
|
+
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.
|
|
239
|
+
CTX
|
|
240
|
+
|
|
381
241
|
def ai_client
|
|
382
242
|
MysqlGenius::Core::Ai::Client.new(ai_config_for_core)
|
|
383
243
|
end
|
|
@@ -391,28 +251,12 @@ module MysqlGenius
|
|
|
391
251
|
model: cfg.ai_model,
|
|
392
252
|
auth_style: cfg.ai_auth_style,
|
|
393
253
|
system_context: cfg.ai_system_context,
|
|
254
|
+
domain_context: RAILS_DOMAIN_CONTEXT,
|
|
394
255
|
)
|
|
395
256
|
end
|
|
396
257
|
|
|
397
258
|
def ai_not_configured
|
|
398
259
|
render(json: { error: "AI features are not configured." }, status: :not_found)
|
|
399
260
|
end
|
|
400
|
-
|
|
401
|
-
def ai_domain_context
|
|
402
|
-
parts = []
|
|
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")
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
def build_schema_for_query(sql)
|
|
410
|
-
connection = ActiveRecord::Base.connection
|
|
411
|
-
tables = MysqlGenius::Core::SqlValidator.extract_table_references(sql, connection)
|
|
412
|
-
tables.map do |t|
|
|
413
|
-
cols = connection.columns(t).map { |c| "#{c.name} (#{c.type})" }
|
|
414
|
-
"#{t}: #{cols.join(", ")}"
|
|
415
|
-
end.join("\n")
|
|
416
|
-
end
|
|
417
261
|
end
|
|
418
262
|
end
|
|
@@ -5,40 +5,35 @@ module MysqlGenius
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
def duplicate_indexes
|
|
8
|
-
connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
|
|
9
8
|
duplicates = MysqlGenius::Core::Analysis::DuplicateIndexes
|
|
10
|
-
.new(
|
|
9
|
+
.new(rails_connection, blocked_tables: mysql_genius_config.blocked_tables)
|
|
11
10
|
.call
|
|
12
11
|
render(json: duplicates)
|
|
13
12
|
end
|
|
14
13
|
|
|
15
14
|
def table_sizes
|
|
16
|
-
|
|
17
|
-
tables = MysqlGenius::Core::Analysis::TableSizes.new(connection).call
|
|
15
|
+
tables = MysqlGenius::Core::Analysis::TableSizes.new(rails_connection).call
|
|
18
16
|
render(json: tables)
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
def query_stats
|
|
22
|
-
connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
|
|
23
20
|
sort = params[:sort].to_s
|
|
24
21
|
limit = params.fetch(:limit, MysqlGenius::Core::Analysis::QueryStats::MAX_LIMIT).to_i
|
|
25
|
-
queries = MysqlGenius::Core::Analysis::QueryStats.new(
|
|
22
|
+
queries = MysqlGenius::Core::Analysis::QueryStats.new(rails_connection).call(sort: sort, limit: limit)
|
|
26
23
|
render(json: queries)
|
|
27
24
|
rescue ActiveRecord::StatementInvalid => e
|
|
28
25
|
render(json: { error: "Query statistics require performance_schema to be enabled. #{e.message.split(":").last.strip}" }, status: :unprocessable_entity)
|
|
29
26
|
end
|
|
30
27
|
|
|
31
28
|
def unused_indexes
|
|
32
|
-
|
|
33
|
-
indexes = MysqlGenius::Core::Analysis::UnusedIndexes.new(connection).call
|
|
29
|
+
indexes = MysqlGenius::Core::Analysis::UnusedIndexes.new(rails_connection).call
|
|
34
30
|
render(json: indexes)
|
|
35
31
|
rescue ActiveRecord::StatementInvalid => e
|
|
36
32
|
render(json: { error: "Unused index detection requires performance_schema. #{e.message.split(":").last.strip}" }, status: :unprocessable_entity)
|
|
37
33
|
end
|
|
38
34
|
|
|
39
35
|
def server_overview
|
|
40
|
-
|
|
41
|
-
overview = MysqlGenius::Core::Analysis::ServerOverview.new(connection).call
|
|
36
|
+
overview = MysqlGenius::Core::Analysis::ServerOverview.new(rails_connection).call
|
|
42
37
|
render(json: overview)
|
|
43
38
|
rescue => e
|
|
44
39
|
render(json: { error: "Failed to load server overview: #{e.message}" }, status: :unprocessable_entity)
|
|
@@ -12,13 +12,12 @@ module MysqlGenius
|
|
|
12
12
|
mysql_genius_config.default_row_limit
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
|
|
16
15
|
runner_config = MysqlGenius::Core::QueryRunner::Config.new(
|
|
17
16
|
blocked_tables: mysql_genius_config.blocked_tables,
|
|
18
17
|
masked_column_patterns: mysql_genius_config.masked_column_patterns,
|
|
19
18
|
query_timeout_ms: mysql_genius_config.query_timeout_ms,
|
|
20
19
|
)
|
|
21
|
-
runner = MysqlGenius::Core::QueryRunner.new(
|
|
20
|
+
runner = MysqlGenius::Core::QueryRunner.new(rails_connection, runner_config)
|
|
22
21
|
|
|
23
22
|
begin
|
|
24
23
|
result = runner.run(sql, row_limit: row_limit)
|
|
@@ -48,13 +47,12 @@ module MysqlGenius
|
|
|
48
47
|
sql = params[:sql].to_s.strip
|
|
49
48
|
skip_validation = params[:from_slow_query] == "true"
|
|
50
49
|
|
|
51
|
-
connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
|
|
52
50
|
runner_config = MysqlGenius::Core::QueryRunner::Config.new(
|
|
53
51
|
blocked_tables: mysql_genius_config.blocked_tables,
|
|
54
52
|
masked_column_patterns: mysql_genius_config.masked_column_patterns,
|
|
55
53
|
query_timeout_ms: mysql_genius_config.query_timeout_ms,
|
|
56
54
|
)
|
|
57
|
-
explainer = MysqlGenius::Core::QueryExplainer.new(
|
|
55
|
+
explainer = MysqlGenius::Core::QueryExplainer.new(rails_connection, runner_config)
|
|
58
56
|
|
|
59
57
|
result = explainer.explain(sql, skip_validation: skip_validation)
|
|
60
58
|
render(json: { columns: result.columns, rows: result.rows })
|
|
@@ -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
|
|
@@ -16,5 +16,14 @@ module MysqlGenius
|
|
|
16
16
|
def mysql_genius_config
|
|
17
17
|
MysqlGenius.configuration
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
# Wraps ActiveRecord::Base.connection in a Core::Connection::ActiveRecordAdapter.
|
|
21
|
+
# Every controller action that delegates to a Core::* service calls this
|
|
22
|
+
# instead of instantiating the adapter inline. Shared across all concerns
|
|
23
|
+
# (QueryExecution, DatabaseAnalysis, AiFeatures) via BaseController's
|
|
24
|
+
# private method lookup.
|
|
25
|
+
def rails_connection
|
|
26
|
+
MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
|
|
27
|
+
end
|
|
19
28
|
end
|
|
20
29
|
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
|
-
|
|
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])
|
|
30
|
+
|
|
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)
|
|
23
35
|
end
|
|
24
|
-
|
|
25
|
-
unless ActiveRecord::Base.connection.tables.include?(table)
|
|
26
|
-
return render(json: { error: "Table '#{table}' does not exist." }, status: :not_found)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
defaults = mysql_genius_config.default_columns[table] || []
|
|
30
|
-
cols = ActiveRecord::Base.connection.columns(table).reject { |c| masked_column?(c.name) }.map do |c|
|
|
31
|
-
{ name: c.name, type: c.type.to_s, default: defaults.empty? || defaults.include?(c.name) }
|
|
32
|
-
end
|
|
33
|
-
render(json: cols)
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def slow_queries
|
|
@@ -57,15 +59,5 @@ module MysqlGenius
|
|
|
57
59
|
def queryable_tables
|
|
58
60
|
ActiveRecord::Base.connection.tables - mysql_genius_config.blocked_tables
|
|
59
61
|
end
|
|
60
|
-
|
|
61
|
-
# Delegates to Core::SqlValidator's 2-arg class method. A bare
|
|
62
|
-
# `masked_column?(name)` call survives on line 30 because this helper
|
|
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)
|
|
69
|
-
end
|
|
70
62
|
end
|
|
71
63
|
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
|
@@ -20,7 +20,6 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.required_ruby_version = ">= 2.6.0"
|
|
21
21
|
|
|
22
22
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
23
|
-
spec.metadata["source_code_uri"] = spec.homepage
|
|
24
23
|
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
25
24
|
|
|
26
25
|
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
|
@@ -30,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
|
30
29
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
31
30
|
spec.require_paths = ["lib"]
|
|
32
31
|
|
|
33
|
-
spec.add_dependency("activerecord", ">=
|
|
34
|
-
spec.add_dependency("mysql_genius-core", "~> 0.
|
|
35
|
-
spec.add_dependency("railties", ">=
|
|
32
|
+
spec.add_dependency("activerecord", ">= 6.0", "< 9")
|
|
33
|
+
spec.add_dependency("mysql_genius-core", "~> 0.6.0")
|
|
34
|
+
spec.add_dependency("railties", ">= 6.0", "< 9")
|
|
36
35
|
end
|