sql_query_analyzer 0.2.1 → 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: de53885b50c1f9d62d98d6713ac536f29b30ebdada947a54bde86610cbc594f2
4
- data.tar.gz: 3211b01b3d49e290bb9a7724a3527602b6a80c3cd2d9b7416160967885c6b7dc
3
+ metadata.gz: dc77c3595765e78f3b480fff180bc02c3a075466decb48a3b97e01c32be36f5c
4
+ data.tar.gz: 157c99237424e4cdc85f01c1dc93b3aefab554f3052c09e860fceaa58417b2fe
5
5
  SHA512:
6
- metadata.gz: 8bbd2f6b1c5cc37034909e5f4a7e24aefc77e2d7d5719ee0d2d5908873249a01ba6ec9d32641658d0d316119ca9b30ce1da0cfbf31d43db02b49930321a92288
7
- data.tar.gz: 2c016cca40185f82db401a0a582f5746c15072d6726ebcf7f6ad4e2ff228aecff6e863c00a08ff1da0dce5c7b43d9549e3b047dded9da5255af6f77b0576396c
6
+ metadata.gz: 8224852f6757433d6b44813b96addc97cca6e9d5457c751444f6c8d9c6b7ac0c7220767ba9ad1b2b5ff342892f221841a81edc1c0ae44efe56ebe3ff29ff249d
7
+ data.tar.gz: 46d9cb2b4d207de95c7a1d574676c321b9220f128095d5d824e8f2eccd21a53f72eb9b5926e9709309abc0baf55ee65537ad5fc6b0ab5c1b89d2162f0f50f867
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SqlQueryAnalyzer
4
+ # Executes EXPLAIN queries on the database
2
5
  class Execute
3
6
  def self.explain_sql(raw_sql, run)
4
7
  query_with_options = run ? "EXPLAIN ANALYZE #{raw_sql}" : "EXPLAIN #{raw_sql}"
@@ -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
7
  def explain_with_suggestions(run: false)
5
-
6
- unless self.is_a?(ActiveRecord::Relation)
7
- puts "⚠️ Not an ActiveRecord Relation. Skipping explain_with_suggestions."
8
- return
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
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,4 +1,7 @@
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
6
  attr_reader :query_plan
4
7
 
@@ -13,8 +16,8 @@ module SqlQueryAnalyzer
13
16
 
14
17
  if column_names.empty?
15
18
  return "👉 [CRITICAL] ⚡ Sequential Scan detected on '#{table_name}', " \
16
- "but no filter condition found. Likely a full table read " \
17
- "(e.g., SELECT *), or small table size makes index use unnecessary."
19
+ 'but no filter condition found. Likely a full table read ' \
20
+ '(e.g., SELECT *), or small table size makes index use unnecessary.'
18
21
  end
19
22
 
20
23
  messages = []
@@ -29,47 +32,47 @@ module SqlQueryAnalyzer
29
32
  private
30
33
 
31
34
  def sequential_scan_detected?
32
- query_plan.include?("Seq Scan on")
35
+ query_plan.include?('Seq Scan on')
33
36
  end
34
37
 
35
38
  def extract_table_and_columns
36
39
  table_name = query_plan.match(/Seq Scan on (\w+)/)&.captures&.first
37
- [ table_name, extract_columns_from_filter ]
40
+ [table_name, extract_columns_from_filter]
38
41
  end
39
42
 
40
43
  def extract_columns_from_filter
41
44
  # Grab the Filter line out of the full plan
42
- filter_line = query_plan.lines.find { |l| l.strip.start_with?("Filter:") }
45
+ filter_line = query_plan.lines.find { |l| l.strip.start_with?('Filter:') }
43
46
  return [] unless filter_line
44
47
 
45
48
  # Remove the "Filter:" prefix and any Postgres typecasts
46
49
  cleaned = filter_line
47
- .sub("Filter:", "")
48
- .gsub(/::[a-zA-Z_ ]+/, "")
49
- .downcase
50
+ .sub('Filter:', '')
51
+ .gsub(/::[a-zA-Z_ ]+/, '')
52
+ .downcase
50
53
 
51
54
  # Pull actual column names for this table from the schema
52
55
  table_name = query_plan.match(/Seq Scan on (\w+)/)&.captures&.first
53
56
  return [] unless table_name
57
+
54
58
  schema_cols = ActiveRecord::Base
55
- .connection
56
- .columns(table_name.to_sym)
57
- .map(&:name)
58
- .map(&:downcase)
59
+ .connection
60
+ .columns(table_name.to_sym)
61
+ .map(&:name)
62
+ .map(&:downcase)
59
63
 
60
64
  # Of all schema columns, pick those mentioned in the cleaned filter
61
- extracted = schema_cols.select { |col| cleaned.include?(col) }.uniq
62
- extracted
65
+ schema_cols.select { |col| cleaned.include?(col) }.uniq
63
66
  end
64
67
 
65
68
  def missing_composite_index?(table_name, columns)
66
69
  existing = ActiveRecord::Base
67
- .connection
68
- .indexes(table_name.to_sym)
69
- .map(&:columns)
70
- .map { |cols| cols.map(&:downcase) }
70
+ .connection
71
+ .indexes(table_name.to_sym)
72
+ .map(&:columns)
73
+ .map { |cols| cols.map(&:downcase) }
71
74
 
72
- !existing.any? { |idx_cols| idx_cols == columns || idx_cols.first(columns.length) == columns }
75
+ existing.none? { |idx_cols| idx_cols == columns || idx_cols.first(columns.length) == columns }
73
76
  end
74
77
  end
75
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,9 +1,12 @@
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'
4
6
  require_relative 'sequential_scan_advisor'
5
7
 
6
8
  module SqlQueryAnalyzer
9
+ # Represents a single suggestion with severity and message
7
10
  class Suggestion
8
11
  attr_reader :severity, :message
9
12
 
@@ -17,6 +20,7 @@ module SqlQueryAnalyzer
17
20
  end
18
21
  end
19
22
 
23
+ # Main engine that analyzes EXPLAIN output and generates suggestions
20
24
  class SuggestionEngine
21
25
  def initialize(explain_output, sql = nil)
22
26
  @explain_output = explain_output
@@ -30,31 +34,31 @@ module SqlQueryAnalyzer
30
34
  actual_time = nil
31
35
 
32
36
  # Build the full plan text (all lines) so advisors can see Filters, etc.
33
- full_plan = @explain_output.map { |row| row["QUERY PLAN"] }.join("\n")
37
+ full_plan = @explain_output.map { |row| row['QUERY PLAN'] }.join("\n")
34
38
 
35
39
  @explain_output.each_with_index do |row, idx|
36
- line = row["QUERY PLAN"]
40
+ line = row['QUERY PLAN']
37
41
 
38
42
  # Capture cost & estimates
39
43
  if line =~ /cost=\d+\.\d+\.\.(\d+\.\d+) rows=(\d+)/
40
- total_cost = $1.to_f
41
- rows_estimate = $2.to_i
44
+ total_cost = ::Regexp.last_match(1).to_f
45
+ rows_estimate = ::Regexp.last_match(2).to_i
42
46
  end
43
- 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+)/
44
48
 
45
49
  SuggestionRules.all.each do |rule|
46
50
  next unless rule[:matcher].call(line)
47
51
 
48
- suggestion = Suggestion.new(rule[:severity], rule[:message])
49
- if rule[:message].include?("Sequential Scan")
52
+ message = rule[:message]
53
+ if rule[:message].include?('Sequential Scan')
50
54
  dynamic = SequentialScanAdvisor.new(full_plan).enhanced_message
51
- suggestion = Suggestion.new(rule[:severity], dynamic) if dynamic
55
+ message = dynamic unless dynamic.nil?
52
56
  end
53
57
 
54
58
  warnings << {
55
59
  line_number: idx + 1,
56
- line_text: line,
57
- suggestion: suggestion
60
+ line_text: line,
61
+ suggestion: Suggestion.new(rule[:severity], message)
58
62
  }
59
63
  end
60
64
  end
@@ -62,15 +66,16 @@ module SqlQueryAnalyzer
62
66
  warnings.concat(SqlLevelRules.evaluate(@sql))
63
67
 
64
68
  presenter = QueryPlanPresenter.new(
65
- output: @explain_output.map { |r| r["QUERY PLAN"] },
66
- warnings: warnings,
67
- total_cost: total_cost,
69
+ output: @explain_output.map { |r| r['QUERY PLAN'] },
70
+ warnings: warnings,
71
+ total_cost: total_cost,
68
72
  rows_estimate: rows_estimate,
69
- actual_time: actual_time
73
+ actual_time: actual_time
70
74
  )
71
75
 
72
76
  presenter.display
73
77
  QueryPlanPresenter.classify_cost(total_cost)
78
+ warnings
74
79
  end
75
80
  end
76
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
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.1"
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.1
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
  - - ">="