shiba 0.2.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +11 -2
- data/Gemfile +1 -2
- data/Gemfile.lock +4 -2
- data/README.md +1 -1
- data/bin/explain +10 -41
- data/bin/mysql_dump_stats +20 -0
- data/bin/postgres_dump_stats +3 -0
- data/bin/review +181 -0
- data/bin/shiba +3 -3
- data/lib/shiba.rb +65 -4
- data/lib/shiba/activerecord_integration.rb +30 -13
- data/lib/shiba/checker.rb +89 -25
- data/lib/shiba/configure.rb +22 -5
- data/lib/shiba/connection.rb +25 -0
- data/lib/shiba/connection/mysql.rb +45 -0
- data/lib/shiba/connection/postgres.rb +91 -0
- data/lib/shiba/diff.rb +21 -11
- data/lib/shiba/explain.rb +18 -53
- data/lib/shiba/explain/mysql_explain.rb +47 -0
- data/lib/shiba/explain/postgres_explain.rb +91 -0
- data/lib/shiba/explain/postgres_explain_index_conditions.rb +137 -0
- data/lib/shiba/fuzzer.rb +16 -16
- data/lib/shiba/index_stats.rb +9 -5
- data/lib/shiba/output.rb +1 -1
- data/lib/shiba/output/tags.yaml +14 -8
- data/lib/shiba/query_watcher.rb +13 -1
- data/lib/shiba/review/api.rb +100 -0
- data/lib/shiba/review/comment_renderer.rb +62 -0
- data/lib/shiba/reviewer.rb +136 -0
- data/lib/shiba/version.rb +1 -1
- data/shiba.gemspec +2 -0
- data/web/dist/bundle.js +23 -1
- data/web/main.css +3 -0
- data/web/main.js +1 -0
- data/web/package-lock.json +5 -0
- data/web/package.json +1 -0
- data/web/results.html.erb +77 -20
- metadata +43 -5
- data/bin/check +0 -75
- data/bin/dump_stats +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8aaef4cac972cd661d5398bd510d0a78f4d1e078
|
4
|
+
data.tar.gz: 61d077a4f31b4ff21eb652c2c866e8c5f3bb9e49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80e2b32747df07efbbd89227b86347530ad955fdc4520f9179bbfe274440397b1063d6dfb50bf2c737f8e88460609e80ae86361b712f9c2e3b0d7ae86d55d728
|
7
|
+
data.tar.gz: 4b540f27e5033c153621a0f2292cba50786857f018d2de54f8d9a5f58755d0a6b8872b2dae5af11e097a98f9b18935499d176ed5aedcedb4a859ce7219c7fc0c
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -6,8 +6,17 @@ rvm:
|
|
6
6
|
|
7
7
|
services:
|
8
8
|
- mysql
|
9
|
+
- postgresql
|
10
|
+
|
11
|
+
env:
|
12
|
+
- SHIBA_TEST_ENV=test_postgres
|
13
|
+
- SHIBA_TEST_ENV=test_mysql
|
9
14
|
|
10
15
|
before_script:
|
11
16
|
- cp .travis/my.cnf ~/.my.cnf
|
12
|
-
|
13
|
-
|
17
|
+
|
18
|
+
after_script:
|
19
|
+
- bundle exec shiba review --submit --verbose
|
20
|
+
|
21
|
+
addons:
|
22
|
+
postgresql: "9.5"
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
shiba (0.
|
4
|
+
shiba (0.3.0)
|
5
5
|
activesupport
|
6
|
+
mysql2
|
7
|
+
pg
|
6
8
|
|
7
9
|
GEM
|
8
10
|
remote: https://rubygems.org/
|
@@ -25,6 +27,7 @@ GEM
|
|
25
27
|
concurrent-ruby (~> 1.0)
|
26
28
|
minitest (5.11.3)
|
27
29
|
mysql2 (0.5.2)
|
30
|
+
pg (1.1.4)
|
28
31
|
rake (10.5.0)
|
29
32
|
thread_safe (0.3.6)
|
30
33
|
tzinfo (1.2.5)
|
@@ -37,7 +40,6 @@ DEPENDENCIES
|
|
37
40
|
activerecord
|
38
41
|
bundler (~> 2.0)
|
39
42
|
byebug
|
40
|
-
mysql2
|
41
43
|
rake (~> 10.0)
|
42
44
|
shiba!
|
43
45
|
|
data/README.md
CHANGED
@@ -101,7 +101,7 @@ This information can be obtained by running the bin/dump_stats command in produc
|
|
101
101
|
production$
|
102
102
|
git clone https://github.com/burrito-brothers/shiba.git
|
103
103
|
cd shiba ; bundle
|
104
|
-
bin/
|
104
|
+
bin/mysql_dump_stats -d DATABASE_NAME -h HOST -u USER -pPASS > ~/shiba_index.yml
|
105
105
|
|
106
106
|
local$
|
107
107
|
scp production:~/shiba_index.yml RAILS_PROJECT/config
|
data/bin/explain
CHANGED
@@ -15,58 +15,27 @@ parser = Shiba::Configure.make_options_parser(options)
|
|
15
15
|
parser.banner = "Run a list of queries through shiba's analyzer."
|
16
16
|
parser.parse!
|
17
17
|
|
18
|
-
option_path = Shiba::Configure.mysql_config_path
|
19
|
-
|
20
|
-
if option_path
|
21
|
-
puts "Found config at #{option_path}" if options["verbose"]
|
22
|
-
options['default_file'] ||= option_path
|
23
|
-
end
|
24
|
-
|
25
|
-
option_file = if options['default_file'] && File.exist?(options['default_file'])
|
26
|
-
File.read(options['default_file'])
|
27
|
-
else
|
28
|
-
""
|
29
|
-
end
|
30
18
|
|
31
19
|
if options['json'] && options['html']
|
32
20
|
$stderr.puts "Can only output to json or html, not both"
|
33
21
|
$stderr.puts parser.banner
|
34
|
-
exit
|
35
|
-
end
|
36
|
-
|
37
|
-
if option_file && !options['default_group']
|
38
|
-
if option_file.include?("[client]")
|
39
|
-
options['default_group'] = 'client'
|
40
|
-
end
|
41
|
-
if option_file.include?("[mysql]")
|
42
|
-
options['default_group'] = 'mysql'
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def require_option(parser, name)
|
47
|
-
$stderr.puts "Required: #{name}"
|
48
|
-
$stderr.puts parser.banner
|
49
|
-
exit 1
|
50
|
-
end
|
51
|
-
|
52
|
-
if !options["username"] && !option_file.include?('user')
|
53
|
-
require_option(parser, 'username')
|
54
|
-
end
|
55
|
-
|
56
|
-
if !options["database"] && !option_file.include?('database')
|
57
|
-
require_option(parser, 'database')
|
22
|
+
exit 2
|
58
23
|
end
|
59
24
|
|
60
25
|
file = options.delete("file")
|
61
26
|
file = File.open(file, "r") if file
|
62
27
|
|
63
|
-
Shiba.configure(options)
|
28
|
+
Shiba.configure(options) do |err_msg|
|
29
|
+
$stderr.puts(err_msg)
|
30
|
+
$stderr.puts(parser)
|
31
|
+
exit 2
|
32
|
+
end
|
64
33
|
|
65
34
|
schema_stats_fname = options["stats"]
|
66
35
|
|
67
36
|
if schema_stats_fname && !File.exist?(schema_stats_fname)
|
68
37
|
$stderr.puts "No such file: #{schema_stats_fname}"
|
69
|
-
exit
|
38
|
+
exit 2
|
70
39
|
end
|
71
40
|
|
72
41
|
file = $stdin if file.nil?
|
@@ -88,16 +57,16 @@ if problems.any?
|
|
88
57
|
$stderr.puts "#{problems.size} problematic #{query_word} detected"
|
89
58
|
|
90
59
|
if options['json']
|
91
|
-
exit
|
60
|
+
exit 1
|
92
61
|
end
|
93
62
|
|
94
63
|
page = Shiba::Output.new(queries, { 'output' => options['html'] }).make_web!
|
95
64
|
|
96
65
|
if !File.exist?(page)
|
97
66
|
$stderr.puts("Failed to generate #{page}")
|
98
|
-
exit
|
67
|
+
exit 2
|
99
68
|
end
|
100
69
|
|
101
70
|
$stderr.puts "Report available at #{page}"
|
102
|
-
exit
|
71
|
+
exit 1
|
103
72
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'shiba'
|
5
|
+
require 'shiba/configure'
|
6
|
+
require 'shiba/fuzzer'
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
parser = Shiba::Configure.make_options_parser(options, only_basics: true)
|
10
|
+
parser.banner = "Dump database statistics into yaml file."
|
11
|
+
parser.parse!
|
12
|
+
|
13
|
+
Shiba.configure(options) do |errmsg|
|
14
|
+
$stderr.puts(errmsg)
|
15
|
+
$stderr.puts(parser.help)
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
|
19
|
+
index = Shiba::Fuzzer.new(Shiba.connection).fetch_index
|
20
|
+
puts index.to_yaml
|
data/bin/review
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << File.expand_path("../lib", File.dirname(__FILE__))
|
4
|
+
require 'optionparser'
|
5
|
+
require 'shiba/reviewer'
|
6
|
+
require 'shiba/checker'
|
7
|
+
require 'shiba/configure'
|
8
|
+
|
9
|
+
options = {}
|
10
|
+
parser = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Review changes for query problems. Optionally submit the comments to a Github pull request."
|
12
|
+
|
13
|
+
opts.separator "Required:"
|
14
|
+
|
15
|
+
opts.on("-f","--file FILE", "The explain output log to compare with. Automatically configured when $CI environment variable is set") do |f|
|
16
|
+
options["file"] = f
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.separator ""
|
20
|
+
opts.separator "Git diff options:"
|
21
|
+
|
22
|
+
opts.on("-b", "--branch GIT_BRANCH", "Compare to changes between origin/HEAD and BRANCH. Attempts to read from CI environment when not set.") do |b|
|
23
|
+
options["branch"] = b
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("--staged", "Only check files that are staged for commit") do
|
27
|
+
options["staged"] = true
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on("--unstaged", "Only check files that are not staged for commit") do
|
31
|
+
options["unstaged"] = true
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.separator ""
|
35
|
+
opts.separator "Github options:"
|
36
|
+
|
37
|
+
opts.on("--submit", "Submit comments to Github") do
|
38
|
+
options["submit"] = true
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("-p", "--pull-request PR_ID", "The ID of the pull request to comment on. Attempts to read from CI environment when not set.") do |p|
|
42
|
+
options["pull_request"] = p
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("-t", "--token TOKEN", "The Github API token to use for commenting. Defaults to $GITHUB_TOKEN.") do |t|
|
46
|
+
options["token"] = t
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.separator ""
|
50
|
+
opts.separator "Common options:"
|
51
|
+
|
52
|
+
opts.on("--verbose", "Verbose/debug mode") do
|
53
|
+
options["verbose"] = true
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
57
|
+
puts opts
|
58
|
+
exit
|
59
|
+
end
|
60
|
+
|
61
|
+
opts.on_tail("--version", "Show version") do
|
62
|
+
require 'shiba/version'
|
63
|
+
puts Shiba::VERSION
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
end
|
67
|
+
parser.parse!
|
68
|
+
|
69
|
+
# This is a noop since it's the default behavior. Ignore.
|
70
|
+
if options["staged"] && options["unstaged"]
|
71
|
+
options.delete("staged")
|
72
|
+
options.delete("unstaged")
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
log = options["file"]
|
77
|
+
|
78
|
+
if log.nil? && Shiba::Configure.ci?
|
79
|
+
log = options["file"] = File.join(Shiba.path, 'ci.json')
|
80
|
+
$stderr.puts "CI detected, setting file to #{log}" if options["verbose"]
|
81
|
+
end
|
82
|
+
|
83
|
+
if log.nil?
|
84
|
+
$stderr.puts "Provide an explain log, or run 'shiba explain' to generate one."
|
85
|
+
$stderr.puts ""
|
86
|
+
$stderr.puts parser
|
87
|
+
exit 1
|
88
|
+
end
|
89
|
+
|
90
|
+
if !File.exist?(log)
|
91
|
+
$stderr.puts "File not found: '#{log}'"
|
92
|
+
exit 1
|
93
|
+
end
|
94
|
+
|
95
|
+
pr_sha = ENV['TRAVIS_PULL_REQUEST_SHA'] || ENV['CIRCLE_BRANCH']
|
96
|
+
|
97
|
+
if options["branch"] == nil && pr_sha && !pr_sha.empty?
|
98
|
+
options["branch"] = pr_sha
|
99
|
+
end
|
100
|
+
|
101
|
+
if options["token"] == nil
|
102
|
+
options["token"] = ENV['GITHUB_TOKEN']
|
103
|
+
end
|
104
|
+
|
105
|
+
# https://circleci.com/docs/2.0/env-vars/
|
106
|
+
# This may be wrong for circle ci
|
107
|
+
pr_id = ENV['TRAVIS_PULL_REQUEST'] || ENV['CIRCLE_PR_NUMBER']
|
108
|
+
|
109
|
+
if options["pull_request"] == nil && pr_id && !pr_id.empty?
|
110
|
+
options["pull_request"] = pr_id
|
111
|
+
end
|
112
|
+
|
113
|
+
if options["verbose"]
|
114
|
+
$stderr.puts "DIFF: #{ENV['DIFF']}" if ENV['DIFF']
|
115
|
+
$stderr.puts "branch: #{options["branch"].inspect}" if options["branch"]
|
116
|
+
$stderr.puts "pull_request: #{options["pull_request"]}" if options["pull_request"]
|
117
|
+
end
|
118
|
+
|
119
|
+
repo_cmd = "git config --get remote.origin.url"
|
120
|
+
repo_url = `#{repo_cmd}`.chomp
|
121
|
+
|
122
|
+
if options["verbose"]
|
123
|
+
$stderr.puts "#{repo_cmd}\t#{repo_url}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def require_option(parser, name)
|
127
|
+
$stderr.puts "Required: #{name}"
|
128
|
+
$stderr.puts ""
|
129
|
+
$stderr.puts parser
|
130
|
+
exit 1
|
131
|
+
end
|
132
|
+
|
133
|
+
if repo_url.empty?
|
134
|
+
$stderr.puts "'#{Dir.pwd}' does not appear to be a git repo"
|
135
|
+
exit 1
|
136
|
+
end
|
137
|
+
|
138
|
+
if options["submit"]
|
139
|
+
if (options["branch"].nil? || options["branch"].empty?) && (ENV['DIFF'].nil? || ENV['DIFF'].empty?)
|
140
|
+
require_option(parser, "branch")
|
141
|
+
end
|
142
|
+
require_option(parser, "token") if options["token"].nil?
|
143
|
+
require_option(parser, "pull_request") if options["pull_request"].nil?
|
144
|
+
end
|
145
|
+
|
146
|
+
if ENV['DIFF']
|
147
|
+
options['diff'] = ENV['DIFF']
|
148
|
+
end
|
149
|
+
|
150
|
+
# Check to see if the log overlaps with the git diff
|
151
|
+
result = Shiba::Checker.new(options).run(log)
|
152
|
+
|
153
|
+
if result.message
|
154
|
+
$stderr.puts result.message
|
155
|
+
end
|
156
|
+
|
157
|
+
if result.status == :pass
|
158
|
+
exit
|
159
|
+
end
|
160
|
+
|
161
|
+
# Generate comments for the problem queries
|
162
|
+
reviewer = Shiba::Reviewer.new(repo_url, result.problems, options)
|
163
|
+
|
164
|
+
if !options["submit"] || options["verbose"]
|
165
|
+
reviewer.comments.each do |c|
|
166
|
+
puts "#{c[:path]}:#{c[:line]} (#{c[:position]})"
|
167
|
+
puts c[:body]
|
168
|
+
puts ""
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
if options["submit"]
|
173
|
+
if reviewer.repo_host.empty? || reviewer.repo_path.empty?
|
174
|
+
$stderr.puts "Invalid repo url '#{repo_url}' from git config --get remote.origin.url"
|
175
|
+
exit 1
|
176
|
+
end
|
177
|
+
|
178
|
+
reviewer.submit
|
179
|
+
end
|
180
|
+
|
181
|
+
exit 2
|
data/bin/shiba
CHANGED
@@ -6,7 +6,7 @@ APP = File.basename(__FILE__)
|
|
6
6
|
|
7
7
|
commands = {
|
8
8
|
"explain" => "Generate a report from logged SQL queries",
|
9
|
-
"
|
9
|
+
"review" => "Review changed files for query problems",
|
10
10
|
}
|
11
11
|
|
12
12
|
global = OptionParser.new do |opts|
|
@@ -31,8 +31,8 @@ if command.nil?
|
|
31
31
|
end
|
32
32
|
|
33
33
|
if !commands.key?(command)
|
34
|
-
|
35
|
-
|
34
|
+
puts "#{APP}: '#{command}' is not a '#{APP}' command. See '#{APP} --help'."
|
35
|
+
exit 2
|
36
36
|
end
|
37
37
|
|
38
38
|
path = File.join(File.dirname(__FILE__), command)
|
data/lib/shiba.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "shiba/version"
|
2
2
|
require "shiba/configure"
|
3
|
+
require "shiba/connection"
|
3
4
|
require "mysql2"
|
4
5
|
require "pp"
|
5
6
|
require "byebug" if ENV['SHIBA_DEBUG']
|
@@ -7,12 +8,46 @@ require "byebug" if ENV['SHIBA_DEBUG']
|
|
7
8
|
module Shiba
|
8
9
|
class Error < StandardError; end
|
9
10
|
|
10
|
-
def self.configure(options)
|
11
|
-
|
11
|
+
def self.configure(options, &block)
|
12
|
+
configure_mysql_defaults(options, &block)
|
13
|
+
|
14
|
+
@connection_hash = options.select { |k, v| [ 'default_file', 'default_group', 'server', 'username', 'database', 'host', 'password', 'port'].include?(k) }
|
12
15
|
@main_config = Configure.read_config_file(options['config'], "config/shiba.yml")
|
13
16
|
@index_config = Configure.read_config_file(options['index'], "config/shiba_index.yml")
|
14
17
|
end
|
15
18
|
|
19
|
+
def self.configure_mysql_defaults(options, &block)
|
20
|
+
option_path = Shiba::Configure.mysql_config_path
|
21
|
+
|
22
|
+
if option_path
|
23
|
+
puts "Found config at #{option_path}" if options["verbose"]
|
24
|
+
options['default_file'] ||= option_path
|
25
|
+
end
|
26
|
+
|
27
|
+
option_file = if options['default_file'] && File.exist?(options['default_file'])
|
28
|
+
File.read(options['default_file'])
|
29
|
+
else
|
30
|
+
""
|
31
|
+
end
|
32
|
+
|
33
|
+
if option_file && !options['default_group']
|
34
|
+
if option_file.include?("[client]")
|
35
|
+
options['default_group'] = 'client'
|
36
|
+
end
|
37
|
+
if option_file.include?("[mysql]")
|
38
|
+
options['default_group'] = 'mysql'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if !options["username"] && !option_file.include?('user')
|
43
|
+
yield('Required: --username')
|
44
|
+
end
|
45
|
+
|
46
|
+
if !options["database"] && !option_file.include?('database')
|
47
|
+
yield('Required: --database')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
16
51
|
def self.config
|
17
52
|
@main_config
|
18
53
|
end
|
@@ -22,16 +57,42 @@ module Shiba
|
|
22
57
|
end
|
23
58
|
|
24
59
|
def self.connection
|
25
|
-
@connection
|
60
|
+
return @connection if @connection
|
61
|
+
@connection = Shiba::Connection.build(@connection_hash)
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.database
|
65
|
+
@connection_hash['database']
|
26
66
|
end
|
27
67
|
|
28
68
|
def self.root
|
29
69
|
File.dirname(__dir__)
|
30
70
|
end
|
71
|
+
|
72
|
+
def self.path
|
73
|
+
@log_path ||= ENV['SHIBA_PATH'] || try_tmp || use_tmpdir
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def self.try_tmp
|
79
|
+
return if !Dir.exist?('/tmp')
|
80
|
+
return if !File.writable?('/tmp')
|
81
|
+
|
82
|
+
path = File.join('/tmp', 'shiba')
|
83
|
+
Dir.mkdir(path) if !Dir.exist?(path)
|
84
|
+
path
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.use_tmpdir
|
88
|
+
path = File.join(Dir.tmpdir, 'shiba')
|
89
|
+
Dir.mkdir(path) if !Dir.exist?(path)
|
90
|
+
path
|
91
|
+
end
|
31
92
|
end
|
32
93
|
|
33
94
|
# This goes at the end so that Shiba.root is defined.
|
34
95
|
if defined?(ActiveSupport.on_load)
|
35
96
|
require 'shiba/activerecord_integration'
|
36
97
|
Shiba::ActiveRecordIntegration.install!
|
37
|
-
end
|
98
|
+
end
|