mysql_genius-core 0.7.1 → 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 +20 -0
- data/lib/mysql_genius/core/ai/client.rb +44 -8
- data/lib/mysql_genius/core/ai/config.rb +2 -1
- 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 +208 -18
- data/lib/mysql_genius/core/views/mysql_genius/queries/query_detail.html.erb +4 -4
- data/lib/mysql_genius/core.rb +6 -0
- metadata +8 -2
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,25 @@
|
|
|
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
|
+
|
|
14
|
+
## 0.7.2
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Anthropic Messages API support** in `Core::Ai::Client` — detects `:x_api_key` auth style, sends `x-api-key` + `anthropic-version` headers, uses top-level `system` parameter and `content[0].text` response parsing.
|
|
18
|
+
- **`max_tokens` field** on `Core::Ai::Config` (default 4096) — sent in both OpenAI and Anthropic request bodies.
|
|
19
|
+
- **Copy response button** on all AI result sections in the shared dashboard template.
|
|
20
|
+
- **Dark mode CSS classes** for AI result sections (`mg-ai-section`, `mg-ai-danger`, `mg-ai-warning`, `mg-ai-info`) replacing hardcoded inline styles.
|
|
21
|
+
- **`capability?(:standalone_header)`** guard on the dashboard heading — hides it when the rendering adapter provides its own header.
|
|
22
|
+
|
|
3
23
|
## 0.7.1
|
|
4
24
|
|
|
5
25
|
### Fixed
|
|
@@ -27,12 +27,11 @@ module MysqlGenius
|
|
|
27
27
|
|
|
28
28
|
raise NotConfigured, "AI is not configured" unless @config.enabled?
|
|
29
29
|
|
|
30
|
-
body =
|
|
31
|
-
messages
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
body[:model] = @config.model if @config.model && !@config.model.empty?
|
|
30
|
+
body = if anthropic?
|
|
31
|
+
build_anthropic_body(messages, temperature)
|
|
32
|
+
else
|
|
33
|
+
build_openai_body(messages, temperature)
|
|
34
|
+
end
|
|
36
35
|
|
|
37
36
|
response = post_with_redirects(URI(@config.endpoint), body.to_json)
|
|
38
37
|
parsed = JSON.parse(response.body)
|
|
@@ -41,7 +40,11 @@ module MysqlGenius
|
|
|
41
40
|
raise ApiError, "AI API error: #{parsed["error"]["message"] || parsed["error"]}"
|
|
42
41
|
end
|
|
43
42
|
|
|
44
|
-
content =
|
|
43
|
+
content = if anthropic?
|
|
44
|
+
parsed.dig("content", 0, "text")
|
|
45
|
+
else
|
|
46
|
+
parsed.dig("choices", 0, "message", "content")
|
|
47
|
+
end
|
|
45
48
|
raise ApiError, "No content in AI response" if content.nil?
|
|
46
49
|
|
|
47
50
|
parse_json_content(content)
|
|
@@ -49,6 +52,35 @@ module MysqlGenius
|
|
|
49
52
|
|
|
50
53
|
private
|
|
51
54
|
|
|
55
|
+
def anthropic?
|
|
56
|
+
@config.auth_style == :x_api_key
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_openai_body(messages, temperature)
|
|
60
|
+
body = {
|
|
61
|
+
messages: messages,
|
|
62
|
+
response_format: { type: "json_object" },
|
|
63
|
+
temperature: temperature,
|
|
64
|
+
}
|
|
65
|
+
body[:max_tokens] = @config.max_tokens.to_i if @config.max_tokens
|
|
66
|
+
body[:model] = @config.model if @config.model && !@config.model.empty?
|
|
67
|
+
body
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_anthropic_body(messages, temperature)
|
|
71
|
+
system_text = messages.select { |m| m[:role] == "system" }.map { |m| m[:content] }.join("\n\n")
|
|
72
|
+
user_messages = messages.reject { |m| m[:role] == "system" }
|
|
73
|
+
|
|
74
|
+
body = {
|
|
75
|
+
messages: user_messages,
|
|
76
|
+
max_tokens: (@config.max_tokens || 4096).to_i,
|
|
77
|
+
temperature: temperature,
|
|
78
|
+
}
|
|
79
|
+
body[:system] = system_text unless system_text.empty?
|
|
80
|
+
body[:model] = @config.model if @config.model && !@config.model.empty?
|
|
81
|
+
body
|
|
82
|
+
end
|
|
83
|
+
|
|
52
84
|
def parse_json_content(content)
|
|
53
85
|
JSON.parse(content)
|
|
54
86
|
rescue JSON::ParserError
|
|
@@ -78,8 +110,12 @@ module MysqlGenius
|
|
|
78
110
|
|
|
79
111
|
request = Net::HTTP::Post.new(uri)
|
|
80
112
|
request["Content-Type"] = "application/json"
|
|
81
|
-
|
|
113
|
+
case @config.auth_style
|
|
114
|
+
when :bearer
|
|
82
115
|
request["Authorization"] = "Bearer #{@config.api_key}"
|
|
116
|
+
when :x_api_key
|
|
117
|
+
request["x-api-key"] = @config.api_key
|
|
118
|
+
request["anthropic-version"] = "2023-06-01"
|
|
83
119
|
else
|
|
84
120
|
request["api-key"] = @config.api_key
|
|
85
121
|
end
|
|
@@ -26,10 +26,11 @@ module MysqlGenius
|
|
|
26
26
|
:auth_style,
|
|
27
27
|
:system_context,
|
|
28
28
|
:domain_context,
|
|
29
|
+
:max_tokens,
|
|
29
30
|
keyword_init: true,
|
|
30
31
|
) do
|
|
31
32
|
def initialize(**kwargs)
|
|
32
|
-
super(domain_context: "", **kwargs)
|
|
33
|
+
super(domain_context: "", max_tokens: 4096, **kwargs)
|
|
33
34
|
freeze
|
|
34
35
|
end
|
|
35
36
|
|
|
@@ -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>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
<% if capability?(:standalone_header) %>
|
|
1
2
|
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
2
3
|
<h4>🐘 MySQLGenius</h4>
|
|
3
4
|
<button class="mg-theme-toggle" id="mg-theme-btn" title="Toggle dark/light theme" onclick="(function(){var d=document.documentElement,t=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',t);localStorage.setItem('mg-theme',t);document.getElementById('mg-theme-btn').textContent=t==='dark'?'\u2600\uFE0F':'\uD83C\uDF19';})()">
|
|
4
5
|
<script>document.write(document.documentElement.getAttribute('data-theme')==='dark'?'\u2600\uFE0F':'\uD83C\uDF19')</script>
|
|
5
6
|
</button>
|
|
6
7
|
</div>
|
|
8
|
+
<% end %>
|
|
7
9
|
|
|
8
10
|
<div class="mg-tabs">
|
|
9
11
|
<button class="mg-tab active" data-tab="dashboard">Dashboard</button>
|
|
@@ -60,6 +62,12 @@
|
|
|
60
62
|
anomaly_detection: '<%= path_for(:anomaly_detection) %>',
|
|
61
63
|
root_cause: '<%= path_for(:root_cause) %>',
|
|
62
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) %>',
|
|
63
71
|
query_detail: '/queries/'
|
|
64
72
|
};
|
|
65
73
|
|
|
@@ -299,6 +307,13 @@
|
|
|
299
307
|
btn.classList.add('active');
|
|
300
308
|
el('tab-' + name).classList.add('active');
|
|
301
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
|
+
}
|
|
302
317
|
if (name === 'dashboard') loadDashboard();
|
|
303
318
|
if (name === 'slow') loadSlowQueries();
|
|
304
319
|
if (name === 'indexes') loadDuplicateIndexes();
|
|
@@ -705,6 +720,7 @@
|
|
|
705
720
|
}, function(json) {
|
|
706
721
|
var cls = (json && json.timeout) ? 'mg-alert-warning' : 'mg-alert-danger';
|
|
707
722
|
el('results-alert').innerHTML = '<div class="mg-alert ' + cls + '">' + escHtml((json && json.error) || 'An unexpected error occurred.') + '</div>';
|
|
723
|
+
show(el('query-results'));
|
|
708
724
|
show(el('results-alert'));
|
|
709
725
|
setBtnLoading(['vb-run', 'sql-run'], false);
|
|
710
726
|
});
|
|
@@ -716,9 +732,11 @@
|
|
|
716
732
|
hide(el('results-truncated'));
|
|
717
733
|
el('results-thead').innerHTML = '';
|
|
718
734
|
el('results-tbody').innerHTML = '';
|
|
735
|
+
show(el('query-results'));
|
|
719
736
|
}
|
|
720
737
|
|
|
721
738
|
function renderResults(data) {
|
|
739
|
+
show(el('query-results'));
|
|
722
740
|
if (data.row_count === 0) {
|
|
723
741
|
show(el('results-empty')); show(el('results-stats'));
|
|
724
742
|
el('results-row-count').textContent = '0 rows';
|
|
@@ -746,6 +764,7 @@
|
|
|
746
764
|
el('vb-explain').addEventListener('click', function() { var sql = buildSql(); if (sql) runExplain(sql); });
|
|
747
765
|
el('sql-explain').addEventListener('click', function() { var sql = el('sql-input').value.trim(); if (sql) runExplain(sql); });
|
|
748
766
|
el('explain-close').addEventListener('click', function() { hide(el('explain-results')); });
|
|
767
|
+
el('results-close').addEventListener('click', function() { hide(el('query-results')); });
|
|
749
768
|
|
|
750
769
|
function runExplain(sql, fromSlowQuery) {
|
|
751
770
|
lastExplainSql = sql;
|
|
@@ -825,7 +844,7 @@
|
|
|
825
844
|
});
|
|
826
845
|
|
|
827
846
|
ajax('POST', ROUTES.optimize, data, function(resp) {
|
|
828
|
-
el('optimize-content').innerHTML = formatMarkdown(resp.suggestions || 'No suggestions available.');
|
|
847
|
+
el('optimize-content').innerHTML = formatMarkdown(resp.suggestions || 'No suggestions available.') + copyButton('optimize-content');
|
|
829
848
|
show(el('optimize-results'));
|
|
830
849
|
explainOptimize.disabled = false;
|
|
831
850
|
explainOptimize.innerHTML = '⚡ AI Optimization';
|
|
@@ -1031,7 +1050,9 @@
|
|
|
1031
1050
|
'<td class="mg-num"><strong>' + formatMb(t.total_mb) + '</strong></td>' +
|
|
1032
1051
|
'<td class="mg-num">' + fragText + '</td>' +
|
|
1033
1052
|
'<td>' + updated + '</td>' +
|
|
1034
|
-
'<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>' +
|
|
1035
1056
|
'</tr>';
|
|
1036
1057
|
}).join('');
|
|
1037
1058
|
show(el('sizes-table-wrapper'));
|
|
@@ -1044,6 +1065,24 @@
|
|
|
1044
1065
|
|
|
1045
1066
|
el('sizes-refresh').addEventListener('click', loadTableSizes);
|
|
1046
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
|
+
|
|
1047
1086
|
// --- Query Stats ---
|
|
1048
1087
|
|
|
1049
1088
|
function loadQueryStats() {
|
|
@@ -1230,9 +1269,25 @@
|
|
|
1230
1269
|
});
|
|
1231
1270
|
}
|
|
1232
1271
|
|
|
1272
|
+
function copyButton(targetId) {
|
|
1273
|
+
return '<div style="text-align:right;margin-top:8px;"><button class="mg-btn mg-btn-outline-secondary mg-btn-sm mg-copy-ai" data-target="' + targetId + '">Copy response</button></div>';
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
document.addEventListener('click', function(e) {
|
|
1277
|
+
var btn = e.target.closest('.mg-copy-ai');
|
|
1278
|
+
if (!btn) return;
|
|
1279
|
+
var target = el(btn.dataset.target);
|
|
1280
|
+
if (!target) return;
|
|
1281
|
+
var text = target.innerText || target.textContent;
|
|
1282
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
1283
|
+
btn.textContent = 'Copied!';
|
|
1284
|
+
setTimeout(function() { btn.textContent = 'Copy response'; }, 2000);
|
|
1285
|
+
});
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1233
1288
|
function showAiQueryResult(title, html) {
|
|
1234
1289
|
el('ai-query-title').innerHTML = '<strong>⚡ ' + escHtml(title) + '</strong>';
|
|
1235
|
-
el('ai-query-content').innerHTML = html;
|
|
1290
|
+
el('ai-query-content').innerHTML = html + copyButton('ai-query-content');
|
|
1236
1291
|
show(el('ai-query-result'));
|
|
1237
1292
|
}
|
|
1238
1293
|
|
|
@@ -1306,7 +1361,7 @@
|
|
|
1306
1361
|
});
|
|
1307
1362
|
|
|
1308
1363
|
aiCall(ROUTES.index_advisor, data, function(resp) {
|
|
1309
|
-
el('optimize-content').innerHTML = formatMarkdown(resp.indexes || resp.raw || 'No suggestions.');
|
|
1364
|
+
el('optimize-content').innerHTML = formatMarkdown(resp.indexes || resp.raw || 'No suggestions.') + copyButton('optimize-content');
|
|
1310
1365
|
show(el('optimize-results'));
|
|
1311
1366
|
indexAdvisor.disabled = false;
|
|
1312
1367
|
indexAdvisor.innerHTML = '⚡ Index Advisor';
|
|
@@ -1329,7 +1384,7 @@
|
|
|
1329
1384
|
hide(el('server-ai-result'));
|
|
1330
1385
|
aiCall(ROUTES.root_cause, {}, function(data) {
|
|
1331
1386
|
el('server-ai-title').innerHTML = '<strong>⚡ Root Cause Analysis</strong>';
|
|
1332
|
-
el('server-ai-content').innerHTML = formatMarkdown(data.diagnosis || data.raw || 'No diagnosis.');
|
|
1387
|
+
el('server-ai-content').innerHTML = formatMarkdown(data.diagnosis || data.raw || 'No diagnosis.') + copyButton('server-ai-content');
|
|
1333
1388
|
show(el('server-ai-result'));
|
|
1334
1389
|
rootCauseBtn.disabled = false;
|
|
1335
1390
|
rootCauseBtn.innerHTML = '⚡ Why is it slow?';
|
|
@@ -1354,7 +1409,7 @@
|
|
|
1354
1409
|
hide(el('server-ai-result'));
|
|
1355
1410
|
aiCall(ROUTES.anomaly_detection, {}, function(data) {
|
|
1356
1411
|
el('server-ai-title').innerHTML = '<strong>⚡ Query Health Report</strong>';
|
|
1357
|
-
el('server-ai-content').innerHTML = formatMarkdown(data.report || data.raw || 'No anomalies detected.');
|
|
1412
|
+
el('server-ai-content').innerHTML = formatMarkdown(data.report || data.raw || 'No anomalies detected.') + copyButton('server-ai-content');
|
|
1358
1413
|
show(el('server-ai-result'));
|
|
1359
1414
|
anomalyBtn.disabled = false;
|
|
1360
1415
|
anomalyBtn.innerHTML = '⚡ Anomaly Detection';
|
|
@@ -1369,6 +1424,148 @@
|
|
|
1369
1424
|
}
|
|
1370
1425
|
<% end %>
|
|
1371
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
|
+
|
|
1372
1569
|
// Schema Review
|
|
1373
1570
|
var schemaBtn = el('schema-review-btn');
|
|
1374
1571
|
if (schemaBtn) {
|
|
@@ -1378,7 +1575,7 @@
|
|
|
1378
1575
|
hide(el('schema-result'));
|
|
1379
1576
|
var table = el('schema-table').value;
|
|
1380
1577
|
aiCall(ROUTES.schema_review, { table: table }, function(data) {
|
|
1381
|
-
el('schema-result-content').innerHTML = formatFindings(data.findings || data.raw || '');
|
|
1578
|
+
el('schema-result-content').innerHTML = formatFindings(data.findings || data.raw || '') + copyButton('schema-result-content');
|
|
1382
1579
|
show(el('schema-result'));
|
|
1383
1580
|
schemaBtn.disabled = false;
|
|
1384
1581
|
schemaBtn.innerHTML = '⚡ Analyze Schema';
|
|
@@ -1409,7 +1606,7 @@
|
|
|
1409
1606
|
var level = (data.risk_level || '').toLowerCase();
|
|
1410
1607
|
var badgeClass = level === 'critical' ? 'mg-badge-danger' : level === 'high' ? 'mg-badge-danger' : level === 'medium' ? 'mg-badge-warning' : 'mg-badge-info';
|
|
1411
1608
|
el('migration-risk-badge').innerHTML = level ? '<span class="mg-badge ' + badgeClass + '" style="font-size:14px;padding:4px 12px;">Risk: ' + level.toUpperCase() + '</span>' : '';
|
|
1412
|
-
el('migration-result-content').innerHTML = formatFindings(data.assessment || data.raw || '');
|
|
1609
|
+
el('migration-result-content').innerHTML = formatFindings(data.assessment || data.raw || '') + copyButton('migration-result-content');
|
|
1413
1610
|
show(el('migration-result'));
|
|
1414
1611
|
migrationBtn.disabled = false;
|
|
1415
1612
|
migrationBtn.innerHTML = '⚡ Assess Risk';
|
|
@@ -1565,19 +1762,12 @@
|
|
|
1565
1762
|
|
|
1566
1763
|
if (!sections.length) return formatMarkdown(text);
|
|
1567
1764
|
|
|
1568
|
-
var badgeColors = { danger: '#dc3545', warning: '#ffc107', info: '#17a2b8' };
|
|
1569
|
-
var bgColors = { danger: '#fff5f5', warning: '#fffbeb', info: '#f0f9ff' };
|
|
1570
|
-
var borderColors = { danger: '#f5c6cb', warning: '#ffeeba', info: '#bee5eb' };
|
|
1571
|
-
|
|
1572
1765
|
return sections.map(function(sec) {
|
|
1573
1766
|
var content = formatMarkdown(sec.lines.join('\n').trim());
|
|
1574
1767
|
if (!content || content === '<br>') return '';
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
return '<div class="mg-card mg-mb" style="border-left:4px solid ' + badge + ';background:' + bg + ';border-color:' + border + ';">' +
|
|
1579
|
-
(sec.title ? '<div class="mg-card-header" style="background:transparent;border-bottom:1px solid ' + border + ';"><strong>' + escHtml(sec.title) + '</strong></div>' : '') +
|
|
1580
|
-
'<div class="mg-card-body" style="font-size:13px;">' + content + '</div></div>';
|
|
1768
|
+
return '<div class="mg-ai-section mg-ai-' + sec.severity + ' mg-mb">' +
|
|
1769
|
+
(sec.title ? '<div class="mg-ai-section-header"><strong>' + escHtml(sec.title) + '</strong></div>' : '') +
|
|
1770
|
+
'<div class="mg-ai-section-body">' + content + '</div></div>';
|
|
1581
1771
|
}).filter(function(s) { return s; }).join('');
|
|
1582
1772
|
}
|
|
1583
1773
|
})();
|
|
@@ -411,13 +411,13 @@
|
|
|
411
411
|
// --- Explain ---
|
|
412
412
|
|
|
413
413
|
// performance_schema DIGEST_TEXT uses normalized spacing that isn't valid SQL:
|
|
414
|
-
// "SELECT COUNT ( * ) FROM `riders`"
|
|
415
|
-
// "... IN ( ... )"
|
|
414
|
+
// "SELECT COUNT ( * ) FROM `riders`" -> needs to become "SELECT COUNT(*) FROM `riders`"
|
|
415
|
+
// "... IN ( ... )" -> "... IN (...)"
|
|
416
416
|
// Also replaces placeholder ? with 1 so EXPLAIN can parse it.
|
|
417
417
|
function normalizeDigestSql(sql) {
|
|
418
418
|
return sql
|
|
419
|
-
.replace(/\(\s+/g, '(') // "( "
|
|
420
|
-
.replace(/\s+\)/g, ')') // " )"
|
|
419
|
+
.replace(/\(\s+/g, '(') // "( " -> "("
|
|
420
|
+
.replace(/\s+\)/g, ')') // " )" -> ")"
|
|
421
421
|
.replace(/\s*,\s*/g, ', ') // normalize comma spacing
|
|
422
422
|
.replace(/\?/g, '1'); // replace ? placeholders with literal 1
|
|
423
423
|
}
|
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,14 +1,14 @@
|
|
|
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
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Shared library used by the mysql_genius Rails engine and the mysql_genius-desktop
|
|
14
14
|
standalone app. Contains the SQL validator, query runner, database analyses, and
|
|
@@ -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
|