shiba 0.2.3 → 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/.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
|