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 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