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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a35fb751622eaa5cd7bc6fad555020fb5dbf2a9a2f50d50d27ee9b3da6440818
4
- data.tar.gz: 1423b66748df1f05b0a9e9ff3868d35905953218443823e9cf2b0a554e88c4db
3
+ metadata.gz: 4cec94802aff95468c29e6a4612651afa8c31ad134fd67fb44747487f94b02b0
4
+ data.tar.gz: 5a51de5742dc12032509e115ccc3dfc891f40d72ffb73e2ae048b58e99094289
5
5
  SHA512:
6
- metadata.gz: 8fbd465309eb1068b2c78cc47e56e93ac98badd97f8df834a306cbda5c114f903f410c5a7e927c6e9079556d2a240e44521290a28b959e9af91805f13b1fe22d
7
- data.tar.gz: 2f3fe565bd970da7f1a1ac0f331bc435a59cc5508b1094fdc91003ef01f5fd3fb7782618cc7d55e24e5aec243f597895b5d4bd1238e90b150d5d92ba2cf611ef
6
+ metadata.gz: bd85efe1037646a105930e8d02b29de18b77a5cc1b410dfa7252674ecdc2e1f8513ec4d1c0228a4f60530d0d623c667bdf83153218313929854961e8440f8eac
7
+ data.tar.gz: 4ae937b2f2eaad1baf08a9c5010a7fcaa76f87f5de83051dd025d2b78421e00be4c58b51996498175b041e218827f7c76bd123dc82352f3654af23b9c716439a
@@ -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
@@ -14,3 +14,4 @@ CLAUDE.md
14
14
  Gemfile.lock
15
15
  *.gem
16
16
  docs/superpowers/
17
+ /ralph
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
- mysql_genius.public_send("#{name}_path")
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?
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MysqlGenius
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/mysql_genius.rb CHANGED
@@ -20,6 +20,9 @@ module MysqlGenius
20
20
  def reset_configuration!
21
21
  @configuration = Configuration.new
22
22
  end
23
+
24
+ attr_accessor :stats_history
25
+ attr_accessor :stats_collector
23
26
  end
24
27
  end
25
28
 
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.6.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
+ }
@@ -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.6.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.6.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.6.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