sql_query_analyzer 0.2.0 → 0.3.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/lib/sql_query_analyzer/execute.rb +6 -2
- data/lib/sql_query_analyzer/explain_analyzer.rb +12 -11
- data/lib/sql_query_analyzer/query_plan_presenter.rb +39 -24
- data/lib/sql_query_analyzer/sequential_scan_advisor.rb +55 -31
- data/lib/sql_query_analyzer/sql_level_rules.rb +6 -2
- data/lib/sql_query_analyzer/suggestion_engine.rb +32 -24
- data/lib/sql_query_analyzer/suggestion_rules.rb +11 -8
- data/lib/sql_query_analyzer/version.rb +3 -2
- data/lib/sql_query_analyzer.rb +10 -7
- metadata +81 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc77c3595765e78f3b480fff180bc02c3a075466decb48a3b97e01c32be36f5c
|
4
|
+
data.tar.gz: 157c99237424e4cdc85f01c1dc93b3aefab554f3052c09e860fceaa58417b2fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8224852f6757433d6b44813b96addc97cca6e9d5457c751444f6c8d9c6b7ac0c7220767ba9ad1b2b5ff342892f221841a81edc1c0ae44efe56ebe3ff29ff249d
|
7
|
+
data.tar.gz: 46d9cb2b4d207de95c7a1d574676c321b9220f128095d5d824e8f2eccd21a53f72eb9b5926e9709309abc0baf55ee65537ad5fc6b0ab5c1b89d2162f0f50f867
|
@@ -1,7 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SqlQueryAnalyzer
|
4
|
+
# Executes EXPLAIN queries on the database
|
2
5
|
class Execute
|
3
|
-
def self.explain_sql(raw_sql)
|
4
|
-
|
6
|
+
def self.explain_sql(raw_sql, run)
|
7
|
+
query_with_options = run ? "EXPLAIN ANALYZE #{raw_sql}" : "EXPLAIN #{raw_sql}"
|
8
|
+
ActiveRecord::Base.connection.execute(query_with_options)
|
5
9
|
end
|
6
10
|
end
|
7
11
|
end
|
@@ -1,22 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# lib/sql_query_analyzer/explain_analyzer.rb
|
2
4
|
module SqlQueryAnalyzer
|
5
|
+
# Module that adds explain_with_suggestions method to ActiveRecord::Relation
|
3
6
|
module ExplainAnalyzer
|
4
|
-
def explain_with_suggestions
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
return
|
7
|
+
def explain_with_suggestions(run: false)
|
8
|
+
unless is_a?(ActiveRecord::Relation)
|
9
|
+
puts '⚠️ Not an ActiveRecord Relation. Skipping explain_with_suggestions.'
|
10
|
+
return nil
|
9
11
|
end
|
10
12
|
|
11
|
-
raw_sql =
|
13
|
+
raw_sql = to_sql
|
12
14
|
|
13
|
-
explain_output = SqlQueryAnalyzer::Execute.explain_sql(raw_sql)
|
15
|
+
explain_output = SqlQueryAnalyzer::Execute.explain_sql(raw_sql, run)
|
14
16
|
engine = SqlQueryAnalyzer::SuggestionEngine.new(explain_output, raw_sql)
|
15
|
-
|
16
|
-
|
17
|
-
nil
|
18
|
-
rescue => e
|
17
|
+
engine.analyze
|
18
|
+
rescue StandardError => e
|
19
19
|
puts "Error analyzing query: #{e.message}"
|
20
|
+
nil
|
20
21
|
end
|
21
22
|
end
|
22
23
|
end
|
@@ -1,54 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pastel'
|
2
4
|
|
3
5
|
module SqlQueryAnalyzer
|
6
|
+
# Presents query plan output with colored formatting and suggestions
|
4
7
|
class QueryPlanPresenter
|
5
|
-
def initialize(output:, warnings:, total_cost:, rows_estimate:, actual_time:)
|
8
|
+
def initialize(output:, warnings:, total_cost:, rows_estimate:, actual_time:, output_stream: $stdout)
|
6
9
|
@output = output
|
7
10
|
@warnings = warnings
|
8
11
|
@total_cost = total_cost
|
9
12
|
@rows_estimate = rows_estimate
|
10
13
|
@actual_time = actual_time
|
11
14
|
@pastel = Pastel.new
|
15
|
+
@output_stream = output_stream
|
12
16
|
end
|
13
17
|
|
14
18
|
def display
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
|
-
puts @pastel.bold("\n📊 Query Metrics:")
|
21
|
-
puts " 💰 Total Cost: #{@pastel.green(@total_cost)}" if @total_cost
|
22
|
-
puts " 📈 Rows Estimate: #{@pastel.blue(@rows_estimate)}" if @rows_estimate
|
23
|
-
puts " ⏱️ Actual Time: #{@pastel.magenta("#{@actual_time} ms")}" if @actual_time
|
24
|
-
|
25
|
-
if @warnings.any?
|
26
|
-
puts @pastel.bold("\n🚩 Warnings and Suggestions:")
|
27
|
-
@warnings.each do |warn|
|
28
|
-
puts " #{@pastel.yellow("Line #{warn[:line_number]}:")} #{warn[:line_text]}"
|
29
|
-
puts " 👉 #{colorize_by_severity(warn[:suggestion])}"
|
30
|
-
end
|
31
|
-
else
|
32
|
-
puts @pastel.green("\n✅ No immediate problems detected in the query plan.")
|
33
|
-
end
|
19
|
+
display_query_plan
|
20
|
+
display_metrics
|
21
|
+
display_warnings_or_success
|
34
22
|
end
|
35
23
|
|
36
|
-
def self.classify_cost(cost)
|
24
|
+
def self.classify_cost(cost, output_stream: $stdout)
|
37
25
|
return unless cost
|
26
|
+
|
38
27
|
pastel = Pastel.new
|
39
28
|
|
40
29
|
case cost
|
41
30
|
when 0..300
|
42
|
-
puts pastel.green("\n✅ Query cost is LOW. All good!")
|
31
|
+
output_stream.puts pastel.green("\n✅ Query cost is LOW. All good!")
|
43
32
|
when 301..1000
|
44
|
-
puts pastel.yellow("\n🧐 Query cost is MODERATE. May benefit from optimizations.")
|
33
|
+
output_stream.puts pastel.yellow("\n🧐 Query cost is MODERATE. May benefit from optimizations.")
|
45
34
|
else
|
46
|
-
puts pastel.red.bold("\n🛑 Query cost is HIGH. Recommend tuning immediately!")
|
35
|
+
output_stream.puts pastel.red.bold("\n🛑 Query cost is HIGH. Recommend tuning immediately!")
|
47
36
|
end
|
48
37
|
end
|
49
38
|
|
50
39
|
private
|
51
40
|
|
41
|
+
def display_query_plan
|
42
|
+
@output_stream.puts @pastel.bold("\n🔍 QUERY PLAN:")
|
43
|
+
@output.each_with_index do |line, idx|
|
44
|
+
@output_stream.puts " #{@pastel.cyan("#{idx + 1}:")} #{line}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def display_metrics
|
49
|
+
@output_stream.puts @pastel.bold("\n📊 Query Metrics:")
|
50
|
+
@output_stream.puts " 💰 Total Cost: #{@pastel.green(@total_cost)}" if @total_cost
|
51
|
+
@output_stream.puts " 📈 Rows Estimate: #{@pastel.blue(@rows_estimate)}" if @rows_estimate
|
52
|
+
@output_stream.puts " ⏱️ Actual Time: #{@pastel.magenta("#{@actual_time} ms")}" if @actual_time
|
53
|
+
end
|
54
|
+
|
55
|
+
def display_warnings_or_success
|
56
|
+
if @warnings.any?
|
57
|
+
@output_stream.puts @pastel.bold("\n🚩 Warnings and Suggestions:")
|
58
|
+
@warnings.each do |warn|
|
59
|
+
@output_stream.puts " #{@pastel.yellow("Line #{warn[:line_number]}:")} #{warn[:line_text]}"
|
60
|
+
@output_stream.puts " 👉 #{colorize_by_severity(warn[:suggestion])}"
|
61
|
+
end
|
62
|
+
else
|
63
|
+
@output_stream.puts @pastel.green("\n✅ No immediate problems detected in the query plan.")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
52
67
|
def colorize_by_severity(suggestion)
|
53
68
|
case suggestion.severity
|
54
69
|
when :critical
|
@@ -1,54 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SqlQueryAnalyzer
|
4
|
+
# Analyzes sequential scans and provides enhanced messaging and index suggestions
|
2
5
|
class SequentialScanAdvisor
|
3
|
-
|
4
|
-
|
6
|
+
attr_reader :query_plan
|
7
|
+
|
8
|
+
def initialize(query_plan)
|
9
|
+
@query_plan = query_plan
|
5
10
|
end
|
6
11
|
|
7
12
|
def enhanced_message
|
8
13
|
table_name, column_names = extract_table_and_columns
|
9
|
-
return nil unless table_name
|
10
14
|
|
11
|
-
|
12
|
-
return "⚡ Sequential Scan detected on '#{table_name}', but no filter condition found. Likely a full table read (e.g., SELECT *), or small table size makes index use unnecessary."
|
13
|
-
end
|
15
|
+
return nil unless sequential_scan_detected?
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
+
if column_names.empty?
|
18
|
+
return "👉 [CRITICAL] ⚡ Sequential Scan detected on '#{table_name}', " \
|
19
|
+
'but no filter condition found. Likely a full table read ' \
|
20
|
+
'(e.g., SELECT *), or small table size makes index use unnecessary.'
|
17
21
|
end
|
18
22
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
messages = []
|
24
|
+
messages << "👉 [CRITICAL] ⚡ Sequential Scan detected on '#{table_name}', " \
|
25
|
+
"and filter involves columns: #{column_names.join(', ')}."
|
26
|
+
if missing_composite_index?(table_name, column_names)
|
27
|
+
messages << "💡 Consider adding a composite index on: #{column_names.join(', ')}"
|
23
28
|
end
|
29
|
+
messages.join("\n")
|
24
30
|
end
|
25
31
|
|
26
32
|
private
|
27
33
|
|
34
|
+
def sequential_scan_detected?
|
35
|
+
query_plan.include?('Seq Scan on')
|
36
|
+
end
|
37
|
+
|
28
38
|
def extract_table_and_columns
|
29
|
-
|
30
|
-
|
31
|
-
table_name = $1
|
32
|
-
columns = extract_filter_columns(@line_text)
|
33
|
-
[table_name, columns]
|
34
|
-
else
|
35
|
-
[nil, []]
|
36
|
-
end
|
39
|
+
table_name = query_plan.match(/Seq Scan on (\w+)/)&.captures&.first
|
40
|
+
[table_name, extract_columns_from_filter]
|
37
41
|
end
|
38
42
|
|
39
|
-
def
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
43
|
+
def extract_columns_from_filter
|
44
|
+
# Grab the Filter line out of the full plan
|
45
|
+
filter_line = query_plan.lines.find { |l| l.strip.start_with?('Filter:') }
|
46
|
+
return [] unless filter_line
|
47
|
+
|
48
|
+
# Remove the "Filter:" prefix and any Postgres typecasts
|
49
|
+
cleaned = filter_line
|
50
|
+
.sub('Filter:', '')
|
51
|
+
.gsub(/::[a-zA-Z_ ]+/, '')
|
52
|
+
.downcase
|
53
|
+
|
54
|
+
# Pull actual column names for this table from the schema
|
55
|
+
table_name = query_plan.match(/Seq Scan on (\w+)/)&.captures&.first
|
56
|
+
return [] unless table_name
|
57
|
+
|
58
|
+
schema_cols = ActiveRecord::Base
|
59
|
+
.connection
|
60
|
+
.columns(table_name.to_sym)
|
61
|
+
.map(&:name)
|
62
|
+
.map(&:downcase)
|
63
|
+
|
64
|
+
# Of all schema columns, pick those mentioned in the cleaned filter
|
65
|
+
schema_cols.select { |col| cleaned.include?(col) }.uniq
|
44
66
|
end
|
45
67
|
|
46
|
-
def
|
47
|
-
ActiveRecord::Base
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
68
|
+
def missing_composite_index?(table_name, columns)
|
69
|
+
existing = ActiveRecord::Base
|
70
|
+
.connection
|
71
|
+
.indexes(table_name.to_sym)
|
72
|
+
.map(&:columns)
|
73
|
+
.map { |cols| cols.map(&:downcase) }
|
74
|
+
|
75
|
+
existing.none? { |idx_cols| idx_cols == columns || idx_cols.first(columns.length) == columns }
|
52
76
|
end
|
53
77
|
end
|
54
78
|
end
|
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SqlQueryAnalyzer
|
4
|
+
# Evaluates SQL-level rules like SELECT * and JOIN without ON
|
2
5
|
class SqlLevelRules
|
3
6
|
def self.evaluate(sql)
|
4
7
|
return [] unless sql
|
@@ -9,7 +12,7 @@ module SqlQueryAnalyzer
|
|
9
12
|
warnings << {
|
10
13
|
line_number: 'N/A',
|
11
14
|
line_text: 'SELECT *',
|
12
|
-
suggestion: Suggestion.new(:warning,
|
15
|
+
suggestion: Suggestion.new(:warning, '🚨 Query uses SELECT *. Specify only needed columns for performance.')
|
13
16
|
}
|
14
17
|
end
|
15
18
|
|
@@ -17,7 +20,8 @@ module SqlQueryAnalyzer
|
|
17
20
|
warnings << {
|
18
21
|
line_number: 'N/A',
|
19
22
|
line_text: 'JOIN without ON',
|
20
|
-
suggestion: Suggestion.new(:critical,
|
23
|
+
suggestion: Suggestion.new(:critical,
|
24
|
+
'⚡ JOIN without ON detected. May cause massive row combinations (CROSS JOIN).')
|
21
25
|
}
|
22
26
|
end
|
23
27
|
|
@@ -1,14 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'suggestion_rules'
|
2
4
|
require_relative 'query_plan_presenter'
|
3
5
|
require_relative 'sql_level_rules'
|
6
|
+
require_relative 'sequential_scan_advisor'
|
4
7
|
|
5
8
|
module SqlQueryAnalyzer
|
9
|
+
# Represents a single suggestion with severity and message
|
6
10
|
class Suggestion
|
7
11
|
attr_reader :severity, :message
|
8
12
|
|
9
13
|
def initialize(severity, message)
|
10
14
|
@severity = severity
|
11
|
-
@message
|
15
|
+
@message = message
|
12
16
|
end
|
13
17
|
|
14
18
|
def to_s
|
@@ -16,50 +20,53 @@ module SqlQueryAnalyzer
|
|
16
20
|
end
|
17
21
|
end
|
18
22
|
|
23
|
+
# Main engine that analyzes EXPLAIN output and generates suggestions
|
19
24
|
class SuggestionEngine
|
20
25
|
def initialize(explain_output, sql = nil)
|
21
26
|
@explain_output = explain_output
|
22
|
-
@sql
|
27
|
+
@sql = sql
|
23
28
|
end
|
24
29
|
|
25
30
|
def analyze
|
26
|
-
|
27
|
-
|
28
|
-
|
31
|
+
warnings = []
|
32
|
+
total_cost = nil
|
33
|
+
rows_estimate = nil
|
34
|
+
actual_time = nil
|
35
|
+
|
36
|
+
# Build the full plan text (all lines) so advisors can see Filters, etc.
|
37
|
+
full_plan = @explain_output.map { |row| row['QUERY PLAN'] }.join("\n")
|
29
38
|
|
30
39
|
@explain_output.each_with_index do |row, idx|
|
31
|
-
line = row[
|
32
|
-
output << line
|
40
|
+
line = row['QUERY PLAN']
|
33
41
|
|
42
|
+
# Capture cost & estimates
|
34
43
|
if line =~ /cost=\d+\.\d+\.\.(\d+\.\d+) rows=(\d+)/
|
35
|
-
total_cost
|
36
|
-
rows_estimate =
|
44
|
+
total_cost = ::Regexp.last_match(1).to_f
|
45
|
+
rows_estimate = ::Regexp.last_match(2).to_i
|
37
46
|
end
|
38
|
-
|
39
|
-
actual_time ||= $1.to_f if line =~ /actual time=(\d+\.\d+)/
|
47
|
+
actual_time ||= ::Regexp.last_match(1).to_f if line =~ /actual time=(\d+\.\d+)/
|
40
48
|
|
41
49
|
SuggestionRules.all.each do |rule|
|
42
|
-
|
50
|
+
next unless rule[:matcher].call(line)
|
43
51
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
49
|
-
|
50
|
-
warnings << {
|
51
|
-
line_number: idx + 1,
|
52
|
-
line_text: line,
|
53
|
-
suggestion: suggestion
|
54
|
-
}
|
52
|
+
message = rule[:message]
|
53
|
+
if rule[:message].include?('Sequential Scan')
|
54
|
+
dynamic = SequentialScanAdvisor.new(full_plan).enhanced_message
|
55
|
+
message = dynamic unless dynamic.nil?
|
55
56
|
end
|
57
|
+
|
58
|
+
warnings << {
|
59
|
+
line_number: idx + 1,
|
60
|
+
line_text: line,
|
61
|
+
suggestion: Suggestion.new(rule[:severity], message)
|
62
|
+
}
|
56
63
|
end
|
57
64
|
end
|
58
65
|
|
59
66
|
warnings.concat(SqlLevelRules.evaluate(@sql))
|
60
67
|
|
61
68
|
presenter = QueryPlanPresenter.new(
|
62
|
-
output:
|
69
|
+
output: @explain_output.map { |r| r['QUERY PLAN'] },
|
63
70
|
warnings: warnings,
|
64
71
|
total_cost: total_cost,
|
65
72
|
rows_estimate: rows_estimate,
|
@@ -68,6 +75,7 @@ module SqlQueryAnalyzer
|
|
68
75
|
|
69
76
|
presenter.display
|
70
77
|
QueryPlanPresenter.classify_cost(total_cost)
|
78
|
+
warnings
|
71
79
|
end
|
72
80
|
end
|
73
81
|
end
|
@@ -1,41 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SqlQueryAnalyzer
|
4
|
+
# Contains rules for matching query plan patterns and generating suggestions
|
2
5
|
class SuggestionRules
|
3
6
|
def self.all
|
4
7
|
[
|
5
8
|
{
|
6
9
|
matcher: ->(line) { line.include?('Seq Scan') },
|
7
10
|
severity: :critical,
|
8
|
-
message:
|
11
|
+
message: '⚡ Sequential Scan detected. Add appropriate indexes to avoid full table scans.'
|
9
12
|
},
|
10
13
|
{
|
11
14
|
matcher: ->(line) { line.include?('Nested Loop') },
|
12
15
|
severity: :warning,
|
13
|
-
message:
|
16
|
+
message: '🌀 Nested Loop detected. Ensure JOINs are optimized and indexed.'
|
14
17
|
},
|
15
18
|
{
|
16
19
|
matcher: ->(line) { line.include?('Bitmap Heap Scan') },
|
17
20
|
severity: :info,
|
18
|
-
message:
|
21
|
+
message: '📦 Bitmap Heap Scan used. Acceptable but check if an index-only scan is possible.'
|
19
22
|
},
|
20
23
|
{
|
21
24
|
matcher: ->(line) { line.include?('Materialize') },
|
22
25
|
severity: :warning,
|
23
|
-
message:
|
26
|
+
message: '📄 Materialize detected. May cause extra memory usage if result sets are large.'
|
24
27
|
},
|
25
28
|
{
|
26
29
|
matcher: ->(line) { line.include?('Hash Join') },
|
27
30
|
severity: :info,
|
28
|
-
message:
|
31
|
+
message: '🔗 Hash Join used. Generally fast for large datasets, but check hash table memory usage.'
|
29
32
|
},
|
30
33
|
{
|
31
34
|
matcher: ->(line) { line.include?('Merge Join') },
|
32
35
|
severity: :info,
|
33
|
-
message:
|
36
|
+
message: '🔀 Merge Join used. Efficient if input is sorted properly; check indexes.'
|
34
37
|
},
|
35
38
|
{
|
36
|
-
matcher: ->(line) { line
|
39
|
+
matcher: ->(line) { line&.match?(/^\s*WITH\s+\w+\s+AS\s*\(/i) },
|
37
40
|
severity: :info,
|
38
|
-
message:
|
41
|
+
message: '🔧 CTE usage detected. Be aware CTEs can materialize (costly) in Postgres < 12.'
|
39
42
|
}
|
40
43
|
]
|
41
44
|
end
|
data/lib/sql_query_analyzer.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# lib/sql_query_analyzer.rb
|
2
|
-
require
|
3
|
-
require
|
4
|
+
require 'active_support/all'
|
5
|
+
require 'active_record'
|
4
6
|
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
7
|
+
require 'sql_query_analyzer/version'
|
8
|
+
require 'sql_query_analyzer/suggestion_engine'
|
9
|
+
require 'sql_query_analyzer/explain_analyzer'
|
10
|
+
require 'sql_query_analyzer/execute'
|
11
|
+
require 'sql_query_analyzer/sequential_scan_advisor'
|
10
12
|
|
13
|
+
# Main module for SQL Query Analyzer gem
|
11
14
|
module SqlQueryAnalyzer
|
12
15
|
# Future configurations can go here
|
13
16
|
end
|
metadata
CHANGED
@@ -1,15 +1,69 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sql_query_analyzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anoob Bava
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-09-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '8.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '6.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '8.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activesupport
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '6.0'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '8.0'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '6.0'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '8.0'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: logger
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '1.4'
|
60
|
+
type: :runtime
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '1.4'
|
13
67
|
- !ruby/object:Gem::Dependency
|
14
68
|
name: pastel
|
15
69
|
requirement: !ruby/object:Gem::Requirement
|
@@ -28,16 +82,22 @@ dependencies:
|
|
28
82
|
name: rails
|
29
83
|
requirement: !ruby/object:Gem::Requirement
|
30
84
|
requirements:
|
31
|
-
- - "
|
85
|
+
- - ">="
|
32
86
|
- !ruby/object:Gem::Version
|
33
87
|
version: '6.0'
|
88
|
+
- - "<"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '8.0'
|
34
91
|
type: :runtime
|
35
92
|
prerelease: false
|
36
93
|
version_requirements: !ruby/object:Gem::Requirement
|
37
94
|
requirements:
|
38
|
-
- - "
|
95
|
+
- - ">="
|
39
96
|
- !ruby/object:Gem::Version
|
40
97
|
version: '6.0'
|
98
|
+
- - "<"
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '8.0'
|
41
101
|
- !ruby/object:Gem::Dependency
|
42
102
|
name: rspec
|
43
103
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +112,20 @@ dependencies:
|
|
52
112
|
- - "~>"
|
53
113
|
- !ruby/object:Gem::Version
|
54
114
|
version: '3.13'
|
115
|
+
- !ruby/object:Gem::Dependency
|
116
|
+
name: sqlite3
|
117
|
+
requirement: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - "~>"
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '1.4'
|
122
|
+
type: :development
|
123
|
+
prerelease: false
|
124
|
+
version_requirements: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - "~>"
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '1.4'
|
55
129
|
description: A Ruby on Rails gem that analyzes SQL queries and suggests optimizations
|
56
130
|
like missing indexes, inefficient sorts, and risky joins.
|
57
131
|
email:
|
@@ -72,7 +146,8 @@ files:
|
|
72
146
|
homepage: https://github.com/anoobbava/sql_query_analyzer
|
73
147
|
licenses:
|
74
148
|
- MIT
|
75
|
-
metadata:
|
149
|
+
metadata:
|
150
|
+
rubygems_mfa_required: 'true'
|
76
151
|
post_install_message:
|
77
152
|
rdoc_options: []
|
78
153
|
require_paths:
|
@@ -81,7 +156,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
156
|
requirements:
|
82
157
|
- - ">="
|
83
158
|
- !ruby/object:Gem::Version
|
84
|
-
version:
|
159
|
+
version: 3.0.0
|
85
160
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
161
|
requirements:
|
87
162
|
- - ">="
|