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 +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/mysql_genius/core/ai/client.rb +2 -2
- data/lib/mysql_genius/core/ai/connection_advisor.rb +92 -0
- data/lib/mysql_genius/core/ai/index_planner.rb +91 -0
- data/lib/mysql_genius/core/ai/innodb_interpreter.rb +74 -0
- data/lib/mysql_genius/core/ai/pattern_grouper.rb +94 -0
- data/lib/mysql_genius/core/ai/variable_reviewer.rb +109 -0
- data/lib/mysql_genius/core/ai/workload_digest.rb +86 -0
- data/lib/mysql_genius/core/version.rb +1 -1
- data/lib/mysql_genius/core/views/mysql_genius/queries/_shared_results.html.erb +9 -6
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_query_stats.html.erb +17 -0
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_server.html.erb +6 -1
- data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_unused_indexes.html.erb +17 -1
- data/lib/mysql_genius/core/views/mysql_genius/queries/dashboard.html.erb +180 -1
- data/lib/mysql_genius/core.rb +6 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5075a444aed755238df78580fbd0e8ae79c2cb999ffc7504ff6af1ae8c08c8e7
|
|
4
|
+
data.tar.gz: 9edad6c0620a8c4434b716995ac7e0c2af52494c0f61912d45a1496468fa97e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
@@ -26,13 +26,16 @@
|
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
28
|
<!-- Results Area -->
|
|
29
|
-
<div id="query-results" class="mg-mt">
|
|
30
|
-
<div
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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;">✕ 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">⚡ Workload Digest</button>
|
|
8
|
+
<button id="qstats-pattern-grouper" class="mg-btn mg-btn-outline mg-btn-sm">⚡ 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>⚡ AI Analysis</strong></span>
|
|
16
|
+
<button id="qstats-ai-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm">✕</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">⚡ Why is it slow?</button>
|
|
6
6
|
<button id="server-anomaly" class="mg-btn mg-btn-outline mg-btn-sm">⚡ 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">⚡ Variable Review</button>
|
|
10
|
+
<button id="server-connection-advisor" class="mg-btn mg-btn-outline mg-btn-sm">⚡ Connection Advisor</button>
|
|
11
|
+
<button id="server-innodb-health" class="mg-btn mg-btn-outline mg-btn-sm">⚡ InnoDB Health</button>
|
|
12
|
+
<% end %>
|
|
8
13
|
<button id="server-refresh" class="mg-btn mg-btn-outline-secondary mg-btn-sm">↻ 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
|
-
<
|
|
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">⚡ Index Planner</button>
|
|
8
|
+
<% end %>
|
|
9
|
+
<button id="unused-refresh" class="mg-btn mg-btn-outline-secondary mg-btn-sm">↻ 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>⚡ Index Planner</strong></span>
|
|
16
|
+
<button id="unused-ai-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm">✕</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) +
|
|
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;">⚡ 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 = '⚡ 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 = '⚡ 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>⚡ 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 = '⚡ Variable Review';
|
|
1440
|
+
}, function(err) {
|
|
1441
|
+
el('server-ai-title').innerHTML = '<strong>⚡ 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 = '⚡ 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>⚡ 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 = '⚡ Connection Advisor';
|
|
1463
|
+
}, function(err) {
|
|
1464
|
+
el('server-ai-title').innerHTML = '<strong>⚡ 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 = '⚡ 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>⚡ 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 = '⚡ InnoDB Health';
|
|
1486
|
+
}, function(err) {
|
|
1487
|
+
el('server-ai-title').innerHTML = '<strong>⚡ 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 = '⚡ 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>⚡ 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 = '⚡ Workload Digest';
|
|
1509
|
+
}, function(err) {
|
|
1510
|
+
el('qstats-ai-title').innerHTML = '<strong>⚡ 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 = '⚡ 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>⚡ 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 = '⚡ Pattern Grouper';
|
|
1534
|
+
}, function(err) {
|
|
1535
|
+
el('qstats-ai-title').innerHTML = '<strong>⚡ 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 = '⚡ 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>⚡ 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 = '⚡ Index Planner';
|
|
1557
|
+
}, function(err) {
|
|
1558
|
+
el('unused-ai-title').innerHTML = '<strong>⚡ 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 = '⚡ 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) {
|
data/lib/mysql_genius/core.rb
CHANGED
|
@@ -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.
|
|
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
|