mysql_genius-core 0.7.2 → 0.8.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: 321f2469ac2b5213d704f7afd4ac6211a0a327d1c8d054c9fdb95843dd4b2bde
4
- data.tar.gz: 62a5b202b55bf2ffa2c7d76b5c263c7685bd4d9de14f206903ae35604dd146ce
3
+ metadata.gz: 5075a444aed755238df78580fbd0e8ae79c2cb999ffc7504ff6af1ae8c08c8e7
4
+ data.tar.gz: 9edad6c0620a8c4434b716995ac7e0c2af52494c0f61912d45a1496468fa97e5
5
5
  SHA512:
6
- metadata.gz: 5666c4264a19bfddb7052233e3d02c9499fdbf7ea6ffb3b0e4fa60c1bea117de03a3a349cf399b716cb947733050ac053a5cc52c287d2798b17ed7f6160c6306
7
- data.tar.gz: c5f99646c2c9da80629824366b5ee1a0dcf7710cf84bf06f2cfe8e783d4f42b1a3fd0d4635a9ddc1fd7ff29ea5c7e4e77734bd4f995fef47ea0a32a6174e4504
6
+ metadata.gz: bd2263afa33f3b61b826d5667ba01d319f3cddd39c8b44dad0e24a50dd06ff23b69224322dc8b6762491f1f10fbe4f9e9ccf199fb9899586e99506b07e652fbe
7
+ data.tar.gz: 60f654e26bf1aef5fadfe996caa743911c7303d227e79c67ce6e4a5e6a3bfd4828afa6ccbc58be79b763f391a81f9e7d8e4ecaa273f220b1f0454da493750415
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Added
6
+ - `MysqlGenius::Core::Ai::VariableReviewer` — reviews MySQL config variables against best practices
7
+ - `MysqlGenius::Core::Ai::ConnectionAdvisor` — diagnoses connection pool health
8
+ - `MysqlGenius::Core::Ai::WorkloadDigest` — executive summary of the query workload
9
+ - `MysqlGenius::Core::Ai::InnodbInterpreter` — interprets `SHOW ENGINE INNODB STATUS` in plain English
10
+ - `MysqlGenius::Core::Ai::IndexPlanner` — holistic index optimization plan across tables
11
+ - `MysqlGenius::Core::Ai::PatternGrouper` — groups slow queries by shared root cause
12
+ - AI buttons in shared dashboard templates: Server tab (Variable Review, Connection Advisor, InnoDB Health), Query Stats tab (Workload Digest, Pattern Grouper), Unused Indexes tab (Index Planner)
13
+
3
14
  ## 0.7.2
4
15
 
5
16
  ### Added
@@ -62,7 +62,7 @@ module MysqlGenius
62
62
  response_format: { type: "json_object" },
63
63
  temperature: temperature,
64
64
  }
65
- body[:max_tokens] = @config.max_tokens if @config.max_tokens
65
+ body[:max_tokens] = @config.max_tokens.to_i if @config.max_tokens
66
66
  body[:model] = @config.model if @config.model && !@config.model.empty?
67
67
  body
68
68
  end
@@ -73,7 +73,7 @@ module MysqlGenius
73
73
 
74
74
  body = {
75
75
  messages: user_messages,
76
- max_tokens: @config.max_tokens || 4096,
76
+ max_tokens: (@config.max_tokens || 4096).to_i,
77
77
  temperature: temperature,
78
78
  }
79
79
  body[:system] = system_text unless system_text.empty?
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Diagnoses connection pool health by gathering connection-related
7
+ # metrics from SHOW GLOBAL STATUS and SHOW GLOBAL VARIABLES, then
8
+ # asking the LLM to distinguish between pool misconfiguration,
9
+ # connection leaks, missing pooling, and traffic saturation.
10
+ class ConnectionAdvisor
11
+ STATUS_KEYS = [
12
+ "Threads_connected",
13
+ "Threads_running",
14
+ "Max_used_connections",
15
+ "Aborted_connects",
16
+ "Aborted_clients",
17
+ "Connections",
18
+ "Threads_created",
19
+ ].freeze
20
+
21
+ VARIABLE_KEYS = [
22
+ "max_connections",
23
+ "wait_timeout",
24
+ "interactive_timeout",
25
+ "thread_cache_size",
26
+ ].freeze
27
+
28
+ def initialize(client, config, connection)
29
+ @client = client
30
+ @config = config
31
+ @connection = connection
32
+ end
33
+
34
+ def call
35
+ variables = fetch_variables
36
+ status = fetch_status
37
+
38
+ messages = [
39
+ { role: "system", content: system_prompt },
40
+ { role: "user", content: user_prompt(variables, status) },
41
+ ]
42
+ @client.chat(messages: messages)
43
+ end
44
+
45
+ private
46
+
47
+ def fetch_variables
48
+ result = @connection.exec_query("SHOW GLOBAL VARIABLES")
49
+ result.rows
50
+ .select { |row| VARIABLE_KEYS.include?(row[0]) }
51
+ .map { |row| [row[0], row[1]] }
52
+ end
53
+
54
+ def fetch_status
55
+ result = @connection.exec_query("SHOW GLOBAL STATUS")
56
+ result.rows
57
+ .select { |row| STATUS_KEYS.include?(row[0]) }
58
+ .map { |row| [row[0], row[1]] }
59
+ end
60
+
61
+ def system_prompt
62
+ <<~PROMPT
63
+ You are a MySQL connection health advisor. Analyze the connection-related variables and status counters below, then diagnose the connection health and provide specific recommendations. Consider:
64
+ - Connection utilization: Max_used_connections vs max_connections (target: stay below 80%)
65
+ - Aborted connections: Aborted_connects indicates authentication failures or client errors; Aborted_clients indicates clients disconnecting without proper cleanup
66
+ - Thread cache efficiency: Threads_created vs Connections (high ratio means thread_cache_size is too small)
67
+ - Timeout configuration: wait_timeout and interactive_timeout impact how long idle connections persist
68
+ - Connection leak indicators: high Threads_connected with low Threads_running suggests idle connection accumulation
69
+ - Traffic saturation: high Threads_running relative to CPU cores suggests query contention
70
+
71
+ Distinguish between these root causes:
72
+ 1. Pool misconfiguration (max_connections too low/high, bad timeout values)
73
+ 2. Connection leaks (growing Threads_connected, high Aborted_clients)
74
+ 3. Missing connection pooling (high Connections with short-lived threads)
75
+ 4. Traffic saturation (high Threads_running, query contention)
76
+ #{@config.domain_context}
77
+ Respond with JSON: {"diagnosis": "markdown analysis distinguishing between pool misconfiguration, connection leaks, missing pooling, and traffic saturation, with specific variable recommendations and values"}
78
+ PROMPT
79
+ end
80
+
81
+ def user_prompt(variables, status)
82
+ lines = ["== Connection Variables =="]
83
+ variables.each { |name, value| lines << "#{name} = #{value}" }
84
+ lines << ""
85
+ lines << "== Connection Status Counters =="
86
+ status.each { |name, value| lines << "#{name} = #{value}" }
87
+ lines.join("\n")
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ class IndexPlanner
7
+ def initialize(client, config, connection)
8
+ @client = client
9
+ @config = config
10
+ @connection = connection
11
+ end
12
+
13
+ def call(tables = nil)
14
+ target_tables = resolve_tables(tables)
15
+ return { "plan" => "No tables found to analyze." } if target_tables.empty?
16
+
17
+ unused = Analysis::UnusedIndexes.new(@connection).call
18
+ duplicates = Analysis::DuplicateIndexes.new(@connection, blocked_tables: []).call
19
+ schema = SchemaContextBuilder.new(@connection).call(target_tables, detail: :with_cardinality)
20
+
21
+ index_map = target_tables.to_h do |table|
22
+ [table, @connection.indexes_for(table).map do |idx|
23
+ "#{"UNIQUE " if idx.unique}INDEX #{idx.name} (#{idx.columns.join(", ")})"
24
+ end,]
25
+ end
26
+
27
+ messages = [
28
+ { role: "system", content: system_prompt },
29
+ { role: "user", content: user_prompt(schema, unused, duplicates, index_map) },
30
+ ]
31
+ @client.chat(messages: messages)
32
+ end
33
+
34
+ private
35
+
36
+ def resolve_tables(tables)
37
+ list = Array(tables).reject { |t| t.to_s.empty? }
38
+ return list unless list.empty?
39
+
40
+ top_tables_by_size
41
+ end
42
+
43
+ def top_tables_by_size
44
+ db = @connection.current_database
45
+ result = @connection.exec_query(
46
+ "SELECT table_name FROM information_schema.tables " \
47
+ "WHERE table_schema = #{@connection.quote(db)} AND table_type = 'BASE TABLE' " \
48
+ "ORDER BY (data_length + index_length) DESC LIMIT 10",
49
+ )
50
+ result.rows.map(&:first)
51
+ rescue StandardError
52
+ @connection.tables.first(10)
53
+ end
54
+
55
+ def user_prompt(schema, unused, duplicates, index_map)
56
+ parts = ["Schema with cardinality:\n#{schema}"]
57
+
58
+ if unused.any?
59
+ unused_text = unused.map { |u| "#{u[:table]}.#{u[:index_name]} (reads=#{u[:reads]}, writes=#{u[:writes]})" }
60
+ parts << "Unused indexes (zero reads):\n#{unused_text.join("\n")}"
61
+ end
62
+
63
+ if duplicates.any?
64
+ dup_text = duplicates.map do |d|
65
+ "#{d[:table]}: #{d[:duplicate_index]} (#{d[:duplicate_columns].join(", ")}) covered by #{d[:covered_by_index]} (#{d[:covered_by_columns].join(", ")})"
66
+ end
67
+ parts << "Duplicate indexes:\n#{dup_text.join("\n")}"
68
+ end
69
+
70
+ index_summary = index_map.map { |table, idxs| "#{table}: #{idxs.any? ? idxs.join("; ") : "NONE"}" }
71
+ parts << "Current indexes per table:\n#{index_summary.join("\n")}"
72
+
73
+ parts.join("\n\n")
74
+ end
75
+
76
+ def system_prompt
77
+ <<~PROMPT
78
+ You are a MySQL index consolidation planner. Given schema information, unused indexes, duplicate indexes, and current index listings, produce a consolidated optimization plan. For each recommendation:
79
+ - DROP redundant or unused indexes (with exact ALTER TABLE ... DROP INDEX statements)
80
+ - MERGE overlapping indexes into composites where beneficial
81
+ - KEEP indexes that are actively used and well-structured
82
+ - ADD new composite indexes where query patterns suggest benefit
83
+ - Provide rationale for each change and estimated impact on read/write performance
84
+ #{@config.domain_context}
85
+ Respond with JSON: {"plan": "markdown with specific ALTER TABLE / DROP INDEX / CREATE INDEX statements, rationale for each change, and estimated impact"}
86
+ PROMPT
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Interprets SHOW ENGINE INNODB STATUS output in plain English.
7
+ # Combines the raw InnoDB status text with key metrics from
8
+ # ServerOverview to give the LLM full context for its analysis.
9
+ class InnodbInterpreter
10
+ MAX_STATUS_LENGTH = 4000
11
+
12
+ def initialize(client, config, connection)
13
+ @client = client
14
+ @config = config
15
+ @connection = connection
16
+ end
17
+
18
+ def call
19
+ status_text = fetch_innodb_status
20
+ metrics = fetch_innodb_metrics
21
+
22
+ messages = [
23
+ { role: "system", content: system_prompt },
24
+ { role: "user", content: user_prompt(status_text, metrics) },
25
+ ]
26
+ @client.chat(messages: messages)
27
+ end
28
+
29
+ private
30
+
31
+ def fetch_innodb_status
32
+ result = @connection.exec_query("SHOW ENGINE INNODB STATUS")
33
+ text = result.rows.first&.dig(2).to_s
34
+ text.length > MAX_STATUS_LENGTH ? text[0, MAX_STATUS_LENGTH] : text
35
+ end
36
+
37
+ def fetch_innodb_metrics
38
+ overview = Analysis::ServerOverview.new(@connection).call
39
+ overview[:innodb]
40
+ end
41
+
42
+ def system_prompt
43
+ <<~PROMPT
44
+ You are a MySQL InnoDB internals expert. Analyze the SHOW ENGINE INNODB STATUS output and the supplementary metrics below. Provide a plain-English interpretation organized by these sections:
45
+ - **Deadlocks**: recent deadlock information, lock wait chains, affected transactions
46
+ - **Transaction History**: history list length, purge lag, long-running transactions
47
+ - **Buffer Pool**: hit rate, dirty page ratio, free pages, eviction pressure
48
+ - **I/O**: pending reads/writes, log sequence numbers, checkpoint age, log sizing adequacy
49
+ - **Semaphores**: mutex/rw-lock waits, spin rounds, OS waits indicating contention
50
+
51
+ For each section, explain what the numbers mean in practical terms and recommend specific actions if problems are detected.
52
+ #{@config.domain_context}
53
+ Respond with JSON: {"findings": "markdown analysis organized by: Deadlocks, Transaction History, Buffer Pool, I/O, Semaphores. Each section should include current state assessment, risk level, and actionable recommendations."}
54
+ PROMPT
55
+ end
56
+
57
+ def user_prompt(status_text, metrics)
58
+ lines = ["== SHOW ENGINE INNODB STATUS =="]
59
+ lines << status_text
60
+ lines << ""
61
+ lines << "== InnoDB Metrics Summary =="
62
+ lines << "Buffer Pool Size: #{metrics[:buffer_pool_mb]} MB"
63
+ lines << "Buffer Pool Hit Rate: #{metrics[:buffer_pool_hit_rate]}%"
64
+ lines << "Dirty Pages: #{metrics[:buffer_pool_pages_dirty]}"
65
+ lines << "Free Pages: #{metrics[:buffer_pool_pages_free]}"
66
+ lines << "Total Pages: #{metrics[:buffer_pool_pages_total]}"
67
+ lines << "Row Lock Waits: #{metrics[:row_lock_waits]}"
68
+ lines << "Row Lock Time (ms): #{metrics[:row_lock_time_ms]}"
69
+ lines.join("\n")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Groups slow queries by shared root cause so a single fix can improve
7
+ # multiple queries at once. Pulls high-cost statements from
8
+ # performance_schema, extracts referenced tables, builds schema context,
9
+ # and asks the LLM to cluster queries by underlying issue.
10
+ #
11
+ # Construct with:
12
+ # connection - a Core::Connection implementation
13
+ # client - a Core::Ai::Client
14
+ # config - the Core::Ai::Config
15
+ #
16
+ # Call:
17
+ # .call() -> Hash with "groups" key containing markdown analysis
18
+ class PatternGrouper
19
+ QUERY_LIMIT = 30
20
+ ROWS_RATIO_THRESHOLD = 10
21
+ AVG_TIME_THRESHOLD = 50
22
+
23
+ def initialize(connection, client, config)
24
+ @connection = connection
25
+ @client = client
26
+ @config = config
27
+ end
28
+
29
+ def call
30
+ all_stats = Analysis::QueryStats.new(@connection).call(sort: "total_time", limit: QUERY_LIMIT)
31
+ high_cost = all_stats.select { |s| s[:rows_ratio] > ROWS_RATIO_THRESHOLD || s[:avg_time_ms] > AVG_TIME_THRESHOLD }
32
+ return { "groups" => "No high-cost queries found to analyze." } if high_cost.empty?
33
+
34
+ tables = extract_tables(high_cost)
35
+ schema = tables.any? ? SchemaContextBuilder.new(@connection).call(tables, detail: :basic) : ""
36
+
37
+ messages = [
38
+ { role: "system", content: system_prompt },
39
+ { role: "user", content: user_prompt(high_cost, schema) },
40
+ ]
41
+
42
+ @client.chat(messages: messages)
43
+ end
44
+
45
+ private
46
+
47
+ def extract_tables(stats)
48
+ stats.flat_map { |s| SqlValidator.extract_table_references(s[:sql], @connection) }.uniq
49
+ end
50
+
51
+ def system_prompt
52
+ prompt = <<~PROMPT
53
+ You are a MySQL performance analyst specializing in root-cause analysis.
54
+ PROMPT
55
+
56
+ if @config.domain_context && !@config.domain_context.empty?
57
+ prompt += <<~PROMPT
58
+
59
+ Domain context:
60
+ #{@config.domain_context}
61
+ PROMPT
62
+ end
63
+
64
+ prompt += <<~PROMPT
65
+
66
+ Given a set of high-cost queries and the schema they reference, group them by shared root cause.
67
+ For each group provide:
68
+ 1. The shared root cause (e.g., missing index, full table scan, implicit type conversion)
69
+ 2. The affected queries (numbered)
70
+ 3. A single fix that addresses all queries in the group (with exact SQL: CREATE INDEX, ALTER TABLE, etc.)
71
+ 4. Estimated performance impact
72
+
73
+ Respond with JSON: {"groups": "markdown with each group showing: the shared root cause, affected queries (numbered), the single fix that addresses all of them (with exact SQL), and estimated impact"}
74
+ PROMPT
75
+
76
+ prompt
77
+ end
78
+
79
+ def user_prompt(stats, schema)
80
+ formatted = stats.map.with_index(1) do |s, i|
81
+ "#{i}. SQL: #{s[:sql]}\n " \
82
+ "calls=#{s[:calls]}, avg_time_ms=#{s[:avg_time_ms]}, " \
83
+ "rows_ratio=#{s[:rows_ratio]}, rows_examined=#{s[:rows_examined]}, " \
84
+ "tmp_disk_tables=#{s[:tmp_disk_tables]}"
85
+ end.join("\n")
86
+
87
+ parts = ["High-cost queries (rows_ratio > #{ROWS_RATIO_THRESHOLD} OR avg_time_ms > #{AVG_TIME_THRESHOLD}):\n\n#{formatted}"]
88
+ parts << "Schema context:\n#{schema}" unless schema.empty?
89
+ parts.join("\n\n")
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Reviews MySQL configuration variables against best practices for
7
+ # the observed workload. Gathers SHOW GLOBAL VARIABLES (filtered to
8
+ # ~20 performance-relevant keys) and SHOW GLOBAL STATUS, then asks
9
+ # the LLM to identify misconfigurations.
10
+ class VariableReviewer
11
+ RELEVANT_VARIABLES = [
12
+ "innodb_buffer_pool_size",
13
+ "innodb_log_file_size",
14
+ "innodb_flush_log_at_trx_commit",
15
+ "max_connections",
16
+ "query_cache_type",
17
+ "sort_buffer_size",
18
+ "join_buffer_size",
19
+ "tmp_table_size",
20
+ "max_heap_table_size",
21
+ "thread_cache_size",
22
+ "table_open_cache",
23
+ "innodb_file_per_table",
24
+ "innodb_flush_method",
25
+ "binlog_format",
26
+ "sync_binlog",
27
+ "innodb_io_capacity",
28
+ "innodb_read_io_threads",
29
+ "innodb_write_io_threads",
30
+ "long_query_time",
31
+ "slow_query_log",
32
+ "performance_schema",
33
+ ].freeze
34
+
35
+ RELEVANT_STATUS_KEYS = [
36
+ "Innodb_buffer_pool_reads",
37
+ "Innodb_buffer_pool_read_requests",
38
+ "Created_tmp_disk_tables",
39
+ "Created_tmp_tables",
40
+ "Sort_merge_passes",
41
+ "Threads_created",
42
+ "Threads_connected",
43
+ "Max_used_connections",
44
+ "Slow_queries",
45
+ "Questions",
46
+ "Uptime",
47
+ ].freeze
48
+
49
+ def initialize(client, config, connection)
50
+ @client = client
51
+ @config = config
52
+ @connection = connection
53
+ end
54
+
55
+ def call
56
+ variables = fetch_variables
57
+ status = fetch_status
58
+
59
+ messages = [
60
+ { role: "system", content: system_prompt },
61
+ { role: "user", content: user_prompt(variables, status) },
62
+ ]
63
+ @client.chat(messages: messages)
64
+ end
65
+
66
+ private
67
+
68
+ def fetch_variables
69
+ result = @connection.exec_query("SHOW GLOBAL VARIABLES")
70
+ result.rows
71
+ .select { |row| RELEVANT_VARIABLES.include?(row[0]) }
72
+ .map { |row| [row[0], row[1]] }
73
+ end
74
+
75
+ def fetch_status
76
+ result = @connection.exec_query("SHOW GLOBAL STATUS")
77
+ result.rows
78
+ .select { |row| RELEVANT_STATUS_KEYS.include?(row[0]) }
79
+ .map { |row| [row[0], row[1]] }
80
+ end
81
+
82
+ def system_prompt
83
+ <<~PROMPT
84
+ You are a MySQL configuration reviewer. Analyze the server variables and status counters below, then identify misconfigurations and improvement opportunities. Consider:
85
+ - Buffer pool sizing relative to workload (hit rate from status counters)
86
+ - Temporary table spills to disk (tmp_table_size vs Created_tmp_disk_tables)
87
+ - Sort buffer and join buffer sizing
88
+ - Connection pool sizing (max_connections vs Max_used_connections)
89
+ - Thread cache effectiveness
90
+ - InnoDB flush and sync settings for durability vs performance trade-offs
91
+ - Slow query log configuration
92
+ - Binary log format suitability
93
+ #{@config.domain_context}
94
+ Respond with JSON: {"findings": "markdown-formatted analysis organized by severity (Critical, Warning, Suggestion). Include specific SET GLOBAL or my.cnf recommendations with before/after values."}
95
+ PROMPT
96
+ end
97
+
98
+ def user_prompt(variables, status)
99
+ lines = ["== Server Variables =="]
100
+ variables.each { |name, value| lines << "#{name} = #{value}" }
101
+ lines << ""
102
+ lines << "== Server Status Counters =="
103
+ status.each { |name, value| lines << "#{name} = #{value}" }
104
+ lines.join("\n")
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Produces a high-level executive summary of the query workload by
7
+ # pulling the top statements from performance_schema and asking the
8
+ # LLM to characterize read/write ratio, access patterns, waste
9
+ # concentration, and highest-leverage optimization opportunities.
10
+ #
11
+ # Construct with:
12
+ # connection - a Core::Connection implementation
13
+ # client - a Core::Ai::Client
14
+ # config - the Core::Ai::Config
15
+ #
16
+ # Call:
17
+ # .call() -> Hash with "digest" key containing markdown analysis
18
+ class WorkloadDigest
19
+ TOP_N = 30
20
+
21
+ def initialize(connection, client, config)
22
+ @connection = connection
23
+ @client = client
24
+ @config = config
25
+ end
26
+
27
+ def call
28
+ stats = Analysis::QueryStats.new(@connection).call(sort: "total_time", limit: TOP_N)
29
+ formatted = format_stats(stats)
30
+
31
+ messages = [
32
+ { role: "system", content: system_prompt },
33
+ { role: "user", content: user_prompt(formatted, stats.length) },
34
+ ]
35
+
36
+ @client.chat(messages: messages)
37
+ end
38
+
39
+ private
40
+
41
+ def system_prompt
42
+ prompt = <<~PROMPT
43
+ You are a MySQL performance analyst producing an executive workload digest.
44
+ PROMPT
45
+
46
+ if @config.domain_context && !@config.domain_context.empty?
47
+ prompt += <<~PROMPT
48
+
49
+ Domain context:
50
+ #{@config.domain_context}
51
+ PROMPT
52
+ end
53
+
54
+ prompt += <<~PROMPT
55
+
56
+ Analyze the provided query workload data and produce a concise executive summary covering:
57
+ 1. Read vs write ratio and overall workload characterization
58
+ 2. Access patterns (point lookups, range scans, full table scans, aggregations)
59
+ 3. Waste concentration — which queries examine many rows but return few
60
+ 4. Top 3 highest-leverage changes that would improve overall performance
61
+
62
+ Respond with JSON: {"digest": "markdown-formatted workload analysis"}
63
+ PROMPT
64
+
65
+ prompt
66
+ end
67
+
68
+ def user_prompt(formatted_stats, count)
69
+ <<~PROMPT
70
+ Top #{count} queries by total execution time:
71
+
72
+ #{formatted_stats}
73
+ PROMPT
74
+ end
75
+
76
+ def format_stats(stats)
77
+ stats.map.with_index(1) do |s, i|
78
+ "#{i}. SQL: #{s[:sql]}\n " \
79
+ "calls=#{s[:calls]}, avg_time_ms=#{s[:avg_time_ms]}, " \
80
+ "rows_ratio=#{s[:rows_ratio]}, tmp_disk_tables=#{s[:tmp_disk_tables]}"
81
+ end.join("\n")
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module MysqlGenius
4
4
  module Core
5
- VERSION = "0.7.2"
5
+ VERSION = "0.8.0"
6
6
  end
7
7
  end
@@ -26,13 +26,16 @@
26
26
  </div>
27
27
 
28
28
  <!-- Results Area -->
29
- <div id="query-results" class="mg-mt">
30
- <div id="results-alert" class="mg-hidden"></div>
31
- <div id="results-stats" class="mg-mb mg-hidden">
32
- <span id="results-row-count" class="mg-badge mg-badge-info"></span>
33
- <span id="results-time" class="mg-badge mg-badge-secondary"></span>
34
- <span id="results-truncated" class="mg-badge mg-badge-warning mg-hidden">Results truncated</span>
29
+ <div id="query-results" class="mg-mt mg-hidden">
30
+ <div style="display:flex;justify-content:space-between;align-items:center;">
31
+ <div id="results-stats" class="mg-mb mg-hidden">
32
+ <span id="results-row-count" class="mg-badge mg-badge-info"></span>
33
+ <span id="results-time" class="mg-badge mg-badge-secondary"></span>
34
+ <span id="results-truncated" class="mg-badge mg-badge-warning mg-hidden">Results truncated</span>
35
+ </div>
36
+ <button id="results-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm" style="margin-bottom:12px;">&#10005; Dismiss</button>
35
37
  </div>
38
+ <div id="results-alert" class="mg-hidden"></div>
36
39
  <div id="results-table-wrapper" class="mg-table-wrap mg-hidden">
37
40
  <table class="mg-table">
38
41
  <thead id="results-thead"></thead>
@@ -2,6 +2,23 @@
2
2
  <div class="mg-tab-content" id="tab-qstats">
3
3
  <div class="mg-row" style="justify-content:space-between;align-items:center;margin-bottom:12px;">
4
4
  <div class="mg-text-muted">Top queries from <code>performance_schema.events_statements_summary_by_digest</code>. <span id="qstats-count" class="mg-badge mg-badge-secondary"></span></div>
5
+ <div style="display:flex;gap:4px;">
6
+ <% if @ai_enabled %>
7
+ <button id="qstats-workload-digest" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; Workload Digest</button>
8
+ <button id="qstats-pattern-grouper" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; Pattern Grouper</button>
9
+ <% end %>
10
+ </div>
11
+ </div>
12
+ <div id="qstats-ai-result" class="mg-mt mg-hidden" style="margin-bottom:12px;">
13
+ <div class="mg-card">
14
+ <div class="mg-card-header">
15
+ <span id="qstats-ai-title"><strong>&#9889; AI Analysis</strong></span>
16
+ <button id="qstats-ai-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm">&#10005;</button>
17
+ </div>
18
+ <div class="mg-card-body">
19
+ <div id="qstats-ai-content" style="font-size:13px;"></div>
20
+ </div>
21
+ </div>
5
22
  </div>
6
23
  <div id="qstats-loading" class="mg-text-center mg-hidden"><span class="mg-spinner"></span> Loading...</div>
7
24
  <div id="qstats-error" class="mg-hidden"></div>
@@ -1,10 +1,15 @@
1
1
  <!-- Server Tab -->
2
2
  <div class="mg-tab-content" id="tab-server">
3
- <div class="mg-row" style="justify-content:flex-end;margin-bottom:12px;">
3
+ <div class="mg-row" style="justify-content:flex-end;margin-bottom:12px;flex-wrap:wrap;gap:4px;">
4
4
  <% if @ai_enabled && capability?(:slow_queries) %>
5
5
  <button id="server-root-cause" class="mg-btn mg-btn-primary mg-btn-sm">&#9889; Why is it slow?</button>
6
6
  <button id="server-anomaly" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; Anomaly Detection</button>
7
7
  <% end %>
8
+ <% if @ai_enabled %>
9
+ <button id="server-variable-review" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; Variable Review</button>
10
+ <button id="server-connection-advisor" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; Connection Advisor</button>
11
+ <button id="server-innodb-health" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; InnoDB Health</button>
12
+ <% end %>
8
13
  <button id="server-refresh" class="mg-btn mg-btn-outline-secondary mg-btn-sm">&#8635; Refresh</button>
9
14
  </div>
10
15
  <div id="server-loading" class="mg-text-center mg-hidden"><span class="mg-spinner"></span> Loading...</div>
@@ -2,7 +2,23 @@
2
2
  <div class="mg-tab-content" id="tab-unused">
3
3
  <div class="mg-row" style="justify-content:space-between;align-items:center;margin-bottom:12px;">
4
4
  <div class="mg-text-muted">Indexes with zero reads since last server restart, from <code>performance_schema</code>. <span id="unused-count" class="mg-badge mg-badge-secondary"></span></div>
5
- <button id="unused-refresh" class="mg-btn mg-btn-outline-secondary mg-btn-sm">&#8635; Refresh</button>
5
+ <div style="display:flex;gap:4px;">
6
+ <% if @ai_enabled %>
7
+ <button id="unused-index-planner" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; Index Planner</button>
8
+ <% end %>
9
+ <button id="unused-refresh" class="mg-btn mg-btn-outline-secondary mg-btn-sm">&#8635; Refresh</button>
10
+ </div>
11
+ </div>
12
+ <div id="unused-ai-result" class="mg-mt mg-hidden" style="margin-bottom:12px;">
13
+ <div class="mg-card">
14
+ <div class="mg-card-header">
15
+ <span id="unused-ai-title"><strong>&#9889; Index Planner</strong></span>
16
+ <button id="unused-ai-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm">&#10005;</button>
17
+ </div>
18
+ <div class="mg-card-body">
19
+ <div id="unused-ai-content" style="font-size:13px;"></div>
20
+ </div>
21
+ </div>
6
22
  </div>
7
23
  <div id="unused-loading" class="mg-text-center mg-hidden"><span class="mg-spinner"></span> Scanning...</div>
8
24
  <div id="unused-error" class="mg-hidden"></div>
@@ -62,6 +62,12 @@
62
62
  anomaly_detection: '<%= path_for(:anomaly_detection) %>',
63
63
  root_cause: '<%= path_for(:root_cause) %>',
64
64
  migration_risk: '<%= path_for(:migration_risk) %>',
65
+ variable_review: '<%= path_for(:variable_review) %>',
66
+ connection_advisor: '<%= path_for(:connection_advisor) %>',
67
+ workload_digest: '<%= path_for(:workload_digest) %>',
68
+ innodb_health: '<%= path_for(:innodb_health) %>',
69
+ index_planner: '<%= path_for(:index_planner) %>',
70
+ pattern_grouper: '<%= path_for(:pattern_grouper) %>',
65
71
  query_detail: '/queries/'
66
72
  };
67
73
 
@@ -301,6 +307,13 @@
301
307
  btn.classList.add('active');
302
308
  el('tab-' + name).classList.add('active');
303
309
  history.replaceState(null, '', location.pathname + '#' + name);
310
+
311
+ // Hide query results and AI responses when leaving the explorer tab
312
+ if (name !== 'explorer') {
313
+ hide(el('query-results'));
314
+ hide(el('explain-results'));
315
+ hide(el('ai-query-result'));
316
+ }
304
317
  if (name === 'dashboard') loadDashboard();
305
318
  if (name === 'slow') loadSlowQueries();
306
319
  if (name === 'indexes') loadDuplicateIndexes();
@@ -707,6 +720,7 @@
707
720
  }, function(json) {
708
721
  var cls = (json && json.timeout) ? 'mg-alert-warning' : 'mg-alert-danger';
709
722
  el('results-alert').innerHTML = '<div class="mg-alert ' + cls + '">' + escHtml((json && json.error) || 'An unexpected error occurred.') + '</div>';
723
+ show(el('query-results'));
710
724
  show(el('results-alert'));
711
725
  setBtnLoading(['vb-run', 'sql-run'], false);
712
726
  });
@@ -718,9 +732,11 @@
718
732
  hide(el('results-truncated'));
719
733
  el('results-thead').innerHTML = '';
720
734
  el('results-tbody').innerHTML = '';
735
+ show(el('query-results'));
721
736
  }
722
737
 
723
738
  function renderResults(data) {
739
+ show(el('query-results'));
724
740
  if (data.row_count === 0) {
725
741
  show(el('results-empty')); show(el('results-stats'));
726
742
  el('results-row-count').textContent = '0 rows';
@@ -748,6 +764,7 @@
748
764
  el('vb-explain').addEventListener('click', function() { var sql = buildSql(); if (sql) runExplain(sql); });
749
765
  el('sql-explain').addEventListener('click', function() { var sql = el('sql-input').value.trim(); if (sql) runExplain(sql); });
750
766
  el('explain-close').addEventListener('click', function() { hide(el('explain-results')); });
767
+ el('results-close').addEventListener('click', function() { hide(el('query-results')); });
751
768
 
752
769
  function runExplain(sql, fromSlowQuery) {
753
770
  lastExplainSql = sql;
@@ -1033,7 +1050,9 @@
1033
1050
  '<td class="mg-num"><strong>' + formatMb(t.total_mb) + '</strong></td>' +
1034
1051
  '<td class="mg-num">' + fragText + '</td>' +
1035
1052
  '<td>' + updated + '</td>' +
1036
- '<td>' + sizeBar(pct, color) + '</td>' +
1053
+ '<td>' + sizeBar(pct, color) +
1054
+ (t.needs_optimize ? ' <button class="mg-btn mg-btn-outline mg-btn-sm mg-table-optimize" data-table="' + escHtml(t.table) + '" style="margin-left:4px;">&#9889; AI Optimize</button>' : '') +
1055
+ '</td>' +
1037
1056
  '</tr>';
1038
1057
  }).join('');
1039
1058
  show(el('sizes-table-wrapper'));
@@ -1046,6 +1065,24 @@
1046
1065
 
1047
1066
  el('sizes-refresh').addEventListener('click', loadTableSizes);
1048
1067
 
1068
+ // AI Optimize button on tables with fragmentation
1069
+ document.addEventListener('click', function(e) {
1070
+ var btn = e.target.closest('.mg-table-optimize');
1071
+ if (!btn) return;
1072
+ var table = btn.dataset.table;
1073
+ btn.disabled = true;
1074
+ btn.innerHTML = '<span class="mg-spinner"></span>';
1075
+ aiCall(ROUTES.schema_review, { table: table }, function(data) {
1076
+ showAiQueryResult('Optimization: ' + table, formatFindings(data.findings || data.raw || 'No suggestions.'));
1077
+ btn.disabled = false;
1078
+ btn.innerHTML = '&#9889; AI Optimize';
1079
+ }, function(err) {
1080
+ showAiQueryResult('Error', '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>');
1081
+ btn.disabled = false;
1082
+ btn.innerHTML = '&#9889; AI Optimize';
1083
+ });
1084
+ });
1085
+
1049
1086
  // --- Query Stats ---
1050
1087
 
1051
1088
  function loadQueryStats() {
@@ -1387,6 +1424,148 @@
1387
1424
  }
1388
1425
  <% end %>
1389
1426
 
1427
+ // Server: Variable Review
1428
+ var varReviewBtn = el('server-variable-review');
1429
+ if (varReviewBtn) {
1430
+ varReviewBtn.addEventListener('click', function() {
1431
+ varReviewBtn.disabled = true;
1432
+ varReviewBtn.innerHTML = '<span class="mg-spinner"></span> Reviewing...';
1433
+ hide(el('server-ai-result'));
1434
+ aiCall(ROUTES.variable_review, {}, function(data) {
1435
+ el('server-ai-title').innerHTML = '<strong>&#9889; Variable Configuration Review</strong>';
1436
+ el('server-ai-content').innerHTML = formatMarkdown(data.findings || data.raw || 'No findings.') + copyButton('server-ai-content');
1437
+ show(el('server-ai-result'));
1438
+ varReviewBtn.disabled = false;
1439
+ varReviewBtn.innerHTML = '&#9889; Variable Review';
1440
+ }, function(err) {
1441
+ el('server-ai-title').innerHTML = '<strong>&#9889; Error</strong>';
1442
+ el('server-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
1443
+ show(el('server-ai-result'));
1444
+ varReviewBtn.disabled = false;
1445
+ varReviewBtn.innerHTML = '&#9889; Variable Review';
1446
+ });
1447
+ });
1448
+ }
1449
+
1450
+ // Server: Connection Advisor
1451
+ var connAdvisorBtn = el('server-connection-advisor');
1452
+ if (connAdvisorBtn) {
1453
+ connAdvisorBtn.addEventListener('click', function() {
1454
+ connAdvisorBtn.disabled = true;
1455
+ connAdvisorBtn.innerHTML = '<span class="mg-spinner"></span> Diagnosing...';
1456
+ hide(el('server-ai-result'));
1457
+ aiCall(ROUTES.connection_advisor, {}, function(data) {
1458
+ el('server-ai-title').innerHTML = '<strong>&#9889; Connection Pressure Diagnosis</strong>';
1459
+ el('server-ai-content').innerHTML = formatMarkdown(data.diagnosis || data.raw || 'No diagnosis.') + copyButton('server-ai-content');
1460
+ show(el('server-ai-result'));
1461
+ connAdvisorBtn.disabled = false;
1462
+ connAdvisorBtn.innerHTML = '&#9889; Connection Advisor';
1463
+ }, function(err) {
1464
+ el('server-ai-title').innerHTML = '<strong>&#9889; Error</strong>';
1465
+ el('server-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
1466
+ show(el('server-ai-result'));
1467
+ connAdvisorBtn.disabled = false;
1468
+ connAdvisorBtn.innerHTML = '&#9889; Connection Advisor';
1469
+ });
1470
+ });
1471
+ }
1472
+
1473
+ // Server: InnoDB Health
1474
+ var innodbHealthBtn = el('server-innodb-health');
1475
+ if (innodbHealthBtn) {
1476
+ innodbHealthBtn.addEventListener('click', function() {
1477
+ innodbHealthBtn.disabled = true;
1478
+ innodbHealthBtn.innerHTML = '<span class="mg-spinner"></span> Interpreting...';
1479
+ hide(el('server-ai-result'));
1480
+ aiCall(ROUTES.innodb_health, {}, function(data) {
1481
+ el('server-ai-title').innerHTML = '<strong>&#9889; InnoDB Health Interpretation</strong>';
1482
+ el('server-ai-content').innerHTML = formatMarkdown(data.findings || data.raw || 'No findings.') + copyButton('server-ai-content');
1483
+ show(el('server-ai-result'));
1484
+ innodbHealthBtn.disabled = false;
1485
+ innodbHealthBtn.innerHTML = '&#9889; InnoDB Health';
1486
+ }, function(err) {
1487
+ el('server-ai-title').innerHTML = '<strong>&#9889; Error</strong>';
1488
+ el('server-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
1489
+ show(el('server-ai-result'));
1490
+ innodbHealthBtn.disabled = false;
1491
+ innodbHealthBtn.innerHTML = '&#9889; InnoDB Health';
1492
+ });
1493
+ });
1494
+ }
1495
+
1496
+ // Query Stats: Workload Digest
1497
+ var workloadBtn = el('qstats-workload-digest');
1498
+ if (workloadBtn) {
1499
+ workloadBtn.addEventListener('click', function() {
1500
+ workloadBtn.disabled = true;
1501
+ workloadBtn.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
1502
+ hide(el('qstats-ai-result'));
1503
+ aiCall(ROUTES.workload_digest, {}, function(data) {
1504
+ el('qstats-ai-title').innerHTML = '<strong>&#9889; Workload Digest</strong>';
1505
+ el('qstats-ai-content').innerHTML = formatMarkdown(data.digest || data.raw || 'No digest available.') + copyButton('qstats-ai-content');
1506
+ show(el('qstats-ai-result'));
1507
+ workloadBtn.disabled = false;
1508
+ workloadBtn.innerHTML = '&#9889; Workload Digest';
1509
+ }, function(err) {
1510
+ el('qstats-ai-title').innerHTML = '<strong>&#9889; Error</strong>';
1511
+ el('qstats-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
1512
+ show(el('qstats-ai-result'));
1513
+ workloadBtn.disabled = false;
1514
+ workloadBtn.innerHTML = '&#9889; Workload Digest';
1515
+ });
1516
+ });
1517
+
1518
+ el('qstats-ai-close').addEventListener('click', function() { hide(el('qstats-ai-result')); });
1519
+ }
1520
+
1521
+ // Query Stats: Pattern Grouper
1522
+ var patternBtn = el('qstats-pattern-grouper');
1523
+ if (patternBtn) {
1524
+ patternBtn.addEventListener('click', function() {
1525
+ patternBtn.disabled = true;
1526
+ patternBtn.innerHTML = '<span class="mg-spinner"></span> Grouping...';
1527
+ hide(el('qstats-ai-result'));
1528
+ aiCall(ROUTES.pattern_grouper, {}, function(data) {
1529
+ el('qstats-ai-title').innerHTML = '<strong>&#9889; Slow Query Pattern Groups</strong>';
1530
+ el('qstats-ai-content').innerHTML = formatMarkdown(data.groups || data.raw || 'No patterns found.') + copyButton('qstats-ai-content');
1531
+ show(el('qstats-ai-result'));
1532
+ patternBtn.disabled = false;
1533
+ patternBtn.innerHTML = '&#9889; Pattern Grouper';
1534
+ }, function(err) {
1535
+ el('qstats-ai-title').innerHTML = '<strong>&#9889; Error</strong>';
1536
+ el('qstats-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
1537
+ show(el('qstats-ai-result'));
1538
+ patternBtn.disabled = false;
1539
+ patternBtn.innerHTML = '&#9889; Pattern Grouper';
1540
+ });
1541
+ });
1542
+ }
1543
+
1544
+ // Unused Indexes: Index Planner
1545
+ var indexPlannerBtn = el('unused-index-planner');
1546
+ if (indexPlannerBtn) {
1547
+ indexPlannerBtn.addEventListener('click', function() {
1548
+ indexPlannerBtn.disabled = true;
1549
+ indexPlannerBtn.innerHTML = '<span class="mg-spinner"></span> Planning...';
1550
+ hide(el('unused-ai-result'));
1551
+ aiCall(ROUTES.index_planner, {}, function(data) {
1552
+ el('unused-ai-title').innerHTML = '<strong>&#9889; Index Consolidation Plan</strong>';
1553
+ el('unused-ai-content').innerHTML = formatMarkdown(data.plan || data.raw || 'No plan generated.') + copyButton('unused-ai-content');
1554
+ show(el('unused-ai-result'));
1555
+ indexPlannerBtn.disabled = false;
1556
+ indexPlannerBtn.innerHTML = '&#9889; Index Planner';
1557
+ }, function(err) {
1558
+ el('unused-ai-title').innerHTML = '<strong>&#9889; Error</strong>';
1559
+ el('unused-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
1560
+ show(el('unused-ai-result'));
1561
+ indexPlannerBtn.disabled = false;
1562
+ indexPlannerBtn.innerHTML = '&#9889; Index Planner';
1563
+ });
1564
+ });
1565
+
1566
+ el('unused-ai-close').addEventListener('click', function() { hide(el('unused-ai-result')); });
1567
+ }
1568
+
1390
1569
  // Schema Review
1391
1570
  var schemaBtn = el('schema-review-btn');
1392
1571
  if (schemaBtn) {
@@ -41,6 +41,12 @@ require "mysql_genius/core/ai/schema_review"
41
41
  require "mysql_genius/core/ai/rewrite_query"
42
42
  require "mysql_genius/core/ai/index_advisor"
43
43
  require "mysql_genius/core/ai/migration_risk"
44
+ require "mysql_genius/core/ai/variable_reviewer"
45
+ require "mysql_genius/core/ai/connection_advisor"
46
+ require "mysql_genius/core/ai/workload_digest"
47
+ require "mysql_genius/core/ai/innodb_interpreter"
48
+ require "mysql_genius/core/ai/index_planner"
49
+ require "mysql_genius/core/ai/pattern_grouper"
44
50
  require "mysql_genius/core/analysis/table_sizes"
45
51
  require "mysql_genius/core/analysis/duplicate_indexes"
46
52
  require "mysql_genius/core/analysis/query_stats"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mysql_genius-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antarr Byrd
@@ -24,14 +24,20 @@ files:
24
24
  - lib/mysql_genius/core.rb
25
25
  - lib/mysql_genius/core/ai/client.rb
26
26
  - lib/mysql_genius/core/ai/config.rb
27
+ - lib/mysql_genius/core/ai/connection_advisor.rb
27
28
  - lib/mysql_genius/core/ai/describe_query.rb
28
29
  - lib/mysql_genius/core/ai/index_advisor.rb
30
+ - lib/mysql_genius/core/ai/index_planner.rb
31
+ - lib/mysql_genius/core/ai/innodb_interpreter.rb
29
32
  - lib/mysql_genius/core/ai/migration_risk.rb
30
33
  - lib/mysql_genius/core/ai/optimization.rb
34
+ - lib/mysql_genius/core/ai/pattern_grouper.rb
31
35
  - lib/mysql_genius/core/ai/rewrite_query.rb
32
36
  - lib/mysql_genius/core/ai/schema_context_builder.rb
33
37
  - lib/mysql_genius/core/ai/schema_review.rb
34
38
  - lib/mysql_genius/core/ai/suggestion.rb
39
+ - lib/mysql_genius/core/ai/variable_reviewer.rb
40
+ - lib/mysql_genius/core/ai/workload_digest.rb
35
41
  - lib/mysql_genius/core/analysis/columns.rb
36
42
  - lib/mysql_genius/core/analysis/duplicate_indexes.rb
37
43
  - lib/mysql_genius/core/analysis/query_stats.rb