findex 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Flip Sasser
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,55 @@
1
+ # Findex #
2
+
3
+ Findex is a simple collection of Rake tasks that will help you locate missing database indexes in your Rails app. It can also generate migrations and run them, as well as filter by specific column types, names, and tables.
4
+
5
+ **Please note** that Findex designed to find any potentially overlooked indexes. It is not a good practice to index every matching column it returns - that's way too many.
6
+
7
+ ## Installation ##
8
+
9
+ Install Findex as a gem:
10
+
11
+ $ gem install findex --source=http://gems.github.com
12
+
13
+ You may want to configure it as a gem in environment.rb instead, since you're going to need it in your Rakefile:
14
+
15
+ config.gem 'findex', :source => 'http://gemcutter.org'
16
+
17
+ ... then run:
18
+
19
+ rake gems:install
20
+
21
+ In either case. open your Rails app's Rakefile and add the following line:
22
+
23
+ require 'findex/tasks'
24
+
25
+ And it's installed. Bam.
26
+
27
+ ## Find Missing Indexes ##
28
+
29
+ First, get some instructions:
30
+
31
+ $ rake db:indexes:help
32
+
33
+ rake db:indexes will generate a list of indexes your application's database may or may not need.
34
+
35
+ To see a list of all indexes it thinks you need, just use rake db:indexes
36
+
37
+ You can add migration=true to generate a migration file
38
+ or perform=true to perform the indexing immediately:
39
+ `rake db:indexes migration=true`
40
+
41
+ You can also target specific column types, like so:
42
+ `rake db:indexes:boolean`
43
+ `rake db:indexes:datetime`
44
+ `rake db:indexes:geo`
45
+ `rake db:indexes:primary`
46
+ `rake db:indexes:relationships`
47
+
48
+ You can also filter by column names and types, or by whole tables:
49
+ `rake db:indexes:names names=type,state`
50
+ `rake db:indexes:types types=integer,decimal`
51
+ `rake db:indexes tables=users,posts`
52
+
53
+ Read the instructions above and start finding missing indexes! Thanks to Matt Janowski for the inspiration (http://robots.thoughtbot.com/post/163627511/a-grand-piano-for-your-violin) and Thoughtbot / Jon Yurek for the core of the indexes detection code!
54
+
55
+ Copyright (c) 2009 Flip Sasser, released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gemspec|
8
+ gemspec.name = "findex"
9
+ gemspec.summary = "Rake tasks to check your Rails models for missing indexes"
10
+ gemspec.description = ""
11
+ gemspec.email = "flip@x451.com"
12
+ gemspec.homepage = "http://github.com/flipsasser/findex"
13
+ gemspec.authors = ["Flip Sasser"]
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
17
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/findex.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{findex}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Flip Sasser"]
12
+ s.date = %q{2009-11-12}
13
+ s.description = %q{}
14
+ s.email = %q{flip@x451.com}
15
+ s.extra_rdoc_files = [
16
+ "README.markdown"
17
+ ]
18
+ s.files = [
19
+ "MIT-LICENSE",
20
+ "README.markdown",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "tasks/db/indexes.rake"
24
+ ]
25
+ s.homepage = %q{http://github.com/flipsasser/findex}
26
+ s.rdoc_options = ["--charset=UTF-8"]
27
+ s.require_paths = ["lib"]
28
+ s.rubygems_version = %q{1.3.5}
29
+ s.summary = %q{Rake tasks to check your Rails models for missing indexes}
30
+
31
+ if s.respond_to? :specification_version then
32
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
33
+ s.specification_version = 3
34
+
35
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
36
+ else
37
+ end
38
+ else
39
+ end
40
+ end
41
+
@@ -0,0 +1,261 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/tasklib'
4
+
5
+ namespace :db do
6
+ desc 'Finds indexes your application probably needs'
7
+ task :indexes => [:environment, :prepare] do
8
+ indices = Findex.get_indices(:geo, [:name, [:id, :type]], :primary, :reflection, [:type, [:boolean, :date, :datetime, :time]])
9
+ Findex.send_indices(indices)
10
+ end
11
+
12
+ task :prepare do
13
+ @generate_migration = ENV['migration'] == 'true'
14
+ @perform_index = ENV['perform'] == 'true'
15
+ @tables = ENV['tables'] ? ENV['tables'].split(',').map(&:strip) : nil
16
+ end
17
+
18
+ namespace :indexes do
19
+ desc 'Finds unindexed boolean columns'
20
+ task :boolean => [:environment, :prepare] do
21
+ @migration_name = 'boolean'
22
+ Findex.send_indices(Findex.get_indices([:type, [:boolean]]))
23
+ end
24
+
25
+ desc 'Finds unindexed date, time, and datetime columns'
26
+ task :datetime => [:environment, :prepare] do
27
+ @migration_name = 'datetime'
28
+ Findex.send_indices(Findex.get_indices([:type, [:date, :datetime, :time]]))
29
+ end
30
+
31
+ desc 'Finds unindexed geo columns'
32
+ task :geo => [:environment, :prepare] do
33
+ @migration_name = 'geo'
34
+ Findex.send_indices(Findex.get_indices(:geo))
35
+ end
36
+
37
+ desc 'Prints instructions on how to use rake:db:indexes'
38
+ task :help do
39
+ puts ''
40
+ puts " rake db:indexes will generate a list of indexes your application's database may or may not need."
41
+ puts ''
42
+ puts ' To see a list of all indexes it thinks you need, just use rake db:indexes'
43
+ puts ''
44
+ puts " You can add migration=true to generate a migration file\n or perform=true to perform the indexing immediately:"
45
+ puts ' `rake db:indexes migration=true`'
46
+ puts ''
47
+ puts ' You can also target specific column types, like so:'
48
+ for type in [:boolean, :datetime, :geo, :primary, :relationships]
49
+ puts " `rake db:indexes:#{type}`"
50
+ end
51
+ puts ''
52
+ puts ' You can also filter by column names and types, or by whole tables:'
53
+ puts ' `rake db:indexes:names names=type,state`'
54
+ puts ' `rake db:indexes:types types=integer,decimal`'
55
+ puts ' `rake db:indexes tables=users,posts`'
56
+ puts ''
57
+ end
58
+
59
+ desc 'Generates a migration file with the recommended indexes'
60
+ task :migration => :environment do
61
+ @generate_migration = true
62
+ @perform_index = false
63
+ indices = Findex.get_indices(:geo, [:name, [:id, :type]], :primary, :reflection, [:type, [:boolean, :date, :datetime, :time]])
64
+ Findex.send_indices(indices)
65
+ end
66
+
67
+ desc 'Finds unindexed columns matching the names you supply'
68
+ task :names => [:environment, :prepare] do
69
+ if ENV['names']
70
+ indices = Findex.get_indices([:name, ENV['names'].split(',').map(&:strip).map(&:intern)])
71
+ Findex.send_indices(indices)
72
+ else
73
+ puts ''
74
+ puts ' You must pass in a comma-separated collection of names like so'
75
+ puts ' `rake db:indexes:names names=type,state`'
76
+ puts ''
77
+ end
78
+ end
79
+
80
+ desc 'Performs a migration with the recommended indexes'
81
+ task :perform => :environment do
82
+ @generate_migration = false
83
+ @perform_index = true
84
+ indices = Findex.get_indices(:geo, [:name, [:id, :type]], :primary, :reflection, [:type, [:boolean, :date, :datetime, :time]])
85
+ Findex.send_indices(indices)
86
+ end
87
+
88
+ desc 'Finds unindexed primary keys'
89
+ task :primary => [:environment, :prepare] do
90
+ @migration_name = 'primary'
91
+ Findex.send_indices(Findex.get_indices(:primary))
92
+ end
93
+
94
+ desc 'Finds unindexed relationship foreign keys'
95
+ task :relationships => [:environment, :prepare] do
96
+ @migration_name = 'relationship'
97
+ Findex.send_indices(Findex.get_indices(:reflection))
98
+ end
99
+
100
+ desc 'Finds unindexed columns matching the types you supply'
101
+ task :types => [:environment, :prepare] do
102
+ if ENV['types']
103
+ indices = Findex.get_indices([:type, ENV['types'].split(',').map(&:strip).map(&:intern)])
104
+ Findex.send_indices(indices)
105
+ else
106
+ puts ''
107
+ puts ' You must pass in a comma-separated collection of types like so'
108
+ puts ' `rake db:indexes:types types=integer,decimal`'
109
+ puts ''
110
+ end
111
+ end
112
+
113
+ end
114
+ end
115
+
116
+ module Findex
117
+ def self.check_index(*args)
118
+ index = args.shift
119
+ !args.any?{|array| array.any?{|comparison_index| comparison_index == index}}
120
+ end
121
+
122
+ def self.collect_indices(indices)
123
+ indices.collect{|table, columns| [table, columns.sort{|a, b|
124
+ if a == :id
125
+ -1
126
+ elsif b == :id
127
+ 1
128
+ else
129
+ (a.is_a?(Array) ? a.map(&:to_s).join('_') : a.to_s) <=> (b.is_a?(Array) ? b.map(&:to_s).join('_') : b.to_s)
130
+ end
131
+ }]}.sort{|a, b| a[0].to_s <=> b[0].to_s}
132
+ end
133
+
134
+ def self.connection
135
+ @connection ||= ActiveRecord::Base.connection
136
+ end
137
+
138
+ def self.get_indices(*args)
139
+ indices = {}
140
+ ObjectSpace.each_object(Class) do |model|
141
+ next unless model.ancestors.include?(ActiveRecord::Base) && model != ActiveRecord::Base && model.table_exists?
142
+ next if @tables && !@tables.include?(model.table_name.to_s)
143
+ existing_indices = connection.indexes(model.table_name).map{|index| index.columns.length == 1 ? index.columns.first.to_sym : index.columns.map(&:to_sym) }
144
+ args.each do |method, options|
145
+ indices = send("get_model_#{method}_indices", *[model, options, indices, existing_indices].compact)
146
+ end
147
+ end
148
+ collect_indices(indices)
149
+ end
150
+
151
+ def self.get_model_geo_indices(model, indices, existing_indices)
152
+ indices[model.table_name] ||= []
153
+ parse_columns(model) do |column, column_name|
154
+ if column.type == :decimal && column.name =~ /(lat|lng)/ && model.column_names.include?(alternate_column_name = column.name.gsub(/(^|_)(lat|lng)($|_)/) { "#{$1}#{$2 == 'lat' ? 'lng' : 'lat'}#{$3}"})
155
+ index = [column_name, alternate_column_name.to_sym]
156
+ indices[model.table_name].push([column_name, alternate_column_name.to_sym]) if check_index(index, indices[model.table_name], existing_indices)
157
+ end
158
+ end
159
+ indices
160
+ end
161
+
162
+ def self.get_model_name_indices(model, names, indices, existing_indices)
163
+ indices[model.table_name] ||= []
164
+ parse_columns(model) do |column, column_name|
165
+ if names.include?(column_name) && check_index(column_name, indices[model.table_name], existing_indices)
166
+ indices[model.table_name].push(column_name)
167
+ end
168
+ end
169
+ indices
170
+ end
171
+
172
+ def self.get_model_primary_indices(model, indices, existing_indices)
173
+ indices[model.table_name] ||= []
174
+ parse_columns(model) do |column, column_name|
175
+ if column.primary && check_index(column_name, indices[model.table_name], existing_indices)
176
+ indices[model.table_name].push(column_name)
177
+ end
178
+ end
179
+ indices
180
+ end
181
+
182
+ def self.get_model_reflection_indices(model, indices, existing_indices)
183
+ indices[model.table_name] ||= []
184
+ for name, reflection in model.reflections
185
+ case reflection.macro.to_sym
186
+ when :belongs_to
187
+ foreign_key = reflection.primary_key_name.to_sym
188
+ indices[model.table_name].push(foreign_key) if check_index(foreign_key, indices[model.table_name], existing_indices)
189
+ when :has_and_belongs_to_many
190
+ index = [reflection.primary_key_name.to_sym, reflection.association_foreign_key.to_sym]
191
+ if (table_name = reflection.options[:join_table] || reflection.options['join_table']) && connection.table_exists?(table_name)
192
+ indices[table_name] ||= []
193
+ unless indices[table_name].any?{|existing_index| existing_index == index} || connection.indexes(table_name).map{|join_index| join_index.columns.length == 1 ? join_index.columns.first.to_sym : join_index.columns.map(&:to_sym) }.any?{|existing_index| existing_index == index}
194
+ indices[table_name].push(index)
195
+ end
196
+ end
197
+ end
198
+ end
199
+ indices
200
+ end
201
+
202
+ def self.get_model_type_indices(model, types, indices, existing_indices)
203
+ indices[model.table_name] ||= []
204
+ parse_columns(model) do |column, column_name|
205
+ if types.include?(column.type) && check_index(column_name, indices[model.table_name], existing_indices)
206
+ indices[model.table_name].push(column_name)
207
+ end
208
+ end
209
+ indices
210
+ end
211
+
212
+ def self.parse_columns(model)
213
+ model.columns.each{|column| yield(column, column.name.to_sym)} if block_given?
214
+ end
215
+
216
+ def self.send_indices(indices)
217
+ if @generate_migration
218
+ require 'rails_generator'
219
+ migration_path = File.join(RAILS_ROOT, 'db', 'migrate')
220
+ migration_number = 1
221
+ migration_test = "add#{"_#{@migration_name}" if @migration_name}_indexes"
222
+ for file in Dir[File.join(migration_path, '*.rb')]
223
+ file = File.basename(file)
224
+ next unless file =~ /^\d+_#{migration_test}(\d+)\.rb$/
225
+ migration_number += 1
226
+ end
227
+ migration_name = "#{migration_test}#{migration_number}"
228
+ Rails::Generator::Base.instance('migration', [migration_name], {:command => :create, :generator => 'migration'}).command(:create).invoke!
229
+ if migration = Dir[File.join(migration_path, "*#{migration_name}.rb")].first
230
+ migration_up = []
231
+ migration_down = []
232
+ for table, columns in indices.sort{|a, b| a[0].to_s <=> b[0].to_s}
233
+ next if columns.empty?
234
+ migration_up << "\s\s\s\s# Indices for `#{table}`"
235
+ migration_down << "\s\s\s\s# Remove indices for `#{table}`"
236
+ for column in columns
237
+ migration_up << "\s\s\s\sadd_index :#{table}, #{column.inspect}"
238
+ migration_down << "\s\s\s\sremove_index :#{table}, #{column.inspect}"
239
+ end
240
+ end
241
+ migration_contents = File.read(migration).gsub("def self.up", "def self.up\n#{migration_up.join("\n")}").gsub("def self.down", "def self.down\n#{migration_down.join("\n")}")
242
+ File.open(migration, 'w+') do |file|
243
+ file.puts migration_contents
244
+ end
245
+ end
246
+ else
247
+ for table, columns in indices.sort{|a, b| a[0].to_s <=> b[0].to_s}
248
+ next if columns.empty?
249
+ puts "\s\s# Indices for `#{table}`"
250
+ for column in columns
251
+ if @perform_index
252
+ ActiveRecord::Migration.add_index(table, column)
253
+ else
254
+ puts "\s\sadd_index :#{table}, #{column.inspect}"
255
+ end
256
+ end
257
+ puts ''
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,51 @@
1
+ require 'findex/tasks'
2
+
3
+ describe Findex do
4
+ describe "indexes task" do
5
+ it "should exist" do
6
+ Rake::Task['db:indexes'].should_not be_nil
7
+ end
8
+
9
+ it "should default to listing all missing indices"
10
+ end
11
+
12
+ it "should create an indexes:boolean task" do
13
+ Rake::Task['db:indexes:boolean'].should_not be_nil
14
+ end
15
+
16
+ it "should create an indexes:datetime task" do
17
+ Rake::Task['db:indexes:datetime'].should_not be_nil
18
+ end
19
+
20
+ it "should create an indexes:geo task" do
21
+ Rake::Task['db:indexes:geo'].should_not be_nil
22
+ end
23
+
24
+ it "should create an indexes:help task" do
25
+ Rake::Task['db:indexes:help'].should_not be_nil
26
+ end
27
+
28
+ it "should create an indexes:migration task" do
29
+ Rake::Task['db:indexes:migration'].should_not be_nil
30
+ end
31
+
32
+ it "should create an indexes:names task" do
33
+ Rake::Task['db:indexes:names'].should_not be_nil
34
+ end
35
+
36
+ it "should create an indexes:perform task" do
37
+ Rake::Task['db:indexes:perform'].should_not be_nil
38
+ end
39
+
40
+ it "should create an indexes:primary task" do
41
+ Rake::Task['db:indexes:primary'].should_not be_nil
42
+ end
43
+
44
+ it "should create an indexes:relationships task" do
45
+ Rake::Task['db:indexes:relationships'].should_not be_nil
46
+ end
47
+
48
+ it "should create an indexes:types task" do
49
+ Rake::Task['db:indexes:types'].should_not be_nil
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: findex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Flip Sasser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-12 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: ""
17
+ email: flip@x451.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.markdown
24
+ files:
25
+ - MIT-LICENSE
26
+ - README.markdown
27
+ - Rakefile
28
+ - VERSION
29
+ - findex.gemspec
30
+ - lib/findex/tasks.rb
31
+ - spec/findex_tasks_spec.rb
32
+ has_rdoc: true
33
+ homepage: http://github.com/flipsasser/findex
34
+ licenses: []
35
+
36
+ post_install_message:
37
+ rdoc_options:
38
+ - --charset=UTF-8
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.3.5
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: Rake tasks to check your Rails models for missing indexes
60
+ test_files:
61
+ - spec/findex_tasks_spec.rb