Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,8 @@ jobs:
fail-fast: false
matrix:
ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"]
rails: ["5.2", "6.0", "6.1", "7.0", "7.1", "7.2", "8.0", "8.1"]
rails: ["6.0", "6.1", "7.0", "7.1", "7.2", "8.0", "8.1"]
exclude:
# Rails 5.2 doesn't support Ruby 3.1+
- ruby: "3.1"
rails: "5.2"
- ruby: "3.2"
rails: "5.2"
- ruby: "3.3"
rails: "5.2"
- ruby: "3.4"
rails: "5.2"
# Rails 5.2 + Ruby 3.0: Rails 5.2's ActionDispatch::Static#initialize
# signature is incompatible with modern Rack (Rack 3.x changed arity),
# and MiddlewareStack#operations was also renamed. Phase 2a's
# integration specs boot Rails, which surfaces the bit-rot. Both
# Rails 5.2 (EOL 2022-06) and Ruby 3.0 (EOL 2024-03) are past EOL.
- ruby: "3.0"
rails: "5.2"
# Rails 6.0 doesn't support Ruby 3.2+
- ruby: "3.2"
rails: "6.0"
Expand Down Expand Up @@ -68,7 +52,7 @@ jobs:
RAILS_VERSION: ${{ matrix.rails }}

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Set up Ruby ${{ matrix.ruby }}
uses: ruby/setup-ruby@v1
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
Expand All @@ -25,7 +25,7 @@ jobs:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## 0.6.0

### Changed
- **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.
- `mysql_genius` now declares runtime dependency on `mysql_genius-core ~> 0.6.0` (was `~> 0.5.0`).

### Fixed
- **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.

### Internal
- **`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.
- **`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.
- **`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.
- **`Core::Analysis::Columns` spec** covers the `default: false` branch (6th column outside `default_columns`).
- **`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.
- **Gemspec `source_code_uri` duplicate dropped** from both gemspecs (was equal to `homepage_uri` and triggered a RubyGems build warning on every release).

### Documentation
- README Compatibility table no longer lists Rails 5.2. The note explaining the drop and pinning instructions stays in place.

## 0.5.0

### Changed
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ This helps if the problem is a stale trust store, but does **not** help if the u

| Rails | Ruby |
|-------|------|
| 5.2 | 2.7 |
| 6.0 | 2.7, 3.0, 3.1 |
| 6.1 | 2.7, 3.0, 3.1, 3.2, 3.3 |
| 7.0 | 2.7, 3.0, 3.1, 3.2, 3.3 |
Expand All @@ -143,7 +142,7 @@ This helps if the problem is a stale trust store, but does **not** help if the u
| 8.0 | 3.2, 3.3, 3.4 |
| 8.1 | 3.2, 3.3, 3.4 |

> **Rails 5.2 note:** `mysql_genius 0.5.0` is the last version to support Rails 5.2. Rails 5.2 has been end-of-life since June 2022, and 0.5.0's integration test suite exposed Rails 5.2's incompatibilities with modern Rack (`ActionDispatch::Static#initialize` arity mismatch, `MiddlewareStack#operations` removal) on every Ruby 3.x combination. Rails 5.2 is tested only against Ruby 2.7 (also EOL) going forward. The next minor version (0.6.0 or later) will raise the floor to Rails 6.0+.
> **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.

## Documentation

Expand Down
23 changes: 7 additions & 16 deletions app/controllers/concerns/mysql_genius/ai_features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ def suggest
prompt = params[:prompt].to_s.strip
return render(json: { error: "Please describe what you want to query." }, status: :unprocessable_entity) if prompt.blank?

connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
service = MysqlGenius::Core::Ai::Suggestion.new(connection, ai_client, ai_config_for_core)
service = MysqlGenius::Core::Ai::Suggestion.new(rails_connection, ai_client, ai_config_for_core)
result = service.call(prompt, queryable_tables)
sql = sanitize_ai_sql(result["sql"].to_s)
render(json: { sql: sql, explanation: result["explanation"] })
Expand All @@ -33,8 +32,7 @@ def optimize
return render(json: { error: "SQL and EXPLAIN output are required." }, status: :unprocessable_entity)
end

connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
service = MysqlGenius::Core::Ai::Optimization.new(connection, ai_client, ai_config_for_core)
service = MysqlGenius::Core::Ai::Optimization.new(rails_connection, ai_client, ai_config_for_core)
result = service.call(sql, explain_rows, queryable_tables)
render(json: result)
rescue StandardError => e
Expand Down Expand Up @@ -125,6 +123,7 @@ def anomaly_detection

slow_summary = slow_data.first(50).map { |q| "#{q["duration_ms"]}ms @ #{q["timestamp"]}: #{q["sql"].to_s.truncate(150)}" }.join("\n")
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")
domain_ctx = mysql_genius_config.ai_system_context.present? ? "\nDomain context:\n#{mysql_genius_config.ai_system_context}" : ""

messages = [
{ role: "system", content: <<~PROMPT },
Expand All @@ -134,7 +133,7 @@ def anomaly_detection
3. Full table scans (rows_examined >> rows_sent)
4. Sudden new query patterns that may indicate code changes
5. Queries creating excessive temp tables or sorts
#{ai_domain_context}
#{domain_ctx}

Respond with JSON: {"report": "markdown-formatted health report organized by severity. For each finding, explain the issue, affected query, and recommended fix."}
PROMPT
Expand Down Expand Up @@ -196,6 +195,8 @@ def root_cause
slow_summary = slows.map { |q| "#{q["duration_ms"]}ms: #{q["sql"].to_s.truncate(150)}" }.join("\n")
end

domain_ctx = mysql_genius_config.ai_system_context.present? ? "\nDomain context:\n#{mysql_genius_config.ai_system_context}" : ""

messages = [
{ role: "system", content: <<~PROMPT },
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:
Expand All @@ -206,7 +207,7 @@ def root_cause
- Disk I/O saturation
- Replication lag
- Unusual query patterns
#{ai_domain_context}
#{domain_ctx}

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."}
PROMPT
Expand Down Expand Up @@ -257,15 +258,5 @@ def ai_config_for_core
def ai_not_configured
render(json: { error: "AI features are not configured." }, status: :not_found)
end

def rails_connection
MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
end

def ai_domain_context
cfg = mysql_genius_config
ctx = cfg.ai_system_context
ctx.present? ? "\nDomain context:\n#{ctx}" : ""
end
end
end
15 changes: 5 additions & 10 deletions app/controllers/concerns/mysql_genius/database_analysis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,35 @@ module DatabaseAnalysis
extend ActiveSupport::Concern

def duplicate_indexes
connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
duplicates = MysqlGenius::Core::Analysis::DuplicateIndexes
.new(connection, blocked_tables: mysql_genius_config.blocked_tables)
.new(rails_connection, blocked_tables: mysql_genius_config.blocked_tables)
.call
render(json: duplicates)
end

def table_sizes
connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
tables = MysqlGenius::Core::Analysis::TableSizes.new(connection).call
tables = MysqlGenius::Core::Analysis::TableSizes.new(rails_connection).call
render(json: tables)
end

def query_stats
connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
sort = params[:sort].to_s
limit = params.fetch(:limit, MysqlGenius::Core::Analysis::QueryStats::MAX_LIMIT).to_i
queries = MysqlGenius::Core::Analysis::QueryStats.new(connection).call(sort: sort, limit: limit)
queries = MysqlGenius::Core::Analysis::QueryStats.new(rails_connection).call(sort: sort, limit: limit)
render(json: queries)
rescue ActiveRecord::StatementInvalid => e
render(json: { error: "Query statistics require performance_schema to be enabled. #{e.message.split(":").last.strip}" }, status: :unprocessable_entity)
end

def unused_indexes
connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
indexes = MysqlGenius::Core::Analysis::UnusedIndexes.new(connection).call
indexes = MysqlGenius::Core::Analysis::UnusedIndexes.new(rails_connection).call
render(json: indexes)
rescue ActiveRecord::StatementInvalid => e
render(json: { error: "Unused index detection requires performance_schema. #{e.message.split(":").last.strip}" }, status: :unprocessable_entity)
end

def server_overview
connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
overview = MysqlGenius::Core::Analysis::ServerOverview.new(connection).call
overview = MysqlGenius::Core::Analysis::ServerOverview.new(rails_connection).call
render(json: overview)
rescue => e
render(json: { error: "Failed to load server overview: #{e.message}" }, status: :unprocessable_entity)
Expand Down
6 changes: 2 additions & 4 deletions app/controllers/concerns/mysql_genius/query_execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ def execute
mysql_genius_config.default_row_limit
end

connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
runner_config = MysqlGenius::Core::QueryRunner::Config.new(
blocked_tables: mysql_genius_config.blocked_tables,
masked_column_patterns: mysql_genius_config.masked_column_patterns,
query_timeout_ms: mysql_genius_config.query_timeout_ms,
)
runner = MysqlGenius::Core::QueryRunner.new(connection, runner_config)
runner = MysqlGenius::Core::QueryRunner.new(rails_connection, runner_config)

begin
result = runner.run(sql, row_limit: row_limit)
Expand Down Expand Up @@ -48,13 +47,12 @@ def explain
sql = params[:sql].to_s.strip
skip_validation = params[:from_slow_query] == "true"

connection = MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
runner_config = MysqlGenius::Core::QueryRunner::Config.new(
blocked_tables: mysql_genius_config.blocked_tables,
masked_column_patterns: mysql_genius_config.masked_column_patterns,
query_timeout_ms: mysql_genius_config.query_timeout_ms,
)
explainer = MysqlGenius::Core::QueryExplainer.new(connection, runner_config)
explainer = MysqlGenius::Core::QueryExplainer.new(rails_connection, runner_config)

result = explainer.explain(sql, skip_validation: skip_validation)
render(json: { columns: result.columns, rows: result.rows })
Expand Down
9 changes: 9 additions & 0 deletions app/controllers/mysql_genius/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,14 @@ def authenticate_mysql_genius!
def mysql_genius_config
MysqlGenius.configuration
end

# Wraps ActiveRecord::Base.connection in a Core::Connection::ActiveRecordAdapter.
# Every controller action that delegates to a Core::* service calls this
# instead of instantiating the adapter inline. Shared across all concerns
# (QueryExecution, DatabaseAnalysis, AiFeatures) via BaseController's
# private method lookup.
def rails_connection
MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
end
end
end
4 changes: 0 additions & 4 deletions app/controllers/mysql_genius/queries_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,5 @@ def slow_queries
def queryable_tables
ActiveRecord::Base.connection.tables - mysql_genius_config.blocked_tables
end

def rails_connection
MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection)
end
end
end
4 changes: 4 additions & 0 deletions gems/mysql_genius-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.6.0

No functional changes in `mysql_genius-core`. Version bumped to maintain lockstep with `mysql_genius 0.6.0`, which drops Rails 5.2 support from the Rails adapter. See the root `CHANGELOG.md` for the full change list.

## 0.5.0

### Added
Expand Down
2 changes: 1 addition & 1 deletion gems/mysql_genius-core/lib/mysql_genius/core/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module MysqlGenius
module Core
VERSION = "0.5.0"
VERSION = "0.6.0"
end
end
1 change: 0 additions & 1 deletion gems/mysql_genius-core/mysql_genius-core.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = ">= 2.7.0"

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/gems/mysql_genius-core/CHANGELOG.md"

spec.files = Dir.chdir(__dir__) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
MysqlGenius::Core::ColumnDefinition.new(name: "password_hash", sql_type: "varchar(255)", type: :string, null: false, default: nil, primary_key: false),
MysqlGenius::Core::ColumnDefinition.new(name: "api_token", sql_type: "varchar(64)", type: :string, null: true, default: nil, primary_key: false),
MysqlGenius::Core::ColumnDefinition.new(name: "created_at", sql_type: "datetime", type: :datetime, null: false, default: nil, primary_key: false),
MysqlGenius::Core::ColumnDefinition.new(name: "updated_at", sql_type: "datetime", type: :datetime, null: false, default: nil, primary_key: false),
]
end

Expand Down Expand Up @@ -56,6 +57,15 @@
expect(by_name["created_at"][:default]).to(be(true))
end

it "marks columns outside default_columns with default: false" do
# updated_at is visible (not masked) but not in default_columns — it
# should come back with default: false. Pins the `defaults.empty? ||
# defaults.include?(col.name)` branch where neither side is true.
result = service.call(table: "users")
by_name = result.columns.to_h { |c| [c[:name], c] }
expect(by_name["updated_at"][:default]).to(be(false))
end

it "when default_columns has no entry for the table, marks ALL as default: true" do
service_without_defaults = described_class.new(
connection,
Expand Down
2 changes: 1 addition & 1 deletion lib/mysql_genius/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module MysqlGenius
VERSION = "0.5.0"
VERSION = "0.6.0"
end
7 changes: 3 additions & 4 deletions mysql_genius.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = ">= 2.6.0"

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"

spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
Expand All @@ -30,7 +29,7 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency("activerecord", ">= 5.2", "< 9")
spec.add_dependency("mysql_genius-core", "~> 0.5.0")
spec.add_dependency("railties", ">= 5.2", "< 9")
spec.add_dependency("activerecord", ">= 6.0", "< 9")
spec.add_dependency("mysql_genius-core", "~> 0.6.0")
spec.add_dependency("railties", ">= 6.0", "< 9")
end
9 changes: 3 additions & 6 deletions spec/requests/mysql_genius/ai_features_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@

before do
stub_connection(tables: ["users"])
empty_result = instance_double(
"ActiveRecord::Result",
columns: [],
rows: [],
to_a: [],
)
empty_result = fake_result
# root_cause action iterates exec_query results with .each — stub it here
# since fake_result doesn't include .each by default.
allow(empty_result).to(receive(:each).and_yield({}))
allow(ActiveRecord::Base.connection).to(receive_messages(exec_query: empty_result, select_value: "8.0.35"))
allow(ActiveRecord::Base.connection).to(receive(:columns).with(anything).and_return([]))
Expand Down
9 changes: 1 addition & 8 deletions spec/requests/mysql_genius/analysis_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@
# is unit-tested in gems/mysql_genius-core/spec/.../analysis/*_spec.rb;
# these request specs only verify routing/dispatch/JSON serialization.
stub_connection(tables: ["users"])

empty_result = instance_double(
"ActiveRecord::Result",
columns: [],
rows: [],
to_a: [],
)
allow(ActiveRecord::Base.connection).to(receive_messages(exec_query: empty_result, select_value: "8.0.35"))
allow(ActiveRecord::Base.connection).to(receive_messages(exec_query: fake_result, select_value: "8.0.35"))
end

it "GET /mysql_genius/duplicate_indexes returns 200 + JSON array" do
Expand Down
Loading