schemy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in schemy.gemspec
4
+ gemspec
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.
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,7 @@
1
+ require "schemy/version"
2
+ require 'schemy/railtie' if defined?(Rails)
3
+ require 'schemy/analyzer'
4
+
5
+ module Schemy
6
+ # Your code goes here...
7
+ end
@@ -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,11 @@
1
+ class FakeColumn
2
+ attr_reader :name
3
+
4
+ def initialize(name, type)
5
+ @name = name
6
+ @type = type
7
+ end
8
+ def to_s
9
+ "#{@name}:#{@type}"
10
+ end
11
+ 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,7 @@
1
+ module Schemy
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load "tasks/schemy.rake"
5
+ end
6
+ end
7
+ 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,10 @@
1
+ require_relative 'base_validator'
2
+
3
+ class AssociationIdIdentifierValidator < BaseValidator
4
+
5
+ def validate_column column
6
+ if column == 'association_id' && @table.has_column?('identifier')
7
+ require_index ['association_id', 'identifier']
8
+ end
9
+ end
10
+ end
@@ -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,10 @@
1
+ require_relative 'base_validator'
2
+
3
+ class IdentifierValidator < BaseValidator
4
+
5
+ def validate_column column
6
+ if column == 'identifier'
7
+ require_index column
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'base_validator'
2
+
3
+ class TypeValidator < BaseValidator
4
+
5
+ def validate_column column
6
+ if column == 'type'
7
+ require_index column
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Schemy
2
+ VERSION = "0.0.1"
3
+ 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
+
@@ -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: []