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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 584bbafae1893ed4f45410cc46eafe6e120e98990205b6843f2e82cc9dd3b9f0
4
- data.tar.gz: fe27bac5ebe7e38cc961c6abdffa5fc2bf0eef37f97864cd8ed23a7cb1599d70
3
+ metadata.gz: dc77c3595765e78f3b480fff180bc02c3a075466decb48a3b97e01c32be36f5c
4
+ data.tar.gz: 157c99237424e4cdc85f01c1dc93b3aefab554f3052c09e860fceaa58417b2fe
5
5
  SHA512:
6
- metadata.gz: 893727f91285119c0734f23f76e594f5f5085041cdbe9e3f750f785791de417a23f2b44c8f1f4a1b5392dc605e23b26d35acfe4c675f3ea93a089be6157e4711
7
- data.tar.gz: e703a05ad2758df8b778bd872b2f6eecffe05855c7f48a2a603ddbe34ed17d5914b8eef67aed3491d44c0e5843099d47d3e6783fbb5b590b15ac7ad83bb149f3
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
- ActiveRecord::Base.connection.execute("EXPLAIN ANALYZE #{raw_sql}")
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
- unless self.is_a?(ActiveRecord::Relation)
7
- puts "⚠️ Not an ActiveRecord Relation. Skipping explain_with_suggestions."
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 = self.to_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
- suggestions = engine.analyze
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
- puts @pastel.bold("\n🔍 QUERY PLAN:")
16
- @output.each_with_index do |line, idx|
17
- puts " #{@pastel.cyan("#{idx + 1}:")} #{line}"
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
- def initialize(line_text)
4
- @line_text = line_text
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
- if column_names.empty?
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
- missing_indexes = column_names.select do |column|
16
- !index_exists?(table_name, column)
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
- if missing_indexes.any?
20
- "⚡ Sequential Scan detected on '#{table_name}'. Consider adding indexes on: #{missing_indexes.join(', ')}."
21
- else
22
- "⚡ Sequential Scan detected on '#{table_name}', but indexes seem to exist. Might be due to low table size or outdated stats."
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
- # Simplified pattern: "Seq Scan on users (cost=...)"
30
- if @line_text =~ /Seq Scan on (\w+)/
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 extract_filter_columns(text)
40
- # Example EXPLAIN line might include: "Filter: (email = 'x')"
41
- text.scan(/Filter: \((.*?)\)/).flatten
42
- .flat_map { |f| f.scan(/\b(\w+)\s*=/) }
43
- .flatten
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 index_exists?(table_name, column)
47
- ActiveRecord::Base.connection.indexes(table_name).any? do |idx|
48
- idx.columns.include?(column)
49
- end
50
- rescue => e
51
- false # Assume false in case of table missing or dev env differences
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, "🚨 Query uses SELECT *. Specify only needed columns for performance.")
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, "⚡ JOIN without ON detected. May cause massive row combinations (CROSS JOIN).")
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 = 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 = sql
27
+ @sql = sql
23
28
  end
24
29
 
25
30
  def analyze
26
- output = []
27
- warnings = []
28
- total_cost, rows_estimate, actual_time = nil
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["QUERY PLAN"]
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 = $1.to_f
36
- rows_estimate = $2.to_i
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
- if rule[:matcher].call(line)
50
+ next unless rule[:matcher].call(line)
43
51
 
44
- suggestion = Suggestion.new(rule[:severity], rule[:message])
45
- if rule[:message].include?("Sequential Scan")
46
- dynamic_msg = SequentialScanAdvisor.new(line).enhanced_message
47
- suggestion = Suggestion.new(rule[:severity], dynamic_msg) if dynamic_msg
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: 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: "⚡ Sequential Scan detected. Add appropriate indexes to avoid full table scans."
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: "🌀 Nested Loop detected. Ensure JOINs are optimized and indexed."
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: "📦 Bitmap Heap Scan used. Acceptable but check if an index-only scan is possible."
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: "📄 Materialize detected. May cause extra memory usage if result sets are large."
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: "🔗 Hash Join used. Generally fast for large datasets, but check hash table memory usage."
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: "🔀 Merge Join used. Efficient if input is sorted properly; check indexes."
36
+ message: '🔀 Merge Join used. Efficient if input is sorted properly; check indexes.'
34
37
  },
35
38
  {
36
- matcher: ->(line) { line.downcase.include?('cte') || line.downcase.include?('with') },
39
+ matcher: ->(line) { line&.match?(/^\s*WITH\s+\w+\s+AS\s*\(/i) },
37
40
  severity: :info,
38
- message: "🔧 CTE usage detected. Be aware CTEs can materialize (costly) in Postgres < 12."
41
+ message: '🔧 CTE usage detected. Be aware CTEs can materialize (costly) in Postgres < 12.'
39
42
  }
40
43
  ]
41
44
  end
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/sql_query_analyzer/version.rb
2
4
  module SqlQueryAnalyzer
3
-
4
- VERSION = "0.2.0"
5
+ VERSION = '0.3.0'
5
6
  end
@@ -1,13 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/sql_query_analyzer.rb
2
- require "active_support"
3
- require "active_record"
4
+ require 'active_support/all'
5
+ require 'active_record'
4
6
 
5
- require "sql_query_analyzer/version"
6
- require "sql_query_analyzer/suggestion_engine"
7
- require "sql_query_analyzer/explain_analyzer"
8
- require "sql_query_analyzer/execute"
9
- require "sql_query_analyzer/sequential_scan_advisor"
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.2.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-04-29 00:00:00.000000000 Z
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: '0'
159
+ version: 3.0.0
85
160
  required_rubygems_version: !ruby/object:Gem::Requirement
86
161
  requirements:
87
162
  - - ">="