mysql_genius-core 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba1c5800131408bf90c87a052719196ebca6c84c9a9eae175e205a027d24e7a4
4
- data.tar.gz: e5ae1079f22dea06691754b08b685550fb23b68c1b3dbec3f455ad9e38dc8436
3
+ metadata.gz: 197d8fdf10094c1acf1487e9cc958e8a948f2faf16d0fc06d89193c1ccbc69c7
4
+ data.tar.gz: b2503519e572e9520fcda9c2e0adfd166cb68623d3a4fa1115034ff2e51ead0b
5
5
  SHA512:
6
- metadata.gz: 6142e5bd5cf98d4c1bda216240f48889e840af67d317fe9cb5b3c12afb48b252d37409fe652ada956c11867fe0d3ba98681ac41b1eda273be339f54c4b5404af
7
- data.tar.gz: addd52a4e81dc87af3f5eec1f016208a1cd22b8a018fd471722d460af6d911a05241d771e2ec4d6765acde3cb52b47ad4b8f7346787231722377862d0a50c0e1
6
+ metadata.gz: 22a75ed744368906927aada6311e76b4b8f4a2ae8dd8d6acbc8b7ac0486d1f7634b82f6cc0a1ef10324fa6984d191142d5e295210f0641896d654882536f9218
7
+ data.tar.gz: f3bbd7408ed91148f288cd95130158e4400c5c6d31e1b14527bd90fde64d2e211e6ff6e25c170847982ed19e73b9d9470c0a04d182a051e403ef71bf3ae2d235
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Added
6
+ - `MysqlGenius::Core::Analysis::Columns` — service class for `GET /columns` logic with a tagged-result struct (`:ok`, `:blocked`, `:not_found`). Takes a `Core::Connection`, uses `Core::SqlValidator.masked_column?` for the masked-column rule.
7
+ - `MysqlGenius::Core::Ai::{DescribeQuery, SchemaReview, RewriteQuery, IndexAdvisor, MigrationRisk}` — 5 AI prompt builder classes extracted from the `mysql_genius` Rails adapter's `AiFeatures` concern. Each takes `(client, config)` or `(client, config, connection)` and exposes a single `#call` method.
8
+ - `MysqlGenius::Core::Ai::SchemaContextBuilder` — shared helper for building "Table: X (~N rows), Columns: …, Indexes: …" schema descriptions. Supports `detail: :basic` and `detail: :with_cardinality`.
9
+ - `MysqlGenius::Core::Ai::Config#domain_context` — new optional keyword field (empty string default) interpolated into every extracted prompt builder's system prompt.
10
+ - `MysqlGenius::Core.views_path` — public module method returning the absolute path to the shared ERB template directory. Adapters register this path with their own view loader.
11
+ - **ERB templates extracted from the Rails adapter.** `MysqlGenius::Core.views_path` now points at `lib/mysql_genius/core/views/` which contains `mysql_genius/queries/dashboard.html.erb` and the 10 tab/partial files. Any adapter (Rails, Sinatra, or future desktop) can load these templates by registering this path with its own view loader. Templates depend on a minimal 2-method contract: `path_for(name)` and `render_partial(name)`.
12
+
3
13
  ## 0.4.1
4
14
 
5
15
  No functional changes in `mysql_genius-core`. Version bumped to maintain lockstep with `mysql_genius 0.4.1`, which hotfixes a regression in the Rails adapter's `GET /columns` endpoint. See the root `CHANGELOG.md` for details.
@@ -16,6 +16,8 @@ module MysqlGenius
16
16
  # auth_style - :bearer or :api_key
17
17
  # system_context - optional domain context string that services
18
18
  # append to their system prompts
19
+ # domain_context - optional host-app context string interpolated into
20
+ # AI system prompts (e.g. "Rails app, no FKs")
19
21
  Config = Struct.new(
20
22
  :client,
21
23
  :endpoint,
@@ -23,10 +25,11 @@ module MysqlGenius
23
25
  :model,
24
26
  :auth_style,
25
27
  :system_context,
28
+ :domain_context,
26
29
  keyword_init: true,
27
30
  ) do
28
- def initialize(*)
29
- super
31
+ def initialize(**kwargs)
32
+ super(domain_context: "", **kwargs)
30
33
  freeze
31
34
  end
32
35
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Builds and sends a "describe this query" prompt to Core::Ai::Client.
7
+ # Pure function of SQL + config.domain_context — no connection lookup.
8
+ #
9
+ # Extracted from app/controllers/concerns/mysql_genius/ai_features.rb
10
+ # in Phase 2a.
11
+ class DescribeQuery
12
+ def initialize(client, config)
13
+ @client = client
14
+ @config = config
15
+ end
16
+
17
+ def call(sql)
18
+ messages = [
19
+ { role: "system", content: system_prompt },
20
+ { role: "user", content: sql },
21
+ ]
22
+ @client.chat(messages: messages)
23
+ end
24
+
25
+ private
26
+
27
+ def system_prompt
28
+ <<~PROMPT
29
+ You are a MySQL query explainer. Given a SQL query, explain in plain English:
30
+ 1. What the query does (tables involved, joins, filters, aggregations)
31
+ 2. How data flows through the query
32
+ 3. Any subtle behaviors (implicit type casts, NULL handling in NOT IN, DISTINCT effects, etc.)
33
+ 4. Potential performance concerns visible from the SQL structure alone
34
+ #{@config.domain_context}
35
+ Respond with JSON: {"explanation": "your plain-English explanation using markdown formatting"}
36
+ PROMPT
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ class IndexAdvisor
7
+ def initialize(client, config, connection)
8
+ @client = client
9
+ @config = config
10
+ @connection = connection
11
+ end
12
+
13
+ def call(sql, explain_rows)
14
+ tables = SqlValidator.extract_table_references(sql, @connection)
15
+ schema = SchemaContextBuilder.new(@connection).call(tables, detail: :with_cardinality)
16
+ explain_text = explain_rows.map { |row| row.join(" | ") }.join("\n")
17
+
18
+ messages = [
19
+ { role: "system", content: system_prompt },
20
+ { role: "user", content: "Query:\n#{sql}\n\nEXPLAIN:\n#{explain_text}\n\nSchema:\n#{schema}" },
21
+ ]
22
+ @client.chat(messages: messages)
23
+ end
24
+
25
+ private
26
+
27
+ def system_prompt
28
+ <<~PROMPT
29
+ You are a MySQL index advisor. Given a query, its EXPLAIN output, and current index/cardinality information, suggest optimal indexes. Consider:
30
+ - Composite index column ordering (most selective first, or matching query order)
31
+ - Covering indexes to avoid table lookups
32
+ - Partial indexes for long string columns
33
+ - Write-side costs (if this is a high-write table, note the INSERT/UPDATE overhead)
34
+ - Whether existing indexes could be extended rather than creating new ones
35
+ #{@config.domain_context}
36
+
37
+ Respond with JSON: {"indexes": "markdown-formatted recommendations with exact CREATE INDEX statements, rationale for column ordering, and estimated impact. Include any indexes that should be DROPPED as part of the change."}
38
+ PROMPT
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ class MigrationRisk
7
+ def initialize(client, config, connection)
8
+ @client = client
9
+ @config = config
10
+ @connection = connection
11
+ end
12
+
13
+ def call(migration_sql)
14
+ tables = extract_table_names(migration_sql)
15
+ schema = SchemaContextBuilder.new(@connection).call(tables, detail: :basic)
16
+ schema_text = schema.to_s.empty? ? "Could not determine" : schema
17
+
18
+ messages = [
19
+ { role: "system", content: system_prompt },
20
+ { role: "user", content: "Migration:\n#{migration_sql}\n\nAffected Tables:\n#{schema_text}" },
21
+ ]
22
+ @client.chat(messages: messages)
23
+ end
24
+
25
+ private
26
+
27
+ def extract_table_names(migration_sql)
28
+ # Match Rails migration helpers and raw SQL ALTER TABLE statements.
29
+ rails_matches = migration_sql.scan(/(?:create_table|add_column|remove_column|add_index|remove_index|rename_column|change_column|alter\s+table)\s+[:"]?(\w+)/i).flatten
30
+ sql_matches = migration_sql.scan(/ALTER\s+TABLE\s+`?(\w+)`?/i).flatten
31
+ (rails_matches + sql_matches).uniq
32
+ end
33
+
34
+ def system_prompt
35
+ <<~PROMPT
36
+ You are a MySQL migration risk assessor. Given a Rails migration or DDL, evaluate:
37
+ 1. Will this lock the table? For how long given the row count?
38
+ 2. Is this safe to run during traffic, or does it need a maintenance window?
39
+ 3. Should pt-online-schema-change or gh-ost be used instead?
40
+ 4. Will it break or degrade any of the active queries against this table?
41
+ 5. Are there any data loss risks?
42
+ 6. What is the recommended deployment strategy?
43
+ #{@config.domain_context}
44
+
45
+ Respond with JSON: {"risk_level": "low|medium|high|critical", "assessment": "markdown-formatted risk assessment with specific recommendations and estimated lock duration"}
46
+ PROMPT
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Suggests a rewritten version of a SQL query based on the schema
7
+ # context of the tables it references.
8
+ class RewriteQuery
9
+ def initialize(client, config, connection)
10
+ @client = client
11
+ @config = config
12
+ @connection = connection
13
+ end
14
+
15
+ def call(sql)
16
+ tables = SqlValidator.extract_table_references(sql, @connection)
17
+ schema = SchemaContextBuilder.new(@connection).call(tables, detail: :basic)
18
+
19
+ messages = [
20
+ { role: "system", content: system_prompt(schema) },
21
+ { role: "user", content: sql },
22
+ ]
23
+ @client.chat(messages: messages)
24
+ end
25
+
26
+ private
27
+
28
+ def system_prompt(schema)
29
+ <<~PROMPT
30
+ You are a MySQL query rewrite expert. Analyze the SQL for anti-patterns and suggest a rewritten version. Look for:
31
+ - SELECT * when specific columns would suffice
32
+ - Correlated subqueries that could be JOINs
33
+ - OR conditions preventing index use (suggest UNION ALL)
34
+ - LIKE '%prefix' patterns (leading wildcard)
35
+ - Implicit type conversions in WHERE clauses
36
+ - NOT IN with NULLable columns (suggest NOT EXISTS)
37
+ - ORDER BY on non-indexed columns with LIMIT
38
+ - Unnecessary DISTINCT
39
+ - Functions on indexed columns in WHERE (e.g., DATE(created_at) instead of range)
40
+
41
+ Available schema:
42
+ #{schema}
43
+ #{@config.domain_context}
44
+
45
+ Respond with JSON: {"original": "the original SQL", "rewritten": "the improved SQL", "changes": "markdown list of each change and why it helps"}
46
+ PROMPT
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Builds formatted schema-description strings for AI prompt context.
7
+ # Used by SchemaReview, RewriteQuery, IndexAdvisor, and MigrationRisk.
8
+ #
9
+ # Consolidates the ~10 lines of schema description logic that were
10
+ # duplicated across 4 AI features in the Rails adapter's
11
+ # app/controllers/concerns/mysql_genius/ai_features.rb.
12
+ class SchemaContextBuilder
13
+ def initialize(connection)
14
+ @connection = connection
15
+ end
16
+
17
+ # Returns a formatted multi-line string describing the given tables.
18
+ #
19
+ # detail:
20
+ # :basic — name, row count, primary key, columns, indexes
21
+ # :with_cardinality — adds information_schema.STATISTICS cardinality per index
22
+ def call(tables, detail: :basic)
23
+ Array(tables).filter_map { |t| describe_table(t, detail: detail) }.join("\n\n")
24
+ end
25
+
26
+ private
27
+
28
+ def describe_table(table, detail:)
29
+ return unless @connection.tables.include?(table)
30
+
31
+ cols = @connection.columns_for(table).map do |c|
32
+ parts = ["#{c.name} #{c.sql_type}"]
33
+ parts << "NOT NULL" unless c.null
34
+ parts << "DEFAULT #{c.default}" if c.default
35
+ parts.join(" ")
36
+ end
37
+
38
+ pk = @connection.primary_key(table)
39
+ indexes = @connection.indexes_for(table).map do |idx|
40
+ "#{"UNIQUE " if idx.unique}INDEX #{idx.name} (#{idx.columns.join(", ")})"
41
+ end
42
+
43
+ row_count = fetch_row_count(table)
44
+
45
+ parts = [
46
+ "Table: #{table} (~#{row_count || "unknown"} rows)",
47
+ "Primary Key: #{pk || "NONE"}",
48
+ "Columns: #{cols.join(", ")}",
49
+ "Indexes: #{indexes.any? ? indexes.join(", ") : "NONE"}",
50
+ ]
51
+
52
+ parts << index_cardinality(table) if detail == :with_cardinality
53
+
54
+ parts.join("\n")
55
+ end
56
+
57
+ def fetch_row_count(table)
58
+ sql = "SELECT TABLE_ROWS FROM information_schema.tables " \
59
+ "WHERE table_schema = #{@connection.quote(@connection.current_database)} " \
60
+ "AND table_name = #{@connection.quote(table)}"
61
+ result = @connection.exec_query(sql)
62
+ result.rows.first&.first
63
+ rescue StandardError
64
+ nil
65
+ end
66
+
67
+ def index_cardinality(table)
68
+ sql = "SELECT INDEX_NAME, COLUMN_NAME, CARDINALITY, SEQ_IN_INDEX " \
69
+ "FROM information_schema.STATISTICS " \
70
+ "WHERE TABLE_SCHEMA = #{@connection.quote(@connection.current_database)} " \
71
+ "AND TABLE_NAME = #{@connection.quote(table)} " \
72
+ "ORDER BY INDEX_NAME, SEQ_IN_INDEX"
73
+ result = @connection.exec_query(sql)
74
+ stats = result.rows.map { |r| "#{r[0]}.#{r[1]}: cardinality=#{r[2]}" }
75
+ "Cardinality: #{stats.join(", ")}"
76
+ rescue StandardError
77
+ "Cardinality: (unavailable)"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Reviews a schema for anti-patterns. Takes a specific table name
7
+ # (or nil to review the top 20 queryable tables).
8
+ class SchemaReview
9
+ def initialize(client, config, connection)
10
+ @client = client
11
+ @config = config
12
+ @connection = connection
13
+ end
14
+
15
+ def call(table)
16
+ tables_to_review = table.nil? || table.to_s.empty? ? @connection.tables.first(20) : [table]
17
+ schema_desc = SchemaContextBuilder.new(@connection).call(tables_to_review, detail: :basic)
18
+
19
+ messages = [
20
+ { role: "system", content: system_prompt },
21
+ { role: "user", content: schema_desc },
22
+ ]
23
+ @client.chat(messages: messages)
24
+ end
25
+
26
+ private
27
+
28
+ def system_prompt
29
+ <<~PROMPT
30
+ You are a MySQL schema reviewer. Analyze the following schema and identify anti-patterns and improvement opportunities. Look for:
31
+ - Inappropriate column types (VARCHAR(255) for short values, TEXT where VARCHAR suffices, INT for booleans)
32
+ - Missing indexes on foreign key columns or frequently filtered columns
33
+ - Missing NOT NULL constraints where NULLs are unlikely
34
+ - ENUM columns that should be lookup tables
35
+ - Missing created_at/updated_at timestamps
36
+ - Tables without a PRIMARY KEY
37
+ - Overly wide indexes or redundant indexes
38
+ - Column naming inconsistencies
39
+ #{@config.domain_context}
40
+ Respond with JSON: {"findings": "markdown-formatted findings organized by severity (Critical, Warning, Suggestion). Include specific ALTER TABLE statements where applicable."}
41
+ PROMPT
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Analysis
6
+ # Service class for the Rails engine's GET /columns action and its
7
+ # Phase 2b sidecar equivalent. Takes a Core::Connection and the
8
+ # relevant configuration; returns a tagged Result struct that
9
+ # adapters map to HTTP responses.
10
+ #
11
+ # Each status maps 1:1 to an HTTP status code:
12
+ # :ok → 200 with columns: array
13
+ # :blocked → 403 with error_message:
14
+ # :not_found → 404 with error_message:
15
+ #
16
+ # The adapter reads result.status and dispatches accordingly.
17
+ class Columns
18
+ Result = Struct.new(:status, :columns, :error_message, keyword_init: true)
19
+
20
+ def initialize(connection, blocked_tables:, masked_column_patterns:, default_columns:)
21
+ @connection = connection
22
+ @blocked_tables = blocked_tables
23
+ @masked_column_patterns = masked_column_patterns
24
+ @default_columns = default_columns
25
+ end
26
+
27
+ def call(table:)
28
+ return blocked_result(table) if @blocked_tables.include?(table)
29
+ return not_found_result(table) unless @connection.tables.include?(table)
30
+
31
+ defaults = @default_columns[table] || []
32
+ visible = @connection.columns_for(table).reject do |col|
33
+ SqlValidator.masked_column?(col.name, @masked_column_patterns)
34
+ end
35
+ formatted = visible.map do |col|
36
+ {
37
+ name: col.name,
38
+ type: col.type.to_s,
39
+ default: defaults.empty? || defaults.include?(col.name),
40
+ }
41
+ end
42
+
43
+ Result.new(status: :ok, columns: formatted)
44
+ end
45
+
46
+ private
47
+
48
+ def blocked_result(table)
49
+ Result.new(
50
+ status: :blocked,
51
+ error_message: "Table '#{table}' is not available for querying.",
52
+ )
53
+ end
54
+
55
+ def not_found_result(table)
56
+ Result.new(
57
+ status: :not_found,
58
+ error_message: "Table '#{table}' does not exist.",
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module MysqlGenius
4
4
  module Core
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -10,6 +10,17 @@ module MysqlGenius
10
10
  # overall design.
11
11
  module Core
12
12
  class Error < StandardError; end
13
+
14
+ class << self
15
+ # Absolute path to the shared ERB template directory. Adapters
16
+ # register this path with their view loader:
17
+ #
18
+ # Rails: engine.config.paths["app/views"] << MysqlGenius::Core.views_path
19
+ # Sinatra: set :views, MysqlGenius::Core.views_path
20
+ def views_path
21
+ File.expand_path("core/views", __dir__)
22
+ end
23
+ end
13
24
  end
14
25
  end
15
26
 
@@ -24,11 +35,18 @@ require "mysql_genius/core/ai/config"
24
35
  require "mysql_genius/core/ai/client"
25
36
  require "mysql_genius/core/ai/suggestion"
26
37
  require "mysql_genius/core/ai/optimization"
38
+ require "mysql_genius/core/ai/schema_context_builder"
39
+ require "mysql_genius/core/ai/describe_query"
40
+ require "mysql_genius/core/ai/schema_review"
41
+ require "mysql_genius/core/ai/rewrite_query"
42
+ require "mysql_genius/core/ai/index_advisor"
43
+ require "mysql_genius/core/ai/migration_risk"
27
44
  require "mysql_genius/core/analysis/table_sizes"
28
45
  require "mysql_genius/core/analysis/duplicate_indexes"
29
46
  require "mysql_genius/core/analysis/query_stats"
30
47
  require "mysql_genius/core/analysis/unused_indexes"
31
48
  require "mysql_genius/core/analysis/server_overview"
49
+ require "mysql_genius/core/analysis/columns"
32
50
  require "mysql_genius/core/execution_result"
33
51
  require "mysql_genius/core/query_runner/config"
34
52
  require "mysql_genius/core/query_runner"
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.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antarr Byrd
@@ -24,8 +24,15 @@ 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/describe_query.rb
28
+ - lib/mysql_genius/core/ai/index_advisor.rb
29
+ - lib/mysql_genius/core/ai/migration_risk.rb
27
30
  - lib/mysql_genius/core/ai/optimization.rb
31
+ - lib/mysql_genius/core/ai/rewrite_query.rb
32
+ - lib/mysql_genius/core/ai/schema_context_builder.rb
33
+ - lib/mysql_genius/core/ai/schema_review.rb
28
34
  - lib/mysql_genius/core/ai/suggestion.rb
35
+ - lib/mysql_genius/core/analysis/columns.rb
29
36
  - lib/mysql_genius/core/analysis/duplicate_indexes.rb
30
37
  - lib/mysql_genius/core/analysis/query_stats.rb
31
38
  - lib/mysql_genius/core/analysis/server_overview.rb