shiba 0.6.4 → 0.8.1

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
- SHA256:
3
- metadata.gz: beb49cd39a8eee9adba9a2d1e430c8f8d17c1534fc8a2f609a0e887191bf72cd
4
- data.tar.gz: e40ba883aaed19427fcb056c88bd63fe3d8ca1bfd9851575ecba44e54769f546
2
+ SHA1:
3
+ metadata.gz: f6dbcdad59562d6337cf6357012a6d7e9e08b1c1
4
+ data.tar.gz: 6583e1f52e1d3ba5f28ed41202b03e9bf6f92124
5
5
  SHA512:
6
- metadata.gz: 1458a067bbebe0b2910a3fa34cf02fa7666042093ae575ceecc1f771845e17e2b41fff6d9cc760a17103d65ac03718017c827035a9acc2365a10ce671eb410ea
7
- data.tar.gz: c173dfa72572b36275f0622641410a06d591a78f5168ddf38ea965d6f859ae250243adbad47ff21bff8c1aaa66ded23043eedb7838208dfc1f372bc30c238e3a
6
+ metadata.gz: 4612ff6c2cdb56c0ab24ed6bd0c938bbb035de7b881b68cf12ef71fd0507152c26cb60600cd5c7e1a039419a5e83a0af65fdea8f57a0695ecaa88287d079091e
7
+ data.tar.gz: 3aa4f8e1d74ae4c50e62fd604678ef1b25db1d4007519c9e4708a442039a913bd775d37a49f43db236fc7de44ae906f9b036d0fdd7967704e7113d4cc202e414
data/.gitignore CHANGED
@@ -9,3 +9,5 @@
9
9
  .*.sw*
10
10
  node_modules
11
11
  test/database.yml
12
+ web/dist/
13
+ logs
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shiba (0.6.4)
4
+ shiba (0.8.1)
5
5
  activesupport
6
6
  mysql2
7
7
  pg
data/README.md CHANGED
@@ -12,7 +12,7 @@ Install using bundler. Note: this gem is not designed to be run on production.
12
12
 
13
13
  ```ruby
14
14
  # Gemfile
15
- gem 'shiba', :group => :test
15
+ gem 'shiba', :group => :test, :require => 'shiba/setup'
16
16
  ```
17
17
 
18
18
  If your application lazy loads gems, you will to manually require it.
@@ -110,7 +110,7 @@ after_script:
110
110
  - bundle exec shiba review --submit
111
111
  ```
112
112
 
113
- Add the Github API token you've generated as an environment variable named `GITHUB_TOKEN` at https://travis-ci.com/{organization}/{repo}/settings.
113
+ Add the Github API token you've generated as an environment variable named `SHIBA_GITHUB_TOKEN` at https://travis-ci.com/{organization}/{repo}/settings.
114
114
 
115
115
  #### CircleCI Integration
116
116
 
@@ -123,7 +123,7 @@ To integrate with CircleCI, add this after the the test run step in `.circleci/c
123
123
  command: bundle exec shiba review --submit
124
124
  ```
125
125
 
126
- An environment variable named `GITHUB_TOKEN` will need to be configured on CircleCI under *Project settings > Environment Variables*
126
+ An environment variable named `SHIBA_GITHUB_TOKEN` will need to be configured on CircleCI under *Project settings > Environment Variables*
127
127
 
128
128
  #### Custom CI Integration
129
129
 
data/Rakefile CHANGED
@@ -2,10 +2,12 @@ require "bundler/gem_tasks"
2
2
  require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new do |t|
5
- t.libs << "test"
6
- t.test_files = FileList['test/*_test.rb']
7
- t.verbose = true
8
- end
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :test
9
11
 
10
12
  task :build_web do
11
13
  Dir.chdir(File.join(File.dirname(__FILE__), "web"))
@@ -13,6 +15,14 @@ task :build_web do
13
15
  sh("npm run build")
14
16
  end
15
17
 
18
+ task :check_master do
19
+ current_branch = `git rev-parse --abbrev-ref HEAD`.chomp
20
+ if "master" != current_branch
21
+ $stderr.puts "\n===== Warning: Not on master. running on branch #{current_branch} =====\n\n"
22
+ end
23
+ end
24
+
25
+ Rake::Task[:release].prerequisites.unshift(:check_master)
16
26
  Rake::Task[:release].prerequisites.unshift(:build_web)
17
27
 
18
- task :default => :test
28
+ task :default => :test
@@ -7,8 +7,6 @@ require 'shiba/table_stats'
7
7
  require 'shiba/configure'
8
8
  require 'shiba/output'
9
9
 
10
- require 'optionparser'
11
-
12
10
  options = {}
13
11
 
14
12
  parser = Shiba::Configure.make_options_parser(options)
data/bin/review CHANGED
@@ -1,71 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  $LOAD_PATH << File.expand_path("../lib", File.dirname(__FILE__))
4
- require 'optionparser'
5
- require 'shiba/configure'
6
- require 'shiba/reviewer'
7
- require 'shiba/review/explain_diff'
8
4
  require 'shiba/review/cli'
9
- require 'json'
10
5
 
11
6
  cli = Shiba::Review::CLI.new
12
- cli.report_options("diff", "branch", "pull_request")
13
-
14
- if !cli.valid?
15
- $stderr.puts cli.failure
16
- exit 1
17
- end
18
-
19
- explain_diff = Shiba::Review::ExplainDiff.new(cli.options["file"], cli.options)
20
-
21
- problems = if explain_diff.diff_requested_by_user?
22
- result = explain_diff.result
23
-
24
- if result.message
25
- $stderr.puts result.message
26
- end
27
-
28
- if result.status == :pass
29
- exit
30
- end
31
-
32
- explain_diff.problems
33
- else
34
- explains = File.open(cli.options["file"]).each_line.map { |json| JSON.parse(json) }
35
- bad = explains.select { |explain| explain["severity"] && explain["severity"] != 'none' }
36
- bad.map { |explain| [ "#{explain["sql"]}:-2", explain ] }
37
- end
38
-
39
- repo_cmd = "git config --get remote.origin.url"
40
- repo_url = `#{repo_cmd}`.chomp
41
-
42
- if cli.options["verbose"]
43
- $stderr.puts "#{repo_cmd}\t#{repo_url}"
44
- end
45
-
46
- if repo_url.empty?
47
- $stderr.puts "'#{Dir.pwd}' does not appear to be a git repo"
48
- exit 1
49
- end
50
-
51
- # Generate comments for the problem queries
52
- reviewer = Shiba::Reviewer.new(repo_url, problems, cli.options)
53
-
54
- if !cli.options["submit"] || cli.options["verbose"]
55
- reviewer.comments.each do |c|
56
- puts "#{c[:path]}:#{c[:line]} (#{c[:position]})"
57
- puts c[:body]
58
- puts ""
59
- end
60
- end
61
-
62
- if cli.options["submit"]
63
- if reviewer.repo_host.empty? || reviewer.repo_path.empty?
64
- $stderr.puts "Invalid repo url '#{repo_url}' from git config --get remote.origin.url"
65
- exit 1
66
- end
67
-
68
- reviewer.submit
69
- end
70
-
71
- exit 2
7
+ exit cli.run
data/bin/shiba CHANGED
@@ -7,7 +7,8 @@ APP = File.basename(__FILE__)
7
7
  commands = {
8
8
  "explain" => "Generate a report from logged SQL queries",
9
9
  "review" => "Review changed files for query problems",
10
- "dump_stats" => "Collect database statistics for more accurate analysis"
10
+ "dump_stats" => "Collect database statistics for more accurate analysis",
11
+ "web" => "Generate a report from JSON-explain"
11
12
  }
12
13
 
13
14
  global = OptionParser.new do |opts|
@@ -38,4 +39,4 @@ end
38
39
 
39
40
  path = File.join(File.dirname(__FILE__), command)
40
41
 
41
- Kernel.exec(path, *ARGV)
42
+ Kernel.exec(path, *ARGV)
data/bin/web ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'shiba/output'
5
+ require 'json'
6
+ require 'optionparser'
7
+ require 'set'
8
+
9
+ options = {}
10
+
11
+ parser = OptionParser.new do |opts|
12
+ opts.on("-h", "--html FILE", "write html report here.") do |h|
13
+ options["html"] = h
14
+ end
15
+ end
16
+
17
+ parser.banner = "Shiba web: accept JSON-explain on stdin, write out an HTML report"
18
+ parser.parse!
19
+
20
+ if !options['html']
21
+ $stderr.puts parser.help
22
+ $stderr.puts "required option: --html"
23
+ exit 2
24
+ end
25
+
26
+ queries = []
27
+ query_md5s = Set.new
28
+
29
+ while line = $stdin.gets
30
+ query = JSON.parse(line)
31
+ next if query_md5s.include?(query['md5'])
32
+ queries << JSON.parse(line)
33
+ query_md5s << query['md5']
34
+ end
35
+
36
+ page = Shiba::Output.new(queries, { 'output' => options['html'] }).make_web!
37
+ if !File.exist?(page)
38
+ $stderr.puts("Failed to generate #{page}")
39
+ exit 2
40
+ end
41
+
42
+ $stderr.puts "Report available at #{page}"
43
+
@@ -75,7 +75,7 @@ module Shiba
75
75
  end
76
76
 
77
77
  def self.path
78
- @log_path ||= ENV['SHIBA_PATH'] || try_tmp || use_tmpdir
78
+ @log_path ||= ENV['SHIBA_DIR'] || try_tmp || use_tmpdir
79
79
  end
80
80
 
81
81
  private
@@ -94,10 +94,4 @@ module Shiba
94
94
  Dir.mkdir(path) if !Dir.exist?(path)
95
95
  path
96
96
  end
97
- end
98
-
99
- # This goes at the end so that Shiba.root is defined.
100
- if defined?(ActiveSupport.on_load)
101
- require 'shiba/activerecord_integration'
102
- Shiba::ActiveRecordIntegration.install!
103
- end
97
+ end
@@ -7,13 +7,17 @@ module Shiba
7
7
  # Integrates ActiveRecord with the Query Watcher by setting up the query log path, and the
8
8
  # connection options for the explain command, which it runs when the process exits.
9
9
  #
10
- # SHIBA_OUT and SHIBA_DEBUG=true environment variables may be set.
10
+ # SHIBA_DIR, SHIBA_QUERY_LOG_NAME and SHIBA_DEBUG=true environment variables may be set.
11
11
  class ActiveRecordIntegration
12
12
 
13
13
  attr_reader :path, :watcher
14
14
 
15
15
  def self.install!
16
16
  return false if @installed
17
+ if defined?(Rails.env) && Rails.env.production?
18
+ Rails.logger.error("Shiba watcher is not intended to run in production, stopping install.")
19
+ return false
20
+ end
17
21
 
18
22
  ActiveSupport.on_load(:active_record) do
19
23
  Shiba::ActiveRecordIntegration.start_watcher
@@ -32,7 +36,7 @@ module Shiba
32
36
  c = { host: cx.host, database: cx.db, username: cx.user, password: cx.pass, port: cx.port, server: 'postgres' }
33
37
  end
34
38
 
35
- options = {
39
+ {
36
40
  'host' => c[:host],
37
41
  'database' => c[:database],
38
42
  'user' => c[:username],
@@ -61,17 +65,16 @@ module Shiba
61
65
  end
62
66
 
63
67
  def self.log_path
64
- name = ENV["SHIBA_OUT"] || "query.log-#{Time.now.to_i}"
68
+ name = ENV["SHIBA_QUERY_LOG_NAME"] || "query.log-#{Time.now.to_i}"
65
69
  File.join(Shiba.path, name)
66
70
  end
67
71
 
68
72
  def self.run_explain(file, path)
69
73
  file.close
70
- puts ""
71
74
 
72
75
  cmd = "shiba explain #{database_args} --file #{path}"
73
- if ENV['SHIBA_OUT']
74
- cmd << " --json #{File.join(Shiba.path, "#{ENV["SHIBA_OUT"]}.json")}"
76
+ if ENV['SHIBA_QUERY_LOG_NAME']
77
+ cmd << " --json #{File.join(Shiba.path, "#{ENV["SHIBA_QUERY_LOG_NAME"]}.json")}"
75
78
  elsif Shiba::Configure.ci?
76
79
  cmd << " --json #{File.join(Shiba.path, 'ci.json')}"
77
80
  end
@@ -129,6 +129,12 @@ module Shiba
129
129
  end
130
130
 
131
131
  @cost = Shiba::Explain::COST_PER_ROW_READ * rows_read
132
+
133
+ # pin fully missed indexes to a 'low' threshold
134
+ if @access_type == 'access_type_tablescan'
135
+ @cost = [0.01, @cost].max
136
+ end
137
+
132
138
  @result.cost += @cost
133
139
 
134
140
  @tbl_message['cost'] = @cost
@@ -61,7 +61,11 @@ module Shiba
61
61
  def make_web!
62
62
  data = as_json
63
63
 
64
- index = File.read(File.join(WEB_PATH, "dist", "index.html"))
64
+ index_path = File.join(WEB_PATH, "dist", "index.html")
65
+ if !File.exist?(index_path)
66
+ raise Shiba::Error.new("dist/index.html not found. Try running 'rake build_web'")
67
+ end
68
+ index = File.read(index_path)
65
69
  data_block = "var shibaData = #{JSON.dump(as_json)};\n"
66
70
 
67
71
  index.sub!(%r{<script src=(.*?)>}) do |match|
@@ -52,7 +52,10 @@ module Shiba
52
52
  tables[table] << col
53
53
  elsif sc.scan(/(\d+|NULL|'.*?') AS `(.*?)`/m)
54
54
  else
55
- raise "unknown stuff: in #{@sql}: #{@sc.rest}"
55
+ if ENV['SHIBA_DEBUG']
56
+ raise Shiba::Error.new("unknown stuff: in #{@sql}: #{@sc.rest}")
57
+ end
58
+ return {}
56
59
  end
57
60
 
58
61
  sc.scan(/,/)
@@ -1,3 +1,9 @@
1
+ require 'optionparser'
2
+ require 'shiba/configure'
3
+ require 'shiba/reviewer'
4
+ require 'shiba/review/explain_diff'
5
+ require 'json'
6
+
1
7
  module Shiba
2
8
  module Review
3
9
  # Builds options for interacting with the reviewer via the command line.
@@ -18,13 +24,108 @@ module Shiba
18
24
  # => "An error message with command line help."
19
25
  class CLI
20
26
 
21
- attr_reader :errors
27
+ attr_reader :out, :err, :input
22
28
 
23
- def initialize
24
- @user_options = {}
29
+ # Options may be provided for testing, in which case the option parser is skipped.
30
+ # When this happens, default options are also skipped.
31
+ def initialize(out: $stdout, err: $stderr, input: $stdin, options: nil)
32
+ @out = out
33
+ @err = err
34
+ @input = input
35
+ @user_options = options || {}
25
36
  @errors = []
26
- parser.parse!
27
- @options = default_options.merge(@user_options)
37
+ parser.parse! if options.nil?
38
+ @options = options || default_options.merge(@user_options)
39
+ end
40
+
41
+ # Generates the review, returning an exit status code.
42
+ # Prints to @out / @err, which default to STDOUT/STDERR.
43
+ def run
44
+ report_options("diff", "branch", "pull_request")
45
+
46
+ if !valid?
47
+ err.puts failure
48
+ return 1
49
+ end
50
+
51
+ explain_diff = Shiba::Review::ExplainDiff.new(options["file"], options)
52
+
53
+ problems = if explain_diff.diff_requested_by_user?
54
+ result = explain_diff.result
55
+
56
+ if result.message
57
+ @err.puts result.message
58
+ end
59
+
60
+ if result.status == :pass
61
+ return 0
62
+ end
63
+
64
+ explain_diff.problems
65
+ else
66
+ # Find all problem explains
67
+ begin
68
+ explains = explain_file.each_line.map { |json| JSON.parse(json) }
69
+ bad = explains.select { |explain| explain["severity"] && explain["severity"] != 'none' }
70
+ bad.map { |explain| [ "#{explain["sql"]}:-2", explain ] }
71
+ rescue Interrupt
72
+ @err.puts "SIGINT: Canceled reading from STDIN. To read from an explain log, provide the --file option."
73
+ exit 1
74
+ end
75
+ end
76
+
77
+ if problems.empty?
78
+ return 0
79
+ end
80
+
81
+ # Dedup
82
+ problems.uniq! { |_,p| p["md5"] }
83
+
84
+ # Output problem explains, this can be provided as a file to shiba review for comments.
85
+ if options["raw"]
86
+ pr = options["pull_request"]
87
+ if pr
88
+ problems.each { |_,problem| problem["pull_request"] = pr }
89
+ end
90
+
91
+
92
+ problems.each { |_,problem| @out.puts JSON.dump(problem) }
93
+ return 2
94
+ end
95
+
96
+ # Generate comments for the problem queries
97
+ repo_cmd = "git config --get remote.origin.url"
98
+ repo_url = `#{repo_cmd}`.chomp
99
+
100
+ if options["verbose"]
101
+ @err.puts "#{repo_cmd}\t#{repo_url}"
102
+ end
103
+
104
+ if repo_url.empty?
105
+ @err.puts "'#{Dir.pwd}' does not appear to be a git repo"
106
+ return 1
107
+ end
108
+
109
+ reviewer = Shiba::Reviewer.new(repo_url, problems, options)
110
+
111
+ if !options["submit"] || options["verbose"]
112
+ reviewer.comments.each do |c|
113
+ @out.puts "#{c[:path]}:#{c[:line]} (#{c[:position]})"
114
+ @out.puts c[:body]
115
+ @out.puts ""
116
+ end
117
+ end
118
+
119
+ if options["submit"]
120
+ if reviewer.repo_host.empty? || reviewer.repo_path.empty?
121
+ @err.puts "Invalid repo url '#{repo_url}' from git config --get remote.origin.url"
122
+ return 1
123
+ end
124
+
125
+ reviewer.submit
126
+ end
127
+
128
+ return 2
28
129
  end
29
130
 
30
131
  def options
@@ -36,14 +137,12 @@ module Shiba
36
137
 
37
138
  validate_log_path
38
139
  #validate_git_repo if branch || options["submit"]
39
- description = "Provide an explain log, or run 'shiba explain' to generate one."
40
-
41
- require_option("file", description: description)
42
140
 
43
141
  if options["submit"]
44
142
  require_option("branch") if options["diff"].nil?
45
- require_option("token")
143
+ require_option("token", description: "This can be read from the $SHIBA_GITHUB_TOKEN environment variable.")
46
144
  require_option("pull_request")
145
+ error("Must specify either 'submit' or 'raw' output option, not both") if options["raw"]
47
146
  end
48
147
 
49
148
  @errors.empty?
@@ -69,16 +168,25 @@ module Shiba
69
168
 
70
169
  protected
71
170
 
171
+ def explain_file
172
+ options.key?('file') ? File.open(options['file']) : @input
173
+ end
174
+
72
175
  def parser
73
176
  @parser ||= OptionParser.new do |opts|
74
- opts.banner = "Review changes for query problems. Optionally submit the comments to a Github pull request."
177
+ opts.banner = "Reads from a file or stdin to review changes for query problems. Optionally submit the comments to a Github pull request."
75
178
 
76
- opts.separator "Required:"
179
+ opts.separator ""
180
+ opts.separator "IO options:"
77
181
 
78
- opts.on("-f","--file FILE", "The explain output log to compare with. Automatically configured when $CI environment variable is set") do |f|
182
+ opts.on("-f","--file FILE", "The JSON explain log to compare with. Automatically configured when $CI environment variable is set") do |f|
79
183
  @user_options["file"] = f
80
184
  end
81
185
 
186
+ opts.on("--raw", "Print the raw JSON with the pull request id") do |r|
187
+ @user_options["raw"] = r
188
+ end
189
+
82
190
  opts.separator ""
83
191
  opts.separator "Git diff options:"
84
192
 
@@ -105,7 +213,7 @@ module Shiba
105
213
  @user_options["pull_request"] = p
106
214
  end
107
215
 
108
- opts.on("-t", "--token TOKEN", "The Github API token to use for commenting. Defaults to $GITHUB_TOKEN.") do |t|
216
+ opts.on("-t", "--token TOKEN", "The Github API token to use for commenting. Defaults to $SHIBA_GITHUB_TOKEN.") do |t|
109
217
  @user_options["token"] = t
110
218
  end
111
219
 
@@ -117,13 +225,13 @@ module Shiba
117
225
  end
118
226
 
119
227
  opts.on_tail("-h", "--help", "Show this message") do
120
- puts opts
228
+ @out.puts opts
121
229
  exit
122
230
  end
123
231
 
124
232
  opts.on_tail("--version", "Show version") do
125
233
  require 'shiba/version'
126
- puts Shiba::VERSION
234
+ @out.puts Shiba::VERSION
127
235
  exit
128
236
  end
129
237
  end
@@ -139,12 +247,12 @@ module Shiba
139
247
  if Shiba::Configure.ci?
140
248
  report("Finding default options from CI environment.")
141
249
 
142
- defaults["file"] = ci_explain_log_path
143
- defaults["pull_request"] = ci_pull_request
144
- defaults["branch"] = ci_branch if !defaults['diff']
250
+ defaults["file"] = ci_explain_log_path if ci_explain_log_path
251
+ defaults["pull_request"] = ci_pull_request if ci_pull_request
252
+ defaults["branch"] = ci_branch if !defaults['diff'] && ci_branch
145
253
  end
146
254
 
147
- defaults["token"] = ENV['GITHUB_TOKEN'] if ENV['GITHUB_TOKEN']
255
+ defaults["token"] = ENV['SHIBA_GITHUB_TOKEN'] if ENV['SHIBA_GITHUB_TOKEN']
148
256
 
149
257
  defaults
150
258
  end
@@ -157,7 +265,8 @@ module Shiba
157
265
  end
158
266
 
159
267
  def ci_explain_log_path
160
- File.join(Shiba.path, 'ci.json')
268
+ name = ENV['SHIBA_QUERY_LOG_NAME'] || 'ci'
269
+ File.join(Shiba.path, "#{name}.json")
161
270
  end
162
271
 
163
272
  def ci_branch
@@ -184,7 +293,7 @@ module Shiba
184
293
  end
185
294
 
186
295
  def report(message)
187
- $stderr.puts message if @user_options["verbose"]
296
+ @err.puts message if @user_options["verbose"]
188
297
  end
189
298
 
190
299
  def error(message, help: false)