suggest-db-indices 0.0.2 → 0.0.3

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.
@@ -0,0 +1,16 @@
1
+ # Learnings from Clojure for make great benefit ruby
2
+
3
+ module Enumerable
4
+ # E.g., {:foo => {:bar => 5}}.get_in(:foo, :bar) #=> 5
5
+ def get_in keys, default = nil
6
+ v = self[keys.first]
7
+ rest = keys.drop 1
8
+ if rest.any?
9
+ v.get_in rest, default
10
+ else
11
+ v ? v : default
12
+ end
13
+ rescue NoMethodError => ex
14
+ default
15
+ end
16
+ end
@@ -4,7 +4,7 @@ module SuggestDbIndices
4
4
  @indexed_columns_by_table ||= connection.tables.reduce({}) do |h, table_name|
5
5
  # Note: can index on multiple columns, which complicates things. Assuming user has done
6
6
  # this correctly for now...
7
- h.merge table_name => connection.indexes(table_name).map {|index| index.columns}.flatten
7
+ h.merge table_name => connection.indexes(table_name).map { |index| index.columns }.flatten
8
8
  end
9
9
  end
10
10
 
@@ -47,45 +47,90 @@ module SuggestDbIndices
47
47
 
48
48
  def unindexed_foreign_key_columns_by_table
49
49
  unindexed_columns_by_table.reduce({}) do |h, (table, columns)|
50
- h.merge table => columns.select {|col| foreign_key?(col) }
50
+ h.merge table => columns.select { |col| foreign_key?(col) }
51
51
  end
52
52
  end
53
53
 
54
- def go! config = {}
55
- @config = default_options.reduce(config) do |h, (k,v)|
56
- if h[k]
57
- h
58
- else
59
- h.merge! k => v
54
+ def config
55
+ @config ||= default_config
56
+ end
57
+
58
+ def go! opts = {}
59
+ @config = opts.reduce(default_config) { |h, (k, v)| h.merge k => v }
60
+ table_col_pair_attributes = hash_of_hashes
61
+
62
+ unindexed_foreign_key_columns_by_table.each do |table, cols|
63
+ cols.each do |col|
64
+ table_col_pair = [table, col]
65
+ table_col_pair_attributes[table_col_pair][:foreign_key_column] = true
60
66
  end
61
67
  end
62
- generate_migration_file! format_index_migration_string unindexed_foreign_key_columns_by_table
68
+
69
+ scan_result = scan_log_files
70
+ columns_found_in_logs_count_by_table = scan_result[:queried_columns_by_table]
71
+ columns_found_in_logs_count_by_table.each do |table, column_hashes|
72
+ column_hashes.each do |col, found_count|
73
+ table_col_pair = [table, col]
74
+ table_col_pair_attributes[table_col_pair][:found_count] = found_count
75
+ end
76
+ end
77
+
78
+ table_col_pair_validator = @config[:mode] == :conservative \
79
+ ? lambda { |_, attributes| attributes[:foreign_key_column] && attributes[:found_count] }
80
+ : lambda { |_, attributes| attributes[:foreign_key_column] }
81
+
82
+ table_col_pairs_to_index_with_attributes =
83
+ table_col_pair_attributes.select &table_col_pair_validator
84
+
85
+ if table_col_pairs_to_index_with_attributes.any?
86
+ generate_migration_file! format_index_migration_string table_col_pairs_to_index_with_attributes
87
+ else
88
+ puts "No missing indexes found!"
89
+ end
63
90
  end
64
91
 
65
- def format_index_migration_string columns_by_table
66
- add_index_statements = columns_by_table.reduce('') do |s, (table, columns)|
67
- columns.each {|col| s += " add_index :#{table}, :#{col}\n" }
92
+ def format_index_migration_string table_col_pairs_to_index_with_attributes
93
+ add_index_statements = table_col_pairs_to_index_with_attributes.reduce('') do |s, (table_col_pair, attributes)|
94
+ table, col = table_col_pair
95
+ s += " add_index :#{table}, :#{col} #"
96
+ comments = []
97
+ comments << "foreign key" if attributes[:foreign_key_column]
98
+ comments << "found in queries #{attributes[:found_count]} times" if attributes[:found_count]
99
+ s += "#{comments.join(', ')}\n"
68
100
  s
69
101
  end
70
102
  " def change\n#{add_index_statements}\n end\nend"
71
103
  end
72
104
 
105
+ def name_migration_file
106
+ name = "add_indexes_via_suggest_db_indices"
107
+ existing_migration_files = Dir.glob File.join Rails.root, 'db', 'migrate/*.rb'
108
+
109
+ if existing_migration_files.any? { |f| f.end_with?("#{name}.rb") }
110
+ i = 1
111
+ i += 1 while existing_migration_files.any? { |f| f.end_with?("#{name}_#{i}.rb") }
112
+ name += "_#{i}"
113
+ end
114
+ name
115
+ end
116
+
73
117
  def generate_migration_file! migration_contents
74
- _ , migration_file_path = Rails::Generators.invoke("active_record:migration",
75
- ["add_indexes_via_suggest_db_indices_#{rand(36**8).to_s(36)}",
76
- 'BoiledGoose:Animal'])
118
+ _, migration_file_path = Rails::Generators.invoke("active_record:migration",
119
+ [name_migration_file,
120
+ 'BoiledGoose:Animal']) # Bogus param, doesn't matter since contents will be replaced
77
121
  file_contents = File.read migration_file_path
78
122
  search_string = "ActiveRecord::Migration"
79
123
  stop_index = (file_contents.index(search_string)) + search_string.length
80
124
  new_file_contents = file_contents[0..stop_index] + migration_contents
81
- File.open(migration_file_path, 'w') {|f| f.write(new_file_contents) }
125
+ File.open(migration_file_path, 'w') { |f| f.write(new_file_contents) }
126
+ puts "Migration result: \n #{new_file_contents}"
82
127
  migration_file_path
83
128
  end
84
129
 
85
- def default_options
130
+ def default_config
86
131
  {:num_lines_to_scan => 10000,
87
- :examine_logs => false,
88
- :log_dir => ""}
132
+ :examine_logs => false,
133
+ :log_dir => File.join(Rails.root, 'log')}
89
134
  end
90
135
 
91
136
  def prepare_log_file! log_dir
@@ -95,48 +140,52 @@ module SuggestDbIndices
95
140
  puts "Found log files: #{log_file_names.inspect}"
96
141
 
97
142
  puts "Tailing each file!"
98
- log_file_names.each {|f| sh_dbg "tail -n #{NUM_LINES_TO_READ} #{f} >> #{tmpfile.path}" }
99
- puts "Stripping color codes!"
100
- stripped_log_file = Tempfile.new('stripped')
101
- # Because text search is too tricky with colors
102
- strip_color_codes! tmpfile.path, stripped_log_file.path
103
- stripped_log_file
143
+ log_file_names.each { |f| sh_dbg "tail -n #{config[:num_lines_to_scan]} #{f} >> #{tmpfile.path}" }
144
+ tmpfile
145
+ end
146
+
147
+ def table_quote_char
148
+ @table_quote_char ||= connection.quote_table_name("boiled_goose")[0]
104
149
  end
105
150
 
106
- def scan_log_files_for_queried_columns log_dir, non_pk_columns_by_table = non_pk_columns_by_table
151
+ def column_quote_char
152
+ @column_quote_char ||= connection.quote_column_name("sauerkraut")[0]
153
+ end
154
+
155
+ # Scans log files for queried columns
156
+ def scan_log_files log_dir = config()[:log_dir]
107
157
  stripped_log_file = prepare_log_file! log_dir
108
158
 
109
- queried_columns_by_table = hash_of_arrays
110
- # For debugging: Record from which SQL statement we got each column
159
+ queried_columns_by_table = hash_of_hashes
160
+ # For debugging: Record from what table and columns we derived from each SQL statement
111
161
  inferred_table_columns_by_raw_where_clause = hash_of_sets
162
+ non_matches = Set.new
112
163
 
113
164
  while line = stripped_log_file.gets
114
165
  line = remove_limit_clause(line.strip)
115
- if matches = /SELECT.+FROM\s\W?(\w+)\W?\sWHERE(.+)/i.match(line)
116
- table = matches[1]
117
-
118
- raw_where_clause = matches[2]
119
- # puts "Where: #{raw_where_clause}"
120
-
121
- raw_where_clause.split.map do |s|
122
- s.gsub('`','')
123
- end.reduce([]) do |memo, identifier| #TODO: Stop reducing to array, reduce to counter
124
- if identifier.include?('.')
166
+ if matches = /SELECT.+WHERE(.+)/i.match(line) #Old: /.+SELECT.+FROM\s\W?(\w+)\W?\sWHERE(.+)/
167
+ raw_where_clause = matches[1]
168
+ # puts "Where: #{raw_where_clause}"
169
+ raw_where_clause.split.each do |identifier|
170
+ next if non_matches.include? identifier
171
+ # Go through the where clause to find columns that were queried
172
+ if identifier.include?('.') # e.g., "post"."user_id"
125
173
  current_table, column_candidate = identifier.split('.')
126
- else
127
- current_table, column_candidate = [table, identifier]
174
+ current_table.gsub! table_quote_char, ''
175
+ column_candidate.gsub! column_quote_char, ''
176
+ if non_pk_columns_by_table[current_table] && non_pk_columns_by_table[current_table].include?(column_candidate)
177
+ # We only care about the identifiers that match up to a table and column.
178
+ # This is a ghetto way to to avoid having to parse SQL (extremely difficult)
179
+ if queried_columns_by_table.get_in([current_table, column_candidate])
180
+ queried_columns_by_table[current_table][column_candidate] += 1
181
+ else
182
+ queried_columns_by_table[current_table] = {column_candidate => 1}
183
+ end
184
+ inferred_table_columns_by_raw_where_clause[raw_where_clause] << [current_table, column_candidate]
185
+ else
186
+ non_matches << identifier
187
+ end
128
188
  end
129
-
130
- if non_pk_columns_by_table[current_table].include? column_candidate
131
- # We only care about the identifiers that match up to a table and column.
132
- # This is a ghetto way to to avoid having to parse SQL (extremely difficult)
133
- memo << [current_table, column_candidate]
134
- else
135
- memo
136
- end
137
- end.each do |(table, column)|
138
- queried_columns_by_table[table] << column
139
- inferred_table_columns_by_raw_where_clause[raw_where_clause] << [table, column]
140
189
  end
141
190
  end
142
191
  end
@@ -145,11 +194,15 @@ module SuggestDbIndices
145
194
  end
146
195
 
147
196
  def hash_of_arrays
148
- Hash.new {|h, k| h[k] = [] }
197
+ Hash.new { |h, k| h[k] = [] }
198
+ end
199
+
200
+ def hash_of_hashes
201
+ Hash.new { |h, k| h[k] = Hash.new }
149
202
  end
150
203
 
151
204
  def hash_of_sets
152
- Hash.new {|h, k| h[k] = Set.new }
205
+ Hash.new { |h, k| h[k] = Set.new }
153
206
  end
154
207
 
155
208
  def remove_limit_clause s
@@ -160,12 +213,6 @@ module SuggestDbIndices
160
213
  end
161
214
  end
162
215
 
163
- def strip_color_codes! file_name, output_path
164
- # From: http://serverfault.com/a/154200
165
- sh_dbg 'sed "s/${esc}[^m]*m//g" ' + "#{file_name} >> #{output_path}"
166
- raise "There was a problem stripping colors" unless $?.success?
167
- end
168
-
169
216
  def sh_dbg cmd
170
217
  puts "Shelling: #{cmd}"
171
218
  `#{cmd}`
@@ -1,3 +1,3 @@
1
1
  module SuggestDbIndices
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -2,4 +2,5 @@ require 'tempfile'
2
2
  require 'rails/generators'
3
3
 
4
4
  require File.join File.dirname(__FILE__), '/suggest_db_indices/version'
5
+ require File.join File.dirname(__FILE__), '/suggest_db_indices/clojurian_imperialism'
5
6
  require File.join File.dirname(__FILE__), '/suggest_db_indices/core'
@@ -2,19 +2,21 @@
2
2
  require File.join File.dirname(__FILE__), 'lib/suggest_db_indices/version'
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.authors = ["Isak Sky"]
6
- gem.email = ["isak.sky@gmail.com"]
7
- gem.description = %q{A gem for your rails project that suggests indices for you to add in your database. Currently it suggests adding indices to unindexed foreign key columns.}
8
- gem.summary = %q{A gem for your rails project that suggests indices for you to add in your database.}
9
- gem.homepage = "https://github.com/isaksky/suggest-db-indices"
10
- gem.license = "MIT"
5
+ gem.authors = ["Isak Sky"]
6
+ gem.email = ["isak.sky@gmail.com"]
7
+ gem.description = %q{A gem for your rails project that suggests indices for you to add in your database. Currently it suggests adding indices to unindexed foreign key columns.}
8
+ gem.summary = %q{A gem for your rails project that suggests indices for you to add in your database.}
9
+ gem.homepage = "https://github.com/isaksky/suggest-db-indices"
10
+ gem.license = "MIT"
11
11
 
12
- gem.files = `git ls-files`.split($\)
13
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
- gem.name = "suggest-db-indices"
12
+ gem.files = `git ls-files`.split($\)
13
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.name = "suggest-db-indices"
16
16
  gem.require_paths = ["lib"]
17
- gem.version = SuggestDbIndices::VERSION
17
+ gem.version = SuggestDbIndices::VERSION
18
18
 
19
19
  gem.add_dependency('rails', '>= 3.0.0')
20
+ gem.add_development_dependency('awesome_print')
21
+ gem.add_development_dependency('pry')
20
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suggest-db-indices
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-11 00:00:00.000000000 Z
12
+ date: 2012-07-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
16
- requirement: &2152362540 !ruby/object:Gem::Requirement
16
+ requirement: &2161081920 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,29 @@ dependencies:
21
21
  version: 3.0.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2152362540
24
+ version_requirements: *2161081920
25
+ - !ruby/object:Gem::Dependency
26
+ name: awesome_print
27
+ requirement: &2161081300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2161081300
36
+ - !ruby/object:Gem::Dependency
37
+ name: pry
38
+ requirement: &2161080600 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2161080600
25
47
  description: A gem for your rails project that suggests indices for you to add in
26
48
  your database. Currently it suggests adding indices to unindexed foreign key columns.
27
49
  email:
@@ -36,6 +58,7 @@ files:
36
58
  - README.md
37
59
  - Rakefile
38
60
  - lib/suggest_db_indices.rb
61
+ - lib/suggest_db_indices/clojurian_imperialism.rb
39
62
  - lib/suggest_db_indices/core.rb
40
63
  - lib/suggest_db_indices/version.rb
41
64
  - suggest_db_indices.gemspec