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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7858ff8adbac9ab758eee1f82b0302f67cc09357ad087f3a3102a6646307f9d
4
- data.tar.gz: 72053d61ff33d5fbf986a7a60445e36ae821ec9c2368ef4c9a0e4768636cb6ec
3
+ metadata.gz: a35fb751622eaa5cd7bc6fad555020fb5dbf2a9a2f50d50d27ee9b3da6440818
4
+ data.tar.gz: 1423b66748df1f05b0a9e9ff3868d35905953218443823e9cf2b0a554e88c4db
5
5
  SHA512:
6
- metadata.gz: 8dd7d033566d1ba8112253768dd491a5083f2daf57f2eb96231f668f229bac16572d97735d86a873efe6d9b44b7fdc649a31647ba10470c877a5c353dd0ebf53
7
- data.tar.gz: b25231d00d367295acc3c84e612bd4345fc3a954a48aa7b93a17c8fec50bb5804936e9605b1663971566542626c93f65fc49522900e36f667ff6d6073143d11c
6
+ metadata.gz: 8fbd465309eb1068b2c78cc47e56e93ac98badd97f8df834a306cbda5c114f903f410c5a7e927c6e9079556d2a240e44521290a28b959e9af91805f13b1fe22d
7
+ data.tar.gz: 2f3fe565bd970da7f1a1ac0f331bc435a59cc5508b1094fdc91003ef01f5fd3fb7782618cc7d55e24e5aec243f597895b5d4bd1238e90b150d5d92ba2cf611ef
@@ -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: ["5.2", "6.0", "6.1", "7.0", "7.1", "7.2", "8.0", "8.1"]
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@v4
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@v4
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@v4
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
@@ -17,6 +17,9 @@ RSpec/MessageSpies:
17
17
  RSpec/VerifiedDoubles:
18
18
  Enabled: false
19
19
 
20
+ RSpec/VerifiedDoubleReference:
21
+ Enabled: false
22
+
20
23
  RSpec/ExampleLength:
21
24
  Max: 25
22
25
 
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
@@ -16,6 +16,8 @@ end
16
16
  group :development, :test do
17
17
  gem "rake"
18
18
  gem "rspec", "~> 3.0"
19
+ gem "rspec-rails"
20
+ gem "rack-test"
19
21
  gem "rubocop"
20
22
  gem "rubocop-shopify"
21
23
  gem "rubocop-rspec"
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
- connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
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
- connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
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
- messages = [
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
- table = params[:table].to_s.strip
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
- schema = build_schema_for_query(sql)
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
- connection = ActiveRecord::Base.connection
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
- #{ai_domain_context}
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
- #{ai_domain_context}
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
- connection = ActiveRecord::Base.connection
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(connection, blocked_tables: mysql_genius_config.blocked_tables)
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
- connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
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(connection).call(sort: sort, limit: limit)
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
- connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
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
- connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
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(connection, runner_config)
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(connection, runner_config)
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
- table = params[:table]
21
- if mysql_genius_config.blocked_tables.include?(table)
22
- return render(json: { error: "Table '#{table}' is not available for querying." }, status: :forbidden)
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
@@ -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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MysqlGenius
4
- VERSION = "0.4.1"
4
+ VERSION = "0.6.0"
5
5
  end
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", ">= 5.2", "< 9")
34
- spec.add_dependency("mysql_genius-core", "~> 0.4.0")
35
- spec.add_dependency("railties", ">= 5.2", "< 9")
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