schemy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/lib/schemy.rb +7 -0
- data/lib/schemy/analyzer.rb +79 -0
- data/lib/schemy/fake_column.rb +11 -0
- data/lib/schemy/fake_index.rb +45 -0
- data/lib/schemy/fake_table.rb +36 -0
- data/lib/schemy/railtie.rb +7 -0
- data/lib/schemy/schema_checker.rb +56 -0
- data/lib/schemy/validators/association_id_identifier_validator.rb +10 -0
- data/lib/schemy/validators/base_validator.rb +33 -0
- data/lib/schemy/validators/foreign_key_validator.rb +15 -0
- data/lib/schemy/validators/identifier_validator.rb +10 -0
- data/lib/schemy/validators/type_validator.rb +10 -0
- data/lib/schemy/version.rb +3 -0
- data/lib/tasks/schemy.rake +14 -0
- data/schemy.gemspec +17 -0
- metadata +66 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Ian C. Anderson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Schemy
|
2
|
+
|
3
|
+
Schemy analyzes schema.rb to suggest new database indexes, providing a migration file that can be used directly.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'schemy'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install schemy
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/schemy.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
class Schemy::Analyzer
|
2
|
+
|
3
|
+
def initialize(schema_path)
|
4
|
+
@schema_path = schema_path
|
5
|
+
end
|
6
|
+
|
7
|
+
def create_migration
|
8
|
+
File.open(migration_file_path, 'w') do |f|
|
9
|
+
f.write(migration_string)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def migration_file_path
|
14
|
+
Rails.root.join 'db', 'migrate', migration_file_base_name + '.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
def new_indices_count
|
18
|
+
new_indices.count
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def analyze
|
24
|
+
# load in our implementation of ActiveRecord::Schema
|
25
|
+
require_relative 'schema_checker'
|
26
|
+
# evaluate the schema with our Schema class
|
27
|
+
@results = eval(schema)
|
28
|
+
end
|
29
|
+
|
30
|
+
def migration_class_name
|
31
|
+
"AddIndices#{migration_timestamp}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def migration_file_base_name
|
35
|
+
@migration_file_base_name ||= "#{migration_timestamp}_add_indices_#{migration_timestamp}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def migration_string
|
39
|
+
if new_indices.any?
|
40
|
+
|
41
|
+
<<MIGRATION
|
42
|
+
class #{migration_class_name} < ActiveRecord::Migration
|
43
|
+
|
44
|
+
def self.up
|
45
|
+
#{new_indices.map{|i| " #{i.to_s}" }.join "\n"}
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.down
|
49
|
+
#{new_indices.map{|i| " #{i.to_s :down}" }.join "\n"}
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
MIGRATION
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def migration_timestamp
|
59
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
60
|
+
end
|
61
|
+
|
62
|
+
def new_indices
|
63
|
+
results.new_indices
|
64
|
+
end
|
65
|
+
|
66
|
+
def results
|
67
|
+
return @results if @results
|
68
|
+
analyze
|
69
|
+
@results
|
70
|
+
end
|
71
|
+
|
72
|
+
def schema
|
73
|
+
file = File.open(@schema_path, "r")
|
74
|
+
contents = file.read
|
75
|
+
file.close
|
76
|
+
contents
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
class FakeIndex
|
4
|
+
MAX_INDEX_NAME_LENGTH = 63
|
5
|
+
|
6
|
+
def initialize(table, column_names, options = {})
|
7
|
+
@column_names = column_names
|
8
|
+
@table = table
|
9
|
+
end
|
10
|
+
|
11
|
+
def match_columns? columns
|
12
|
+
columns.map(&:to_s).sort == @column_names.map(&:to_s).sort
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s(direction = :up)
|
16
|
+
if direction == :up
|
17
|
+
%[add_index '#{table_name}', [#{column_names_quoted}], :name => "#{index_name}"]
|
18
|
+
elsif direction == :down
|
19
|
+
%[remove_index '#{table_name}', :name => "#{index_name}"]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def table_name
|
24
|
+
@table.name
|
25
|
+
end
|
26
|
+
|
27
|
+
def column_names_quoted
|
28
|
+
@column_names.map do |c|
|
29
|
+
"'#{c}'"
|
30
|
+
end.join ','
|
31
|
+
end
|
32
|
+
def column_names_anded
|
33
|
+
@column_names.join '_and_'
|
34
|
+
end
|
35
|
+
def index_name
|
36
|
+
# Postgres only supports index names up to 63 chars
|
37
|
+
name = "index_#{table_name}_on_#{column_names_anded}"
|
38
|
+
if name.length > MAX_INDEX_NAME_LENGTH
|
39
|
+
md5 = Digest::MD5.hexdigest(name)
|
40
|
+
name = "index_#{table_name}_#{md5[0...10]}"
|
41
|
+
name = "index_#{md5[0...20]}" if name.length > MAX_INDEX_NAME_LENGTH
|
42
|
+
end
|
43
|
+
name
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'fake_index'
|
2
|
+
|
3
|
+
class FakeTable
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :columns
|
6
|
+
|
7
|
+
def initialize(table_name)
|
8
|
+
@name = table_name
|
9
|
+
@columns = []
|
10
|
+
@indices = []
|
11
|
+
end
|
12
|
+
def add_index(column_name, options = {})
|
13
|
+
@indices << FakeIndex.new(self, Array(column_name), options)
|
14
|
+
end
|
15
|
+
def method_missing(m, *args, &block)
|
16
|
+
data_types = [:string, :text, :integer, :float, :decimal, :datetime,
|
17
|
+
:timestamp, :time, :date, :binary, :boolean]
|
18
|
+
if data_types.include? m
|
19
|
+
# method names are column data types
|
20
|
+
@columns << FakeColumn.new(args[0], m)
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
def to_s
|
26
|
+
output = "=" * 30
|
27
|
+
output += "\n#{table_name}:\n"
|
28
|
+
output += @columns.inject(''){ |output, column| output += "#{column}\n" }
|
29
|
+
end
|
30
|
+
def has_index? columns
|
31
|
+
@indices.detect{ |index| index.match_columns? columns }
|
32
|
+
end
|
33
|
+
def has_column? column_name
|
34
|
+
@columns.detect{ |col| col.name.to_sym == column_name.to_sym }
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
Dir[File.dirname(__FILE__) + '/validators/*.rb'].each do |file|
|
2
|
+
require_relative File.join "validators", File.basename(file)[0...-3]
|
3
|
+
end
|
4
|
+
|
5
|
+
require_relative 'fake_table'
|
6
|
+
require_relative 'fake_column'
|
7
|
+
|
8
|
+
module ActiveRecord
|
9
|
+
class Schema
|
10
|
+
|
11
|
+
DEFAULT_VALIDATORS = [AssociationIdIdentifierValidator, ForeignKeyValidator,
|
12
|
+
IdentifierValidator, TypeValidator]
|
13
|
+
|
14
|
+
def self.define(options= {}, &block)
|
15
|
+
schema = new
|
16
|
+
schema.instance_eval &block
|
17
|
+
schema.schemy_results
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@tables = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_table(table_name, options = {}, &block)
|
25
|
+
@tables << FakeTable.new(table_name)
|
26
|
+
yield @tables.last
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_index(table_name, column_name, options = {})
|
30
|
+
get_table_by_name(table_name).add_index(column_name, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
@tables.inject(''){ |output, table| output += "#{table}\n" }
|
35
|
+
end
|
36
|
+
|
37
|
+
def schemy_results
|
38
|
+
new_indices = []
|
39
|
+
@tables.each do |table|
|
40
|
+
DEFAULT_VALIDATORS.each do |validator_klass|
|
41
|
+
new_indices += validator_klass.new(table).validate
|
42
|
+
end
|
43
|
+
end
|
44
|
+
Struct.new(:new_indices).new(new_indices)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def get_table_by_name(table_name)
|
50
|
+
@tables.detect{ |table| table.name == table_name }
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class BaseValidator
|
2
|
+
|
3
|
+
attr_reader :table
|
4
|
+
|
5
|
+
def initialize(table)
|
6
|
+
@table = table
|
7
|
+
end
|
8
|
+
|
9
|
+
# returns array of unindexed columns which should be indexed
|
10
|
+
def validate
|
11
|
+
@missing_indices = []
|
12
|
+
@table.columns.each do |column|
|
13
|
+
validate_column column.name.to_s
|
14
|
+
end
|
15
|
+
@missing_indices
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
def validate_column(column)
|
20
|
+
end
|
21
|
+
|
22
|
+
def require_index(columns)
|
23
|
+
columns = Array columns
|
24
|
+
# will add to @missing_indices if no index is present for the columns
|
25
|
+
unless @table.has_index? columns
|
26
|
+
@missing_indices << index_from_columns(columns)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def index_from_columns columns
|
31
|
+
FakeIndex.new @table, columns
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'base_validator'
|
2
|
+
|
3
|
+
class ForeignKeyValidator < BaseValidator
|
4
|
+
|
5
|
+
def validate_column column
|
6
|
+
if column =~ /_id$/
|
7
|
+
type_column = column[0...-2] + 'type'
|
8
|
+
if table.has_column? type_column
|
9
|
+
require_index [type_column, column]
|
10
|
+
else
|
11
|
+
require_index column
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
namespace :schemy do
|
2
|
+
|
3
|
+
desc 'Analyze schema.rb for indexing opportunities.'
|
4
|
+
task indexes: :environment do
|
5
|
+
analyzer = Schemy::Analyzer.new(Rails.root.join('db', 'schema.rb'))
|
6
|
+
analyzer.create_migration
|
7
|
+
|
8
|
+
puts "Schemy here."
|
9
|
+
puts "It looks like you need #{analyzer.new_indices_count} new indices."
|
10
|
+
puts "I created a migration for you to add new indices: #{analyzer.migration_file_path}"
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
data/schemy.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/schemy/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Ian C. Anderson"]
|
6
|
+
gem.email = ["anderson.ian.c@gmail.com"]
|
7
|
+
gem.description = %q{Analyzes schema.rb to suggest new database indexes, providing a migration file that can be used directly.}
|
8
|
+
gem.summary = %q{Analyzes schema.rb to suggest new database indexes.}
|
9
|
+
gem.homepage = 'http://rubygems.org/gems/schemy'
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "schemy"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Schemy::VERSION
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: schemy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ian C. Anderson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-15 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Analyzes schema.rb to suggest new database indexes, providing a migration
|
15
|
+
file that can be used directly.
|
16
|
+
email:
|
17
|
+
- anderson.ian.c@gmail.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .gitignore
|
23
|
+
- Gemfile
|
24
|
+
- LICENSE
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- lib/schemy.rb
|
28
|
+
- lib/schemy/analyzer.rb
|
29
|
+
- lib/schemy/fake_column.rb
|
30
|
+
- lib/schemy/fake_index.rb
|
31
|
+
- lib/schemy/fake_table.rb
|
32
|
+
- lib/schemy/railtie.rb
|
33
|
+
- lib/schemy/schema_checker.rb
|
34
|
+
- lib/schemy/validators/association_id_identifier_validator.rb
|
35
|
+
- lib/schemy/validators/base_validator.rb
|
36
|
+
- lib/schemy/validators/foreign_key_validator.rb
|
37
|
+
- lib/schemy/validators/identifier_validator.rb
|
38
|
+
- lib/schemy/validators/type_validator.rb
|
39
|
+
- lib/schemy/version.rb
|
40
|
+
- lib/tasks/schemy.rake
|
41
|
+
- schemy.gemspec
|
42
|
+
homepage: http://rubygems.org/gems/schemy
|
43
|
+
licenses: []
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.8.24
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: Analyzes schema.rb to suggest new database indexes.
|
66
|
+
test_files: []
|