mysql_genius 0.6.0 → 0.7.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 +23 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +12 -0
- data/Makefile +39 -0
- data/app/controllers/concerns/mysql_genius/shared_view_helpers.rb +20 -2
- data/app/controllers/mysql_genius/queries_controller.rb +70 -0
- data/config/routes.rb +2 -0
- data/lib/mysql_genius/configuration.rb +7 -0
- data/lib/mysql_genius/engine.rb +13 -0
- data/lib/mysql_genius/version.rb +1 -1
- data/lib/mysql_genius.rb +3 -0
- data/mysql_genius.gemspec +1 -1
- data/ralph/prd.json +174 -0
- data/ralph/progress.txt +141 -0
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4cec94802aff95468c29e6a4612651afa8c31ad134fd67fb44747487f94b02b0
|
|
4
|
+
data.tar.gz: 5a51de5742dc12032509e115ccc3dfc891f40d72ffb73e2ae048b58e99094289
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd85efe1037646a105930e8d02b29de18b77a5cc1b410dfa7252674ecdc2e1f8513ec4d1c0228a4f60530d0d623c667bdf83153218313929854961e8440f8eac
|
|
7
|
+
data.tar.gz: 4ae937b2f2eaad1baf08a9c5010a7fcaa76f87f5de83051dd025d2b78421e00be4c58b51996498175b041e218827f7c76bd123dc82352f3654af23b9c716439a
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -65,3 +65,26 @@ jobs:
|
|
|
65
65
|
|
|
66
66
|
- name: Run specs
|
|
67
67
|
run: bundle exec rspec
|
|
68
|
+
|
|
69
|
+
desktop:
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v5
|
|
73
|
+
|
|
74
|
+
- name: Set up Ruby 3.2
|
|
75
|
+
uses: ruby/setup-ruby@v1
|
|
76
|
+
with:
|
|
77
|
+
ruby-version: "3.2"
|
|
78
|
+
bundler-cache: false
|
|
79
|
+
|
|
80
|
+
- name: Install desktop gem dependencies
|
|
81
|
+
working-directory: gems/mysql_genius-desktop
|
|
82
|
+
run: bundle install
|
|
83
|
+
|
|
84
|
+
- name: Run desktop specs
|
|
85
|
+
working-directory: gems/mysql_genius-desktop
|
|
86
|
+
run: bundle exec rspec
|
|
87
|
+
|
|
88
|
+
- name: Run desktop rubocop
|
|
89
|
+
working-directory: gems/mysql_genius-desktop
|
|
90
|
+
run: bundle exec rubocop
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`capability?(name)` helper** in `SharedViewHelpers`. The Rails adapter returns `true` for all capabilities; the desktop sidecar uses it to hide Redis-backed features (slow_queries, anomaly_detection, root_cause) from the shared dashboard templates.
|
|
7
|
+
- **Query detail page** at `GET /queries/:digest` with syntax-highlighted SQL, Explain button, aggregate stats cards, and three inline SVG time-series charts (Total Time, Average Time, Calls).
|
|
8
|
+
- **`Core::Analysis::StatsHistory`** — thread-safe in-memory ring buffer for per-digest query stats snapshots (24hr retention at 60s intervals).
|
|
9
|
+
- **`Core::Analysis::StatsCollector`** — background thread that samples `performance_schema.events_statements_summary_by_digest` every 60s, computes deltas, and records to StatsHistory.
|
|
10
|
+
- **`DIGEST` hash** added to `Core::Analysis::QueryStats` return value for stable URL keys.
|
|
11
|
+
- **Stats collection config option** (`stats_collection`, default `true`) — controls whether the background collector starts on boot.
|
|
12
|
+
- **Query Stats tab linkification** — SQL cells in the Query Stats table are now clickable links to the query detail page.
|
|
13
|
+
- `mysql_genius` now declares runtime dependency on `mysql_genius-core ~> 0.7.0`.
|
|
14
|
+
|
|
3
15
|
## 0.6.0
|
|
4
16
|
|
|
5
17
|
### Changed
|
data/Makefile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.PHONY: test test-rails test-core test-desktop lint lint-rails lint-core lint-desktop setup sidecar
|
|
2
|
+
|
|
3
|
+
# Run all test suites
|
|
4
|
+
test: test-rails test-core test-desktop
|
|
5
|
+
|
|
6
|
+
test-rails:
|
|
7
|
+
bundle exec rspec
|
|
8
|
+
|
|
9
|
+
test-core:
|
|
10
|
+
cd gems/mysql_genius-core && bundle exec rspec
|
|
11
|
+
|
|
12
|
+
test-desktop:
|
|
13
|
+
cd gems/mysql_genius-desktop && bundle exec rspec
|
|
14
|
+
|
|
15
|
+
# Run all linters
|
|
16
|
+
lint: lint-rails lint-core lint-desktop
|
|
17
|
+
|
|
18
|
+
lint-rails:
|
|
19
|
+
bundle exec rubocop
|
|
20
|
+
|
|
21
|
+
lint-core:
|
|
22
|
+
cd gems/mysql_genius-core && bundle exec rubocop
|
|
23
|
+
|
|
24
|
+
lint-desktop:
|
|
25
|
+
cd gems/mysql_genius-desktop && bundle exec rubocop
|
|
26
|
+
|
|
27
|
+
# Run everything (tests + lint)
|
|
28
|
+
check: test lint
|
|
29
|
+
|
|
30
|
+
# Install dependencies for all gems
|
|
31
|
+
setup:
|
|
32
|
+
bundle install
|
|
33
|
+
cd gems/mysql_genius-core && bundle install
|
|
34
|
+
cd gems/mysql_genius-desktop && bundle install
|
|
35
|
+
|
|
36
|
+
# Start the desktop sidecar
|
|
37
|
+
# Usage: make sidecar CONFIG=/path/to/mg.yml
|
|
38
|
+
sidecar:
|
|
39
|
+
cd gems/mysql_genius-desktop && bundle exec exe/mysql-genius-sidecar $(if $(CONFIG),--config $(CONFIG),)
|
|
@@ -5,13 +5,21 @@ module MysqlGenius
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
included do
|
|
8
|
-
helper_method :path_for, :render_partial
|
|
8
|
+
helper_method :path_for, :render_partial, :capability?
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
# URL path helper for shared templates.
|
|
12
12
|
# path_for(:execute) # => "/mysql_genius/execute" (from engine route helpers)
|
|
13
|
+
#
|
|
14
|
+
# When @digest is set (query detail page), routes that require a digest
|
|
15
|
+
# param (query_detail, query_history) are generated with it automatically.
|
|
13
16
|
def path_for(name)
|
|
14
|
-
|
|
17
|
+
digest_routes = [:query_detail, :query_history]
|
|
18
|
+
if digest_routes.include?(name) && @digest
|
|
19
|
+
mysql_genius.public_send("#{name}_path", digest: @digest)
|
|
20
|
+
else
|
|
21
|
+
mysql_genius.public_send("#{name}_path")
|
|
22
|
+
end
|
|
15
23
|
end
|
|
16
24
|
|
|
17
25
|
# Partial renderer for shared templates.
|
|
@@ -19,5 +27,15 @@ module MysqlGenius
|
|
|
19
27
|
def render_partial(name)
|
|
20
28
|
view_context.render(partial: "mysql_genius/queries/#{name}")
|
|
21
29
|
end
|
|
30
|
+
|
|
31
|
+
# Capability flag for shared templates. The Rails adapter always
|
|
32
|
+
# reports every capability as present because it owns all routes
|
|
33
|
+
# (including the Redis-backed slow_queries / anomaly_detection /
|
|
34
|
+
# root_cause features). The Phase 2b sidecar overrides this with a
|
|
35
|
+
# narrower list in its own Sinatra app, which is why shared templates
|
|
36
|
+
# gate the associated UI via `<% if capability?(:slow_queries) %>` etc.
|
|
37
|
+
def capability?(_name)
|
|
38
|
+
true
|
|
39
|
+
end
|
|
22
40
|
end
|
|
23
41
|
end
|
|
@@ -35,6 +35,27 @@ module MysqlGenius
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def query_detail
|
|
39
|
+
@digest = params[:digest].to_s
|
|
40
|
+
render("mysql_genius/queries/query_detail")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def query_history
|
|
44
|
+
digest = params[:digest].to_s
|
|
45
|
+
db = begin
|
|
46
|
+
ActiveRecord::Base.connection.current_database
|
|
47
|
+
rescue
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
current_query = fetch_query_history_current(digest, db)
|
|
52
|
+
history = fetch_query_history_series(digest)
|
|
53
|
+
|
|
54
|
+
render(json: { query: current_query, history: history })
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
render(json: { error: e.message }, status: :unprocessable_entity)
|
|
57
|
+
end
|
|
58
|
+
|
|
38
59
|
def slow_queries
|
|
39
60
|
unless mysql_genius_config.redis_url.present?
|
|
40
61
|
return render(json: [], status: :ok)
|
|
@@ -59,5 +80,54 @@ module MysqlGenius
|
|
|
59
80
|
def queryable_tables
|
|
60
81
|
ActiveRecord::Base.connection.tables - mysql_genius_config.blocked_tables
|
|
61
82
|
end
|
|
83
|
+
|
|
84
|
+
def fetch_query_history_current(digest, db)
|
|
85
|
+
sql = <<~SQL.squish
|
|
86
|
+
SELECT DIGEST_TEXT, COUNT_STAR AS calls,
|
|
87
|
+
ROUND(SUM_TIMER_WAIT / 1000000000.0, 2) AS total_time_ms,
|
|
88
|
+
ROUND(AVG_TIMER_WAIT / 1000000000.0, 2) AS avg_time_ms,
|
|
89
|
+
ROUND(MAX_TIMER_WAIT / 1000000000.0, 2) AS max_time_ms,
|
|
90
|
+
SUM_ROWS_EXAMINED AS rows_examined,
|
|
91
|
+
SUM_ROWS_SENT AS rows_sent,
|
|
92
|
+
FIRST_SEEN, LAST_SEEN
|
|
93
|
+
FROM performance_schema.events_statements_summary_by_digest
|
|
94
|
+
WHERE DIGEST = '#{digest.gsub("'", "''")}'
|
|
95
|
+
#{"AND SCHEMA_NAME = '#{db.to_s.gsub("'", "''")}'" if db}
|
|
96
|
+
LIMIT 1
|
|
97
|
+
SQL
|
|
98
|
+
result = ActiveRecord::Base.connection.exec_query(sql)
|
|
99
|
+
return if result.rows.empty?
|
|
100
|
+
|
|
101
|
+
row = result.to_a.first
|
|
102
|
+
{
|
|
103
|
+
sql: row["DIGEST_TEXT"],
|
|
104
|
+
calls: row["calls"],
|
|
105
|
+
total_time_ms: row["total_time_ms"].to_f,
|
|
106
|
+
avg_time_ms: row["avg_time_ms"].to_f,
|
|
107
|
+
max_time_ms: row["max_time_ms"].to_f,
|
|
108
|
+
rows_examined: row["rows_examined"],
|
|
109
|
+
rows_sent: row["rows_sent"],
|
|
110
|
+
first_seen: row["FIRST_SEEN"].to_s,
|
|
111
|
+
last_seen: row["LAST_SEEN"].to_s,
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def fetch_query_history_series(digest)
|
|
116
|
+
return [] unless MysqlGenius.stats_history
|
|
117
|
+
|
|
118
|
+
digest_text = lookup_digest_text(digest)
|
|
119
|
+
return [] unless digest_text
|
|
120
|
+
|
|
121
|
+
MysqlGenius.stats_history.series_for(digest_text)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def lookup_digest_text(digest)
|
|
125
|
+
sql = <<~SQL.squish
|
|
126
|
+
SELECT DIGEST_TEXT FROM performance_schema.events_statements_summary_by_digest
|
|
127
|
+
WHERE DIGEST = '#{digest.gsub("'", "''")}' LIMIT 1
|
|
128
|
+
SQL
|
|
129
|
+
result = ActiveRecord::Base.connection.exec_query(sql)
|
|
130
|
+
result.rows.empty? ? nil : result.to_a.first["DIGEST_TEXT"]
|
|
131
|
+
end
|
|
62
132
|
end
|
|
63
133
|
end
|
data/config/routes.rb
CHANGED
|
@@ -6,6 +6,8 @@ MysqlGenius::Engine.routes.draw do
|
|
|
6
6
|
get "columns", to: "queries#columns"
|
|
7
7
|
post "execute", to: "queries#execute"
|
|
8
8
|
post "explain", to: "queries#explain"
|
|
9
|
+
get "queries/:digest", to: "queries#query_detail", as: "query_detail"
|
|
10
|
+
get "api/query_history/:digest", to: "queries#query_history", as: "query_history"
|
|
9
11
|
post "suggest", to: "queries#suggest"
|
|
10
12
|
post "optimize", to: "queries#optimize"
|
|
11
13
|
get "slow_queries", to: "queries#slow_queries"
|
|
@@ -66,6 +66,12 @@ module MysqlGenius
|
|
|
66
66
|
# Defaults to "ActionController::Base".
|
|
67
67
|
attr_accessor :base_controller
|
|
68
68
|
|
|
69
|
+
# Whether to start the background stats collector on boot.
|
|
70
|
+
# When enabled, performance_schema is sampled periodically and stored
|
|
71
|
+
# in an in-memory ring buffer accessible via MysqlGenius.stats_history.
|
|
72
|
+
# Defaults to true.
|
|
73
|
+
attr_accessor :stats_collection
|
|
74
|
+
|
|
69
75
|
def initialize
|
|
70
76
|
@featured_tables = []
|
|
71
77
|
@blocked_tables = [
|
|
@@ -89,6 +95,7 @@ module MysqlGenius
|
|
|
89
95
|
@redis_url = nil
|
|
90
96
|
@audit_logger = nil
|
|
91
97
|
@base_controller = "ActionController::Base"
|
|
98
|
+
@stats_collection = true
|
|
92
99
|
end
|
|
93
100
|
|
|
94
101
|
def ai_enabled?
|
data/lib/mysql_genius/engine.rb
CHANGED
|
@@ -13,6 +13,19 @@ module MysqlGenius
|
|
|
13
13
|
require "mysql_genius/slow_query_monitor"
|
|
14
14
|
MysqlGenius::SlowQueryMonitor.subscribe!
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
if MysqlGenius.configuration.stats_collection
|
|
18
|
+
history = MysqlGenius::Core::Analysis::StatsHistory.new
|
|
19
|
+
connection_provider = -> { MysqlGenius::Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection) }
|
|
20
|
+
collector = MysqlGenius::Core::Analysis::StatsCollector.new(
|
|
21
|
+
connection_provider: connection_provider,
|
|
22
|
+
history: history,
|
|
23
|
+
)
|
|
24
|
+
MysqlGenius.stats_history = history
|
|
25
|
+
MysqlGenius.stats_collector = collector
|
|
26
|
+
collector.start
|
|
27
|
+
at_exit { collector.stop }
|
|
28
|
+
end
|
|
16
29
|
end
|
|
17
30
|
end
|
|
18
31
|
end
|
data/lib/mysql_genius/version.rb
CHANGED
data/lib/mysql_genius.rb
CHANGED
data/mysql_genius.gemspec
CHANGED
|
@@ -30,6 +30,6 @@ Gem::Specification.new do |spec|
|
|
|
30
30
|
spec.require_paths = ["lib"]
|
|
31
31
|
|
|
32
32
|
spec.add_dependency("activerecord", ">= 6.0", "< 9")
|
|
33
|
-
spec.add_dependency("mysql_genius-core", "~> 0.
|
|
33
|
+
spec.add_dependency("mysql_genius-core", "~> 0.7.0")
|
|
34
34
|
spec.add_dependency("railties", ">= 6.0", "< 9")
|
|
35
35
|
end
|
data/ralph/prd.json
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "MysqlGenius",
|
|
3
|
+
"branchName": "ralph/query-detail-page",
|
|
4
|
+
"description": "Query Detail Page with Lightweight History - background stats collector, in-memory ring buffer, SVG time-series charts, and a dedicated query detail page accessible from the Query Stats tab",
|
|
5
|
+
"userStories": [
|
|
6
|
+
{
|
|
7
|
+
"id": "US-001",
|
|
8
|
+
"title": "Add StatsHistory ring buffer to core gem",
|
|
9
|
+
"description": "As a developer, I need a thread-safe in-memory ring buffer to store per-digest query stats snapshots.",
|
|
10
|
+
"acceptanceCriteria": [
|
|
11
|
+
"Create gems/mysql_genius-core/lib/mysql_genius/core/analysis/stats_history.rb",
|
|
12
|
+
"StatsHistory.new(max_samples: 1440) initializes empty buffer",
|
|
13
|
+
"record(digest_text, {timestamp:, calls:, total_time_ms:, avg_time_ms:}) appends a snapshot",
|
|
14
|
+
"series_for(digest_text) returns ordered array oldest-to-newest",
|
|
15
|
+
"series_for returns empty array for unknown digests",
|
|
16
|
+
"Ring buffer drops oldest entry when max_samples reached",
|
|
17
|
+
"digests returns all known digest keys",
|
|
18
|
+
"clear empties all data",
|
|
19
|
+
"All operations are thread-safe via Mutex",
|
|
20
|
+
"Add require to gems/mysql_genius-core/lib/mysql_genius/core.rb",
|
|
21
|
+
"Create spec at gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_history_spec.rb with tests for all above",
|
|
22
|
+
"Core gem suite passes: (cd gems/mysql_genius-core && bundle exec rspec)",
|
|
23
|
+
"Typecheck passes"
|
|
24
|
+
],
|
|
25
|
+
"priority": 1,
|
|
26
|
+
"passes": true,
|
|
27
|
+
"notes": "Thread safety: wrap all reads and writes in a single Mutex. The critical section is microseconds (array append/slice). Use a Hash keyed by digest_text, each value is an Array acting as ring buffer."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "US-002",
|
|
31
|
+
"title": "Add StatsCollector background sampler to core gem",
|
|
32
|
+
"description": "As a developer, I need a background thread that periodically samples performance_schema and computes delta snapshots.",
|
|
33
|
+
"acceptanceCriteria": [
|
|
34
|
+
"Create gems/mysql_genius-core/lib/mysql_genius/core/analysis/stats_collector.rb",
|
|
35
|
+
"initialize(connection_provider:, history:, interval: 60) accepts a callable for connection",
|
|
36
|
+
"start spawns a background Thread and returns self",
|
|
37
|
+
"stop signals the thread to exit and joins with 5s timeout",
|
|
38
|
+
"running? returns boolean",
|
|
39
|
+
"Each tick: queries performance_schema for top 50 digests by SUM_TIMER_WAIT",
|
|
40
|
+
"Computes deltas: delta_calls = current - previous, delta_total_time = current - previous",
|
|
41
|
+
"Records delta snapshot into the StatsHistory instance",
|
|
42
|
+
"Negative deltas (server restart) recorded as 0",
|
|
43
|
+
"If performance_schema is unavailable, logs warning and stops (no crash loop)",
|
|
44
|
+
"Add require to gems/mysql_genius-core/lib/mysql_genius/core.rb",
|
|
45
|
+
"Create spec at gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_collector_spec.rb",
|
|
46
|
+
"Core gem suite passes",
|
|
47
|
+
"Typecheck passes"
|
|
48
|
+
],
|
|
49
|
+
"priority": 2,
|
|
50
|
+
"passes": true,
|
|
51
|
+
"notes": "The connection_provider is a callable (lambda/proc) that returns a Core::Connection. This lets Rails pass -> { ActiveRecordAdapter.new(ActiveRecord::Base.connection) } and the sidecar pass -> { session.checkout { |a| a } }. Use the same SQL shape as QueryStats#build_sql but hardcoded to top 50 by SUM_TIMER_WAIT. Store @previous hash of {digest => {calls:, total_time_ms:}} for delta computation."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "US-003",
|
|
55
|
+
"title": "Add DIGEST hash to QueryStats return value",
|
|
56
|
+
"description": "As a developer, I need the DIGEST hex hash in QueryStats output so the detail page can use it as a URL key.",
|
|
57
|
+
"acceptanceCriteria": [
|
|
58
|
+
"Modify gems/mysql_genius-core/lib/mysql_genius/core/analysis/query_stats.rb",
|
|
59
|
+
"Add DIGEST column to the SELECT in build_sql",
|
|
60
|
+
"Add digest: row['DIGEST'] to the transform method return hash",
|
|
61
|
+
"Existing fields unchanged (backward compatible addition)",
|
|
62
|
+
"Update query_stats_spec to verify the new digest field is present",
|
|
63
|
+
"Core gem suite passes",
|
|
64
|
+
"Typecheck passes"
|
|
65
|
+
],
|
|
66
|
+
"priority": 3,
|
|
67
|
+
"passes": true,
|
|
68
|
+
"notes": "DIGEST is a 64-char hex string computed by MySQL. It's stable across identical query templates. The existing sql field continues to hold the truncated DIGEST_TEXT."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "US-004",
|
|
72
|
+
"title": "Add query detail shared template with SVG charts",
|
|
73
|
+
"description": "As a user, I want to see a query's SQL, current stats, and time-series performance charts on a dedicated page.",
|
|
74
|
+
"acceptanceCriteria": [
|
|
75
|
+
"Create gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/query_detail.html.erb",
|
|
76
|
+
"Template shows: full SQL in mg-sql-block styled container",
|
|
77
|
+
"Template shows: Explain button that fires POST /explain",
|
|
78
|
+
"Template shows: stats summary cards (Calls, Total Time, Avg Time, Max Time, Rows Examined, Rows Sent, First Seen, Last Seen)",
|
|
79
|
+
"Template shows: three SVG charts stacked vertically (Total Time ms, Average Time ms, Calls)",
|
|
80
|
+
"SVG charts drawn by a drawChart(containerId, data, label, color) JS function",
|
|
81
|
+
"Charts use polyline with translucent fill below the line",
|
|
82
|
+
"Charts have Y axis auto-scaled with 4-5 ticks, X axis with time labels every ~4 hours",
|
|
83
|
+
"Charts use #89CFF0 line color in light mode, #58a6ff in dark mode",
|
|
84
|
+
"Chart height is 200px, width responsive (100% of container)",
|
|
85
|
+
"Page loads data via fetch to GET /api/query_history/:digest on page load",
|
|
86
|
+
"All JS is inline in the template (no external dependencies)",
|
|
87
|
+
"Typecheck passes"
|
|
88
|
+
],
|
|
89
|
+
"priority": 4,
|
|
90
|
+
"passes": true,
|
|
91
|
+
"notes": "The template is standalone (not a tab partial). It will be rendered through each adapter's layout. The drawChart function creates an SVG element with: a viewBox for responsive scaling, a polyline for the data series, rect elements or text for axis labels. The Explain button reuses the existing POST /explain endpoint. Data fetched from /api/query_history/:digest returns {query: {...stats...}, history: [{timestamp, calls, total_time_ms, avg_time_ms}, ...]}."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "US-005",
|
|
95
|
+
"title": "Make Query Stats tab SQL cells clickable links",
|
|
96
|
+
"description": "As a user, I want to click a query in the stats table to see its detail page.",
|
|
97
|
+
"acceptanceCriteria": [
|
|
98
|
+
"Modify gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/dashboard.html.erb",
|
|
99
|
+
"In the loadQueryStats JS function, render SQL column as <a href='/queries/:digest'> link",
|
|
100
|
+
"Link uses the digest hex hash from the query_stats API response",
|
|
101
|
+
"Link styled with mg-link class or inline color to look clickable",
|
|
102
|
+
"Existing query stats table layout and sorting still work",
|
|
103
|
+
"Rails adapter suite passes: bundle exec rspec",
|
|
104
|
+
"Typecheck passes"
|
|
105
|
+
],
|
|
106
|
+
"priority": 5,
|
|
107
|
+
"passes": true,
|
|
108
|
+
"notes": "The loadQueryStats function is in dashboard.html.erb's inline JS. It currently renders the SQL as plain text in a td. Change it to an anchor tag. The href should use path_for pattern but since this is in JS, just hardcode '/queries/' + digest (the sidecar's PATHS hash and Rails route both serve this path). Also update the dashboard overview's Top 5 Expensive Queries to be clickable."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "US-006",
|
|
112
|
+
"title": "Wire stats collector and detail routes into Rails adapter",
|
|
113
|
+
"description": "As a Rails developer, I want the stats collector to start on boot and the query detail page to be accessible.",
|
|
114
|
+
"acceptanceCriteria": [
|
|
115
|
+
"Add stats_collection config option (default true) to lib/mysql_genius/configuration.rb",
|
|
116
|
+
"Add MysqlGenius.stats_history and MysqlGenius.stats_collector module-level accessors to lib/mysql_genius.rb",
|
|
117
|
+
"Add initializer in lib/mysql_genius/engine.rb that starts StatsCollector when enabled",
|
|
118
|
+
"Initializer creates StatsHistory, StatsCollector with ActiveRecordAdapter connection_provider, calls start",
|
|
119
|
+
"Registers at_exit to stop the collector",
|
|
120
|
+
"Add two routes to config/routes.rb: get 'queries/:digest' and get 'api/query_history/:digest'",
|
|
121
|
+
"Add query_detail action to QueriesController that renders the shared template",
|
|
122
|
+
"Add query_history action that returns JSON with current stats + history",
|
|
123
|
+
"query_history looks up the digest in performance_schema and gets history from MysqlGenius.stats_history",
|
|
124
|
+
"Add request spec for GET /queries/:digest (returns 200)",
|
|
125
|
+
"Add request spec for GET /api/query_history/:digest (returns JSON with query and history keys)",
|
|
126
|
+
"Rails adapter suite passes",
|
|
127
|
+
"Typecheck passes"
|
|
128
|
+
],
|
|
129
|
+
"priority": 6,
|
|
130
|
+
"passes": true,
|
|
131
|
+
"notes": "The connection_provider for Rails is -> { Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection) }. The query_detail action just renders the template with an @digest instance variable. The query_history action queries performance_schema filtered by DIGEST = :digest for current stats, and calls MysqlGenius.stats_history.series_for(digest_text) for history. If stats_history is nil (collection disabled), return empty history array."
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"id": "US-007",
|
|
135
|
+
"title": "Wire stats collector and detail routes into desktop sidecar",
|
|
136
|
+
"description": "As a sidecar user, I want the stats collector running and the query detail page accessible.",
|
|
137
|
+
"acceptanceCriteria": [
|
|
138
|
+
"Update gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb to create StatsHistory + StatsCollector and set on App",
|
|
139
|
+
"Add App settings: :stats_history, :stats_collector",
|
|
140
|
+
"Collector uses connection_provider that checks out from the active session",
|
|
141
|
+
"Register at_exit to stop collector",
|
|
142
|
+
"Update SessionSwapper to stop old collector, clear history, start new collector on profile switch",
|
|
143
|
+
"Add GET /queries/:digest route to App (renders query_detail template through layout)",
|
|
144
|
+
"Add GET /api/query_history/:digest route to App (returns JSON)",
|
|
145
|
+
"Both new routes are under session-token auth",
|
|
146
|
+
"Add request spec for the two new routes",
|
|
147
|
+
"Desktop gem suite passes: (cd gems/mysql_genius-desktop && bundle exec rspec)",
|
|
148
|
+
"Typecheck passes"
|
|
149
|
+
],
|
|
150
|
+
"priority": 7,
|
|
151
|
+
"passes": true,
|
|
152
|
+
"notes": "The connection_provider for the sidecar wraps session.checkout. On profile switch, SessionSwapper should: App.settings.stats_collector&.stop, App.settings.stats_history&.clear, then after swapping the session create a new collector with the new session and start it. The render_query_detail method follows the same pattern as render_dashboard (Tilt through layout)."
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"id": "US-008",
|
|
156
|
+
"title": "Full green sweep across all suites",
|
|
157
|
+
"description": "As a developer, I need all test suites and linters passing before the PR.",
|
|
158
|
+
"acceptanceCriteria": [
|
|
159
|
+
"Rails adapter rspec passes (78+ examples)",
|
|
160
|
+
"Core gem rspec passes (194+ examples plus new StatsHistory/StatsCollector/QueryStats specs)",
|
|
161
|
+
"Desktop gem rspec passes (154+ examples plus new route specs)",
|
|
162
|
+
"Rails adapter rubocop clean",
|
|
163
|
+
"Core gem rubocop clean",
|
|
164
|
+
"Desktop gem rubocop clean",
|
|
165
|
+
"git diff main -- .github/workflows/publish.yml is empty (untouched)",
|
|
166
|
+
"No version bumps committed (version bump is a separate release task)",
|
|
167
|
+
"Typecheck passes"
|
|
168
|
+
],
|
|
169
|
+
"priority": 8,
|
|
170
|
+
"passes": false,
|
|
171
|
+
"notes": "Run all six commands in order. If any fail, fix before proceeding. This is a verification step, not an implementation step."
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
data/ralph/progress.txt
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Ralph Progress Log
|
|
2
|
+
Started: Sun Apr 12 13:52:29 CDT 2026
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Codebase Patterns
|
|
6
|
+
- Core gem specs use `require "spec_helper"` (no Rails boot), FakeAdapter for connection stubs
|
|
7
|
+
- Analysis classes follow `initialize(connection)` + `#call` pattern
|
|
8
|
+
- Core gem has zero runtime dependencies — no Rails-specific code
|
|
9
|
+
- Spec files mirror the lib directory structure: `lib/mysql_genius/core/analysis/foo.rb` → `spec/mysql_genius/core/analysis/foo_spec.rb`
|
|
10
|
+
- RuboCop uses rubocop-shopify + rubocop-rspec; target Ruby 2.6
|
|
11
|
+
- `Time#iso8601` requires `require 'time'` — use `strftime("%Y-%m-%dT%H:%M:%SZ")` in core gem instead
|
|
12
|
+
- FakeAdapter stubs are searched with `find` (first match wins) — clear `@stubs` via `instance_variable_set(:@stubs, [])` before re-stubbing
|
|
13
|
+
- Use `ConditionVariable` + `Mutex` for interruptible sleep in background threads (cleaner than polling loops)
|
|
14
|
+
- Standalone templates (not partials) must duplicate JS helpers from dashboard since they run in a separate page scope
|
|
15
|
+
- Shared templates use `path_for(:name)` for URLs and `@digest` instance var — adapters must set these before rendering
|
|
16
|
+
- Desktop gem has NO ActiveSupport — never use `.squish` or other AS extensions; use plain string concatenation for SQL
|
|
17
|
+
- Desktop `Core::Result#to_a` returns raw row arrays; use `to_hashes` to get `{column => value}` hashes
|
|
18
|
+
- Desktop `rack_helper.rb` sets `@fake_adapter` via before block — specs using it must disable `RSpec/InstanceVariable`
|
|
19
|
+
- Rails `ActiveRecord::Result` `to_a` returns hashes directly; for specs use `instance_double` with `to_a:` returning hash arrays
|
|
20
|
+
- When adding new App settings to desktop, also reset them in `rack_helper.rb` after block and set in before block
|
|
21
|
+
- `StatsCollector.new` spawns real threads — mock it in request specs via `allow(StatsCollector).to receive(:new)` in rack_helper
|
|
22
|
+
- `path_for(:query_detail)` and `path_for(:query_history)` both need `@digest` to build a complete URL; both adapters handle this in their `path_for` implementation
|
|
23
|
+
- Routes that need a dynamic segment (digest) use base-path + value pattern in desktop PATHS hash; Rails uses named route helpers with keyword arg
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2026-04-12 - US-001
|
|
28
|
+
- Implemented `StatsHistory` ring buffer in `gems/mysql_genius-core/lib/mysql_genius/core/analysis/stats_history.rb`
|
|
29
|
+
- Thread-safe via Mutex, supports record/series_for/digests/clear, drops oldest on cap
|
|
30
|
+
- Added require to `gems/mysql_genius-core/lib/mysql_genius/core.rb`
|
|
31
|
+
- Created spec at `gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_history_spec.rb` (9 examples)
|
|
32
|
+
- Files changed: stats_history.rb (new), core.rb (require added), stats_history_spec.rb (new)
|
|
33
|
+
- Core gem suite: 203 examples, 0 failures
|
|
34
|
+
- RuboCop: clean
|
|
35
|
+
- **Learnings for future iterations:**
|
|
36
|
+
- The core gem spec suite runs fast (~0.06s) — safe to run full suite on each story
|
|
37
|
+
- FakeAdapter is only needed for connection-dependent classes; StatsHistory is pure Ruby
|
|
38
|
+
- Thread safety tests work well with 4 threads × 500 iterations pattern
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 2026-04-12 - US-002
|
|
42
|
+
- Implemented `StatsCollector` background sampler in `gems/mysql_genius-core/lib/mysql_genius/core/analysis/stats_collector.rb`
|
|
43
|
+
- Background thread queries performance_schema for top 50 digests by SUM_TIMER_WAIT
|
|
44
|
+
- Computes per-interval deltas, clamps negatives to 0 (server restart handling)
|
|
45
|
+
- Uses ConditionVariable for interruptible sleep (clean stop within 5s timeout)
|
|
46
|
+
- connection_provider callable pattern: adapters supply their own connection strategy
|
|
47
|
+
- Added require to `gems/mysql_genius-core/lib/mysql_genius/core.rb`
|
|
48
|
+
- Created spec at `gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_collector_spec.rb` (12 examples)
|
|
49
|
+
- Files changed: stats_collector.rb (new), core.rb (require added), stats_collector_spec.rb (new)
|
|
50
|
+
- Core gem suite: 215 examples, 0 failures
|
|
51
|
+
- RuboCop: clean
|
|
52
|
+
- **Learnings for future iterations:**
|
|
53
|
+
- `Time#iso8601` is not available without `require 'time'` — use `strftime` in zero-dep core gem
|
|
54
|
+
- FakeAdapter stubs match first-registered-first-matched; clear `@stubs` array before re-stubbing between ticks
|
|
55
|
+
- Testing background threads: set `@running = true` in `start` (before thread spawn) to eliminate race; use `send(:tick)` to test delta logic directly without timing sensitivity
|
|
56
|
+
- SQL shape mirrors QueryStats#build_sql but selects only DIGEST_TEXT, COUNT_STAR, total_time_ms
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 2026-04-12 - US-003
|
|
60
|
+
- Added `DIGEST` column to `build_sql` SELECT and `digest:` key to `transform` return hash in QueryStats
|
|
61
|
+
- Updated spec columns/rows fixtures to include DIGEST as first column, verified new field in assertions
|
|
62
|
+
- Files changed: `gems/mysql_genius-core/lib/mysql_genius/core/analysis/query_stats.rb`, `gems/mysql_genius-core/spec/mysql_genius/core/analysis/query_stats_spec.rb`
|
|
63
|
+
- Core gem suite: 215 examples, 0 failures
|
|
64
|
+
- Rails adapter suite: 78 examples, 0 failures
|
|
65
|
+
- RuboCop: clean
|
|
66
|
+
- **Learnings for future iterations:**
|
|
67
|
+
- QueryStats spec uses positional arrays for row data — when adding a new column to the SELECT, prepend/insert it in ALL test row arrays (easy to miss the truncation test at the bottom)
|
|
68
|
+
- The `transform` method uses case-insensitive column access (`row["DIGEST"] || row["digest"]`) for MariaDB compatibility — follow this pattern for any new columns
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 2026-04-12 - US-004
|
|
72
|
+
- Created standalone `query_detail.html.erb` shared template in core gem views
|
|
73
|
+
- Template includes: full SQL display with syntax highlighting, Copy and EXPLAIN buttons
|
|
74
|
+
- Stats summary cards: Calls, Total Time, Avg Time, Max Time, Rows Examined, Rows Sent, First Seen, Last Seen
|
|
75
|
+
- Three SVG time-series charts: Total Time (ms), Average Time (ms), Calls
|
|
76
|
+
- SVG charts use polyline with translucent fill, auto-scaled Y axis with nice tick values, responsive via viewBox
|
|
77
|
+
- Light mode line color #89CFF0, dark mode #58a6ff (detected via data-theme attribute)
|
|
78
|
+
- Data loaded via fetch to GET /api/query_history/:digest, EXPLAIN via POST /explain
|
|
79
|
+
- All JS is inline (no external dependencies), replicates helper functions from dashboard (highlightSql, formatDuration, ajax, etc.)
|
|
80
|
+
- Files changed: `gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/query_detail.html.erb` (new)
|
|
81
|
+
- Core gem suite: 215 examples, 0 failures
|
|
82
|
+
- Rails adapter suite: 78 examples, 0 failures
|
|
83
|
+
- RuboCop: clean
|
|
84
|
+
- **Learnings for future iterations:**
|
|
85
|
+
- Standalone templates (not tab partials) need their own copies of JS helper functions since they don't share the dashboard's IIFE scope
|
|
86
|
+
- The template uses `path_for(:root)` for the back link, `path_for(:explain)` for EXPLAIN, `path_for(:query_history)` for data — these routes must exist in both Rails adapter and sidecar
|
|
87
|
+
- SVG chart viewBox approach (800x200 with preserveAspectRatio="none") makes charts responsive without JS resize handlers
|
|
88
|
+
- Y-axis nice step calculation: ceil(rawStep/magnitude)*magnitude ensures clean round-number ticks
|
|
89
|
+
- The `@digest` instance variable must be set by the controller action before rendering this template
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 2026-04-11 - US-005
|
|
93
|
+
- Updated `dashboard.html.erb` to linkify SQL cells in both loadQueryStats and Top 5 Expensive Queries
|
|
94
|
+
- Added `query_detail: '/queries/'` to ROUTES JS object (base path, digest appended via JS)
|
|
95
|
+
- SQL cells now render `<a href="/queries/:digest" class="mg-link">` when digest is present; fallback to plain text if no digest
|
|
96
|
+
- Files changed: `gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/dashboard.html.erb`
|
|
97
|
+
- Rails adapter suite: 78 examples, 0 failures
|
|
98
|
+
- RuboCop: clean
|
|
99
|
+
- **Learnings for future iterations:**
|
|
100
|
+
- The ROUTES JS object uses hardcoded base paths (not server-rendered route helpers) for dynamic routes — append the value in JS
|
|
101
|
+
- `mg-link` class is defined in layout.html.erb (both adapters share the layout's CSS)
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 2026-04-11 - US-006
|
|
105
|
+
- Added `stats_collection` config option (default: true) to `lib/mysql_genius/configuration.rb`
|
|
106
|
+
- Added `MysqlGenius.stats_history` and `MysqlGenius.stats_collector` module-level accessors to `lib/mysql_genius.rb`
|
|
107
|
+
- Added `config.after_initialize` block in engine.rb that starts StatsCollector when `stats_collection` is enabled
|
|
108
|
+
- Added routes: `get "queries/:digest"` and `get "api/query_history/:digest"` to `config/routes.rb`
|
|
109
|
+
- Added `query_detail` and `query_history` actions to `QueriesController`
|
|
110
|
+
- Updated `SharedViewHelpers#path_for` to auto-inject `@digest` for `:query_detail` and `:query_history` routes
|
|
111
|
+
- Added private helpers `fetch_query_history_current`, `fetch_query_history_series`, `lookup_digest_text`
|
|
112
|
+
- Added request specs in `spec/requests/mysql_genius/query_detail_spec.rb` (4 examples)
|
|
113
|
+
- Files changed: configuration.rb, lib/mysql_genius.rb, engine.rb, config/routes.rb, queries_controller.rb, shared_view_helpers.rb, query_detail_spec.rb (new)
|
|
114
|
+
- Rails adapter suite: 82 examples, 0 failures
|
|
115
|
+
- RuboCop: clean
|
|
116
|
+
- **Learnings for future iterations:**
|
|
117
|
+
- `ActiveRecord::Result` `instance_double` — use `to_a:` returning hash arrays; use `result.to_a.first` not `result.first` (which needs Enumerable)
|
|
118
|
+
- Engine's `config.after_initialize` creates the collector in test env too, causing a benign "StatsCollector stopped" warning in tests — this is expected
|
|
119
|
+
- `path_for` needs to know about digest routes: use an allowlist `%i[query_detail query_history]` and conditionally pass `digest:` param
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 2026-04-11 - US-007
|
|
123
|
+
- Updated `Launcher#call` to create StatsHistory + StatsCollector, set on App, register collector in at_exit
|
|
124
|
+
- Added `set :stats_history, nil` and `set :stats_collector, nil` to App settings
|
|
125
|
+
- Updated App `path_for` helper to append `@digest` for `:query_detail` and `:query_history` routes
|
|
126
|
+
- Added `GET /queries/:digest` and `GET /api/query_history/:digest` routes to App (under session-token auth)
|
|
127
|
+
- Added `render_query_detail`, `fetch_query_history_current`, `fetch_query_history_series` private methods
|
|
128
|
+
- Updated SessionSwapper to stop old collector, clear history, create new collector after session swap
|
|
129
|
+
- Added `:root`, `:query_detail`, `:query_history` to desktop PATHS hash
|
|
130
|
+
- Updated `rack_helper.rb` to reset stats settings and stub `StatsCollector.new` globally to prevent background threads in specs
|
|
131
|
+
- Updated `session_swapper_spec.rb` to mock StatsCollector/StatsHistory and handle new `set` keys
|
|
132
|
+
- Added `spec/requests/query_detail_spec.rb` for desktop (4 examples)
|
|
133
|
+
- Files changed: app.rb, paths.rb, launcher.rb, session_swapper.rb, rack_helper.rb, session_swapper_spec.rb, query_detail_spec.rb (new)
|
|
134
|
+
- Desktop suite: 158 examples, 0 failures (was 154)
|
|
135
|
+
- RuboCop: clean
|
|
136
|
+
- **Learnings for future iterations:**
|
|
137
|
+
- Desktop gem has NO ActiveSupport — never use `.squish`; use explicit string concatenation
|
|
138
|
+
- `Core::Result#to_a` returns raw row arrays (not hashes) — use `to_hashes` for hash access
|
|
139
|
+
- When new `SessionSwapper#switch_to` logic creates real background objects, existing integration tests can fail — mock the constructor in rack_helper to prevent threads
|
|
140
|
+
- Add `# rubocop:disable RSpec/InstanceVariable` at file top + `enable` at bottom when using `@fake_adapter` directly in specs
|
|
141
|
+
---
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mysql_genius
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antarr Byrd
|
|
@@ -36,14 +36,14 @@ dependencies:
|
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.7.0
|
|
40
40
|
type: :runtime
|
|
41
41
|
prerelease: false
|
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.
|
|
46
|
+
version: 0.7.0
|
|
47
47
|
- !ruby/object:Gem::Dependency
|
|
48
48
|
name: railties
|
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -83,6 +83,7 @@ files:
|
|
|
83
83
|
- CHANGELOG.md
|
|
84
84
|
- Gemfile
|
|
85
85
|
- LICENSE.txt
|
|
86
|
+
- Makefile
|
|
86
87
|
- README.md
|
|
87
88
|
- Rakefile
|
|
88
89
|
- app/controllers/concerns/mysql_genius/ai_features.rb
|
|
@@ -111,6 +112,8 @@ files:
|
|
|
111
112
|
- lib/mysql_genius/slow_query_monitor.rb
|
|
112
113
|
- lib/mysql_genius/version.rb
|
|
113
114
|
- mysql_genius.gemspec
|
|
115
|
+
- ralph/prd.json
|
|
116
|
+
- ralph/progress.txt
|
|
114
117
|
homepage: https://github.com/antarr/mysql_genius
|
|
115
118
|
licenses:
|
|
116
119
|
- MIT
|