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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ad898fa0f36457fbd570698c276b567f6f09f1a2
4
- data.tar.gz: '028e8a99dff1f8c843bcb222151a0c100f0ebbe1'
3
+ metadata.gz: 8aaef4cac972cd661d5398bd510d0a78f4d1e078
4
+ data.tar.gz: 61d077a4f31b4ff21eb652c2c866e8c5f3bb9e49
5
5
  SHA512:
6
- metadata.gz: 716210a01a6fded73a217b03992371c4d8dcff472f1a69a6195cb53e1dfb122b8c43c512fa7f4ef30bbf776ee8eff3fcabe356bb3e5f4967cec5a4383ec1675a
7
- data.tar.gz: 4f7434ec7c78fa7134947ecfe90d9706e4cb22e800eb8036b03216522dee96c8d42e2a33409c5b40e9f4fbb478e48bf6a5e127c3fe0c05284819bd9d87f392ea
6
+ metadata.gz: 80e2b32747df07efbbd89227b86347530ad955fdc4520f9179bbfe274440397b1063d6dfb50bf2c737f8e88460609e80ae86361b712f9c2e3b0d7ae86d55d728
7
+ data.tar.gz: 4b540f27e5033c153621a0f2292cba50786857f018d2de54f8d9a5f58755d0a6b8872b2dae5af11e097a98f9b18935499d176ed5aedcedb4a859ce7219c7fc0c
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /tmp/
9
9
  .*.sw*
10
10
  node_modules
11
+ test/database.yml
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
- - mysql -e 'create database shiba_test;'
13
- - mysql shiba_test -e 'source test/structure.sql'
17
+
18
+ after_script:
19
+ - bundle exec shiba review --submit --verbose
20
+
21
+ addons:
22
+ postgresql: "9.5"
data/Gemfile CHANGED
@@ -1,9 +1,8 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "mysql2"
4
3
  gem "byebug"
5
4
  gemspec
6
5
 
7
6
  group :test do
8
7
  gem 'activerecord'
9
- end
8
+ end
data/Gemfile.lock CHANGED
@@ -1,8 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shiba (0.2.3)
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/dump_stats DATABASE_NAME [MYSQLOPTS] > ~/shiba_index.yml
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 1
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 1
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 3
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 1
67
+ exit 2
99
68
  end
100
69
 
101
70
  $stderr.puts "Report available at #{page}"
102
- exit 3
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
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ exec `dirname $0`/dump_stats --server postgres $*
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
- "check" => "Check staged files for query problems",
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
- puts "#{APP}: '#{command}' is not a '#{APP}' command. See '#{APP} --help'."
35
- exit 1
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
- @connection_hash = options.select { |k, v| [ 'default_file', 'default_group', 'username', 'database', 'host', 'password'].include?(k) }
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 ||= Mysql2::Client.new(@connection_hash)
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