schema_plus_indexes 0.1.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.
@@ -0,0 +1,17 @@
1
+ module SchemaPlusIndexes
2
+ module Middleware
3
+ module Model
4
+
5
+ def self.insert
6
+ SchemaMonkey::Middleware::Model::ResetColumnInformation.append ResetColumnInformation
7
+ end
8
+
9
+ class ResetColumnInformation < SchemaMonkey::Middleware::Base
10
+ def call(env)
11
+ continue env
12
+ env.model.reset_index_information
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ module SchemaPlusIndexes
2
+ module Middleware
3
+ module Sqlite3
4
+ def self.insert
5
+ SchemaMonkey::Middleware::Query::Indexes.append LookupExtensions
6
+ end
7
+
8
+ class LookupExtensions < SchemaMonkey::Middleware::Base
9
+ def call(env)
10
+ continue env
11
+ indexes = Hash[env.index_definitions.map{ |d| [d.name, d] }]
12
+
13
+ env.connection.exec_query("SELECT name, sql FROM sqlite_master WHERE type = 'index'").map do |row|
14
+ if (desc_columns = row['sql'].scan(/['"`]?(\w+)['"`]? DESC\b/).flatten).any?
15
+ index = indexes[row['name']]
16
+ index.orders = Hash[index.columns.map {|column| [column, desc_columns.include?(column) ? :desc : :asc]}]
17
+ end
18
+ end
19
+
20
+ env.index_definitions
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module SchemaPlusIndexes
2
+ VERSION = "0.1.0"
3
+ end
data/schema_dev.yml ADDED
@@ -0,0 +1,9 @@
1
+ ruby:
2
+ - 1.9.3
3
+ - 2.1.5
4
+ rails:
5
+ - 4.2
6
+ db:
7
+ - mysql2
8
+ - sqlite3
9
+ - postgresql
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'schema_plus_indexes/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "schema_plus_indexes"
8
+ spec.version = SchemaPlusIndexes::VERSION
9
+ spec.authors = ["ronen barzel"]
10
+ spec.email = ["ronen@barzel.org"]
11
+ spec.summary = %q{Adds shorthands and conveniences to ActiveRecord's handling of indexes}
12
+ spec.homepage = "https://github.com/SchemaPlus/schema_plus_indexes"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "activerecord", "~> 4.2"
21
+ spec.add_dependency "schema_monkey", "~> 0.2"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 3.0.0"
26
+ spec.add_development_dependency "schema_dev", "~> 2.0"
27
+ spec.add_development_dependency "simplecov"
28
+ spec.add_development_dependency "simplecov-gem-profile"
29
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe "Index definition" do
5
+
6
+ let(:migration) { ::ActiveRecord::Migration }
7
+
8
+ before(:all) do
9
+ define_schema(:auto_create => false) do
10
+ create_table :users, :force => true do |t|
11
+ t.string :login
12
+ t.datetime :deleted_at
13
+ end
14
+
15
+ create_table :posts, :force => true do |t|
16
+ t.text :body
17
+ t.integer :user_id
18
+ t.integer :author_id
19
+ end
20
+
21
+ end
22
+ class User < ::ActiveRecord::Base ; end
23
+ class Post < ::ActiveRecord::Base ; end
24
+ end
25
+
26
+ around(:each) do |example|
27
+ migration.suppress_messages do
28
+ example.run
29
+ end
30
+ end
31
+
32
+ after(:each) do
33
+ migration.remove_index :users, :name => 'users_login_index' if migration.index_name_exists? :users, 'users_login_index', true
34
+ end
35
+
36
+ context "when index is multicolumn" do
37
+ before(:each) do
38
+ migration.execute "CREATE INDEX users_login_index ON users (login, deleted_at)"
39
+ User.reset_column_information
40
+ @index = index_definition(%w[login deleted_at])
41
+ end
42
+
43
+ it "is included in User.indexes" do
44
+ expect(@index).not_to be_nil
45
+ end
46
+
47
+ end
48
+
49
+ it "should not crash on equality test with nil" do
50
+ index = ActiveRecord::ConnectionAdapters::IndexDefinition.new(:table, :column)
51
+ expect{index == nil}.to_not raise_error
52
+ expect(index == nil).to be false
53
+ end
54
+
55
+
56
+ context "when index is ordered", :mysql => :skip do
57
+
58
+ quotes = [
59
+ ["unquoted", ''],
60
+ ["double-quoted", '"'],
61
+ ]
62
+ quotes += [
63
+ ["single-quoted", "'"],
64
+ ["back-quoted", '`']
65
+ ] if SchemaDev::Rspec::Helpers.sqlite3?
66
+
67
+ quotes.each do |quotename, quote|
68
+ it "index definition includes orders for #{quotename} columns" do
69
+ migration.execute "CREATE INDEX users_login_index ON users (#{quote}login#{quote} DESC, #{quote}deleted_at#{quote} ASC)"
70
+ User.reset_column_information
71
+ index = index_definition(%w[login deleted_at])
72
+ expect(index.orders).to eq({"login" => :desc, "deleted_at" => :asc})
73
+ end
74
+
75
+ end
76
+ end
77
+
78
+ context "when index is partial" do
79
+ before(:each) do
80
+ migration.execute "CREATE INDEX users_login_index ON users(login) WHERE deleted_at IS NULL"
81
+ User.reset_column_information
82
+ @index = index_definition("login")
83
+ end
84
+
85
+ it "is included in User.indexes" do
86
+ expect(User.indexes.select { |index| index.columns == ["login"] }.size).to eq(1)
87
+ end
88
+
89
+ it "defines where" do
90
+ expect(@index.where).to match %r{[(]?deleted_at IS NULL[)]?}
91
+ end
92
+
93
+ end if ::ActiveRecord::Migration.supports_partial_index?
94
+
95
+
96
+ protected
97
+ def index_definition(column_names)
98
+ User.indexes.detect { |index| index.columns == Array(column_names) }
99
+ end
100
+
101
+
102
+ end
@@ -0,0 +1,180 @@
1
+ require 'spec_helper'
2
+
3
+ describe "index" do
4
+
5
+ let(:migration) { ::ActiveRecord::Migration }
6
+ let(:connection) { ::ActiveRecord::Base.connection }
7
+
8
+ describe "add_index" do
9
+
10
+ before(:each) do
11
+ connection.tables.each do |table| connection.drop_table table, cascade: true end
12
+
13
+ define_schema(:auto_create => false) do
14
+ create_table :users, :force => true do |t|
15
+ t.string :login
16
+ t.text :address
17
+ t.datetime :deleted_at
18
+ end
19
+
20
+ create_table :posts, :force => true do |t|
21
+ t.text :body
22
+ t.integer :user_id
23
+ t.integer :author_id
24
+ end
25
+
26
+ end
27
+ class User < ::ActiveRecord::Base ; end
28
+ class Post < ::ActiveRecord::Base ; end
29
+ end
30
+
31
+
32
+ after(:each) do
33
+ migration.suppress_messages do
34
+ migration.remove_index(:users, :name => @index.name) if (@index ||= nil)
35
+ end
36
+ end
37
+
38
+ it "should create index when called without additional options" do
39
+ add_index(:users, :login)
40
+ expect(index_for(:login)).not_to be_nil
41
+ end
42
+
43
+ it "should create unique index" do
44
+ add_index(:users, :login, :unique => true)
45
+ expect(index_for(:login).unique).to eq(true)
46
+ end
47
+
48
+ it "should assign given name" do
49
+ add_index(:users, :login, :name => 'users_login_index')
50
+ expect(index_for(:login).name).to eq('users_login_index')
51
+ end
52
+
53
+ it "should assign order", :mysql => :skip do
54
+ add_index(:users, [:login, :deleted_at], :order => {:login => :desc, :deleted_at => :asc})
55
+ expect(index_for([:login, :deleted_at]).orders).to eq({"login" => :desc, "deleted_at" => :asc})
56
+ end
57
+
58
+ context "for duplicate index" do
59
+ it "should not complain if the index is the same" do
60
+ add_index(:users, :login)
61
+ expect(index_for(:login)).not_to be_nil
62
+ expect(ActiveRecord::Base.logger).to receive(:warn).with(/login.*Skipping/)
63
+ expect { add_index(:users, :login) }.to_not raise_error
64
+ expect(index_for(:login)).not_to be_nil
65
+ end
66
+ it "should complain if the index is different" do
67
+ add_index(:users, :login, :unique => true)
68
+ expect(index_for(:login)).not_to be_nil
69
+ expect { add_index(:users, :login) }.to raise_error
70
+ expect(index_for(:login)).not_to be_nil
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def index_for(column_names)
77
+ @index = User.indexes.detect { |i| i.columns == Array(column_names).collect(&:to_s) }
78
+ end
79
+
80
+ end
81
+
82
+ describe "remove_index" do
83
+
84
+ before(:each) do
85
+ connection.tables.each do |table| connection.drop_table table, cascade: true end
86
+ define_schema(:auto_create => false) do
87
+ create_table :users, :force => true do |t|
88
+ t.string :login
89
+ t.datetime :deleted_at
90
+ end
91
+ end
92
+ class User < ::ActiveRecord::Base ; end
93
+ end
94
+
95
+
96
+ it "removes index by column name (symbols)" do
97
+ add_index :users, :login
98
+ expect(User.indexes.length).to eq(1)
99
+ remove_index :users, :login
100
+ expect(User.indexes.length).to eq(0)
101
+ end
102
+
103
+ it "removes index by column name (symbols)" do
104
+ add_index :users, :login
105
+ expect(User.indexes.length).to eq(1)
106
+ remove_index 'users', 'login'
107
+ expect(User.indexes.length).to eq(0)
108
+ end
109
+
110
+ it "removes multi-column index by column names (symbols)" do
111
+ add_index :users, [:login, :deleted_at]
112
+ expect(User.indexes.length).to eq(1)
113
+ remove_index :users, [:login, :deleted_at]
114
+ expect(User.indexes.length).to eq(0)
115
+ end
116
+
117
+ it "removes multi-column index by column names (strings)" do
118
+ add_index 'users', [:login, :deleted_at]
119
+ expect(User.indexes.length).to eq(1)
120
+ remove_index 'users', ['login', 'deleted_at']
121
+ expect(User.indexes.length).to eq(0)
122
+ end
123
+
124
+ it "removes index using column option" do
125
+ add_index :users, :login
126
+ expect(User.indexes.length).to eq(1)
127
+ remove_index :users, column: :login
128
+ expect(User.indexes.length).to eq(0)
129
+ end
130
+
131
+ it "removes index if_exists" do
132
+ add_index :users, :login
133
+ expect(User.indexes.length).to eq(1)
134
+ remove_index :users, :login, :if_exists => true
135
+ expect(User.indexes.length).to eq(0)
136
+ end
137
+
138
+ it "removes multi-column index if exists" do
139
+ add_index :users, [:login, :deleted_at]
140
+ expect(User.indexes.length).to eq(1)
141
+ remove_index :users, [:login, :deleted_at], :if_exists => true
142
+ expect(User.indexes.length).to eq(0)
143
+ end
144
+
145
+ it "removes index if_exists using column option" do
146
+ add_index :users, :login
147
+ expect(User.indexes.length).to eq(1)
148
+ remove_index :users, column: :login, :if_exists => true
149
+ expect(User.indexes.length).to eq(0)
150
+ end
151
+
152
+ it "raises exception if doesn't exist" do
153
+ expect {
154
+ remove_index :users, :login
155
+ }.to raise_error
156
+ end
157
+
158
+ it "doesn't raise exception with :if_exists" do
159
+ expect {
160
+ remove_index :users, :login, :if_exists => true
161
+ }.to_not raise_error
162
+ end
163
+ end
164
+
165
+ protected
166
+ def add_index(*args)
167
+ migration.suppress_messages do
168
+ migration.add_index(*args)
169
+ end
170
+ User.reset_column_information
171
+ end
172
+
173
+ def remove_index(*args)
174
+ migration.suppress_messages do
175
+ migration.remove_index(*args)
176
+ end
177
+ User.reset_column_information
178
+ end
179
+
180
+ end
@@ -0,0 +1,188 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe ActiveRecord::Migration do
5
+
6
+ before(:each) do
7
+ define_schema(:auto_create => true) do
8
+
9
+ create_table :users, :force => true do |t|
10
+ t.string :login, :index => { :unique => true }
11
+ end
12
+
13
+ create_table :members, :force => true do |t|
14
+ t.string :login
15
+ end
16
+
17
+ create_table :comments, :force => true do |t|
18
+ t.string :content
19
+ t.integer :user
20
+ t.integer :user_id
21
+ end
22
+
23
+ create_table :posts, :force => true do |t|
24
+ t.string :content
25
+ end
26
+ end
27
+ class User < ::ActiveRecord::Base ; end
28
+ class Post < ::ActiveRecord::Base ; end
29
+ class Comment < ::ActiveRecord::Base ; end
30
+ end
31
+
32
+ context "when table is created" do
33
+
34
+ before(:each) do
35
+ @model = Post
36
+ end
37
+
38
+ it "should create an index if specified on column" do
39
+ recreate_table(@model) do |t|
40
+ t.integer :state, :index => true
41
+ end
42
+ expect(@model).to have_index.on(:state)
43
+ end
44
+
45
+ it "should create a unique index if specified on column" do
46
+ recreate_table(@model) do |t|
47
+ t.integer :state, :index => { :unique => true }
48
+ end
49
+ expect(@model).to have_unique_index.on(:state)
50
+ end
51
+
52
+ it "should create a unique index if specified on column using shorthand" do
53
+ recreate_table(@model) do |t|
54
+ t.integer :state, :index => :unique
55
+ end
56
+ expect(@model).to have_unique_index.on(:state)
57
+ end
58
+
59
+ it "should pass index length option properly", :mysql => :only do
60
+ recreate_table(@model) do |t|
61
+ t.string :foo
62
+ t.string :bar, :index => { :with => :foo, :length => { :foo => 8, :bar => 12 }}
63
+ end
64
+ index = @model.indexes.first
65
+ expect(Hash[index.columns.zip(index.lengths.map(&:to_i))]).to eq({ "foo" => 8, "bar" => 12})
66
+ end
67
+
68
+ it "should create an index if specified explicitly" do
69
+ recreate_table(@model) do |t|
70
+ t.integer :state
71
+ t.index :state
72
+ end
73
+ expect(@model).to have_index.on(:state)
74
+ end
75
+
76
+ it "should create a unique index if specified explicitly" do
77
+ recreate_table(@model) do |t|
78
+ t.integer :state
79
+ t.index :state, :unique => true
80
+ end
81
+ expect(@model).to have_unique_index.on(:state)
82
+ end
83
+
84
+ it "should create a multiple-column index if specified" do
85
+ recreate_table(@model) do |t|
86
+ t.integer :city
87
+ t.integer :state, :index => { :with => :city }
88
+ end
89
+ expect(@model).to have_index.on([:state, :city])
90
+ end
91
+
92
+ it "should create the index without modifying the input hash" do
93
+ hash = { :with => :foo, :length => { :foo => 8, :bar => 12 }}
94
+ hash_original = hash.dup
95
+ recreate_table(@model) do |t|
96
+ t.string :foo
97
+ t.string :bar, :index => hash
98
+ end
99
+ expect(hash).to eq(hash_original)
100
+ end
101
+
102
+ end
103
+
104
+ context "when table is changed" do
105
+ before(:each) do
106
+ @model = Post
107
+ end
108
+ [false, true].each do |bulk|
109
+ suffix = bulk ? ' with :bulk option' : ""
110
+
111
+ it "should create an index if specified on column"+suffix do
112
+ change_table(@model, :bulk => bulk) do |t|
113
+ t.integer :state, :index => true
114
+ end
115
+ expect(@model).to have_index.on(:state)
116
+ end
117
+
118
+ end
119
+ end
120
+
121
+ context "when column is added", :sqlite3 => :skip do
122
+
123
+ before(:each) do
124
+ @model = Comment
125
+ end
126
+
127
+ it "should create an index" do
128
+ add_column(:slug, :string, :index => true) do
129
+ expect(@model).to have_index.on(:slug)
130
+ end
131
+ end
132
+
133
+ it "should create an index if specified" do
134
+ add_column(:post_id, :integer, :index => true) do
135
+ expect(@model).to have_index.on(:post_id)
136
+ end
137
+ end
138
+
139
+ it "should create a unique index if specified" do
140
+ add_column(:post_id, :integer, :index => { :unique => true }) do
141
+ expect(@model).to have_unique_index.on(:post_id)
142
+ end
143
+ end
144
+
145
+ it "should create a unique index if specified by shorthand" do
146
+ add_column(:post_id, :integer, :index => :unique) do
147
+ expect(@model).to have_unique_index.on(:post_id)
148
+ end
149
+ end
150
+
151
+ it "should allow custom name for index" do
152
+ index_name = 'comments_post_id_unique_index'
153
+ add_column(:post_id, :integer, :index => { :unique => true, :name => index_name }) do
154
+ expect(@model).to have_unique_index(:name => index_name).on(:post_id)
155
+ end
156
+ end
157
+
158
+ protected
159
+
160
+ def add_column(column_name, *args)
161
+ table = @model.table_name
162
+ ActiveRecord::Migration.suppress_messages do
163
+ ActiveRecord::Migration.add_column(table, column_name, *args)
164
+ @model.reset_column_information
165
+ yield if block_given?
166
+ ActiveRecord::Migration.remove_column(table, column_name)
167
+ end
168
+ end
169
+
170
+ end
171
+
172
+
173
+ def recreate_table(model, opts={}, &block)
174
+ ActiveRecord::Migration.suppress_messages do
175
+ ActiveRecord::Migration.create_table model.table_name, opts.merge(:force => true), &block
176
+ end
177
+ model.reset_column_information
178
+ end
179
+
180
+ def change_table(model, opts={}, &block)
181
+ ActiveRecord::Migration.suppress_messages do
182
+ ActiveRecord::Migration.change_table model.table_name, opts, &block
183
+ end
184
+ model.reset_column_information
185
+ end
186
+
187
+ end
188
+