schema_plus_pg_indexes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module SchemaPlusPgIndexes
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,7 @@
1
+ ruby:
2
+ - 1.9.3
3
+ - 2.1.5
4
+ rails:
5
+ - 4.2
6
+ db:
7
+ - postgresql
@@ -0,0 +1,30 @@
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_pg_indexes/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "schema_plus_pg_indexes"
8
+ spec.version = SchemaPlusPgIndexes::VERSION
9
+ spec.authors = ["ronen barzel"]
10
+ spec.email = ["ronen@barzel.org"]
11
+ spec.summary = %q{Adds support in ActiveRecord for PostgreSQL index expressions and operator classes, as well as a shorthand for case-insensitive indexes}
12
+ spec.homepage = "https://github.com/SchemaPlus/schema_plus_pg_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.3"
22
+ spec.add_dependency "schema_plus_indexes", "~> 0.1"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.7"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0.0"
27
+ spec.add_development_dependency "schema_dev", "~> 2.0"
28
+ spec.add_development_dependency "simplecov"
29
+ spec.add_development_dependency "simplecov-gem-profile"
30
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Deprecations' do
4
+
5
+ before(:all) do
6
+ class User < ::ActiveRecord::Base ; end
7
+ end
8
+
9
+ let(:migration) { ::ActiveRecord::Migration }
10
+
11
+ context "on table creation" do
12
+ it "deprecates :conditions" do
13
+ where = "((login)::text ~~ '%xyz'::text)"
14
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/conditions.*where/)
15
+ create_table User, :login => { index: { conditions: where } }
16
+ index = User.indexes.first
17
+ expect(index.where).to eq where
18
+ end
19
+
20
+ it "deprecates :kind" do
21
+ using = :hash
22
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/kind.*using/)
23
+ create_table User, :login => { index: { kind: using } }
24
+ index = User.indexes.first
25
+ expect(index.using).to eq using
26
+ end
27
+ end
28
+
29
+ context "on IndexDefinition object" do
30
+
31
+ it "deprecates #conditions" do
32
+ where = "((login)::text ~~ '%xyz'::text)"
33
+ create_table User, :login => { index: { where: where } }
34
+ index = User.indexes.first
35
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/conditions.*where/)
36
+ expect(index.where).to eq where # sanity check
37
+ expect(index.conditions).to eq index.where
38
+ end
39
+
40
+ it "deprecates #kind" do
41
+ using = :hash
42
+ create_table User, :login => { index: { using: using } }
43
+ index = User.indexes.first
44
+ expect(ActiveSupport::Deprecation).to receive(:warn).with(/kind.*using/)
45
+ expect(index.using).to eq using # sanity check
46
+ expect(index.kind).to eq index.using
47
+ end
48
+ end
49
+
50
+ protected
51
+
52
+ def create_table(model, columns_with_options)
53
+ migration.suppress_messages do
54
+ migration.create_table model.table_name, :force => true do |t|
55
+ columns_with_options.each_pair do |column, options|
56
+ t.send :string, column, options
57
+ end
58
+ end
59
+ model.reset_column_information
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,135 @@
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 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 case insensitive is added" do
37
+
38
+ before(:each) do
39
+ migration.execute "CREATE INDEX users_login_index ON users(LOWER(login))"
40
+ User.reset_column_information
41
+ @index = User.indexes.detect { |i| i.expression =~ /lower\(\(login\)::text\)/i }
42
+ end
43
+
44
+ it "is included in User.indexes" do
45
+ expect(@index).not_to be_nil
46
+ end
47
+
48
+ it "is not case_sensitive" do
49
+ expect(@index).not_to be_case_sensitive
50
+ end
51
+
52
+ it "defines expression" do
53
+ expect(@index.expression).to eq("lower((login)::text)")
54
+ end
55
+
56
+ it "doesn't define where" do
57
+ expect(@index.where).to be_nil
58
+ end
59
+
60
+ end
61
+
62
+
63
+ context "when index contains expression" do
64
+ before(:each) do
65
+ migration.execute "CREATE INDEX users_login_index ON users (extract(EPOCH from deleted_at)) WHERE deleted_at IS NULL"
66
+ User.reset_column_information
67
+ @index = User.indexes.detect { |i| i.expression.present? }
68
+ end
69
+
70
+ it "exists" do
71
+ expect(@index).not_to be_nil
72
+ end
73
+
74
+ it "doesnt have columns defined" do
75
+ expect(@index.columns).to be_empty
76
+ end
77
+
78
+ it "is case_sensitive" do
79
+ expect(@index).to be_case_sensitive
80
+ end
81
+
82
+ it "defines expression" do
83
+ expect(@index.expression).to eq("date_part('epoch'::text, deleted_at)")
84
+ end
85
+
86
+ it "defines where" do
87
+ expect(@index.where).to eq("(deleted_at IS NULL)")
88
+ end
89
+
90
+ end
91
+
92
+ context "when index has a non-btree type" do
93
+ before(:each) do
94
+ migration.execute "CREATE INDEX users_login_index ON users USING hash(login)"
95
+ User.reset_column_information
96
+ @index = User.indexes.detect { |i| i.name == "users_login_index" }
97
+ end
98
+
99
+ it "exists" do
100
+ expect(@index).not_to be_nil
101
+ end
102
+
103
+ it "defines using" do
104
+ expect(@index.using).to eq(:hash)
105
+ end
106
+
107
+ it "does not define expression" do
108
+ expect(@index.expression).to be_nil
109
+ end
110
+
111
+ it "does not define order" do
112
+ expect(@index.orders).to be_blank
113
+ end
114
+ end
115
+
116
+ context "equality" do
117
+
118
+ it "returns true when case sensitivity are the same" do
119
+ expect(ActiveRecord::ConnectionAdapters::IndexDefinition.new("table", "column", case_sensitive: true)).to eq ActiveRecord::ConnectionAdapters::IndexDefinition.new("table", "column", case_sensitive: true)
120
+ end
121
+
122
+ it "returns false when case sensitivity are the different" do
123
+ expect(ActiveRecord::ConnectionAdapters::IndexDefinition.new("table", "column", case_sensitive: true)).not_to eq ActiveRecord::ConnectionAdapters::IndexDefinition.new("table", "column", case_sensitive: false)
124
+ end
125
+
126
+ end
127
+
128
+
129
+ protected
130
+ def index_definition(column_names)
131
+ User.indexes.detect { |index| index.columns == Array(column_names) }
132
+ end
133
+
134
+
135
+ end
@@ -0,0 +1,121 @@
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 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
+ context "extra features" do
39
+
40
+ it "should assign expression, where and using" do
41
+ add_index(:users, :expression => "USING hash (upper(login)) WHERE deleted_at IS NULL", :name => 'users_login_index')
42
+ @index = User.indexes.detect { |i| i.expression.present? }
43
+ expect(@index.expression).to eq("upper((login)::text)")
44
+ expect(@index.where).to eq("(deleted_at IS NULL)")
45
+ expect(@index.using).to eq(:hash)
46
+ end
47
+
48
+ it "should allow to specify expression, where and using separately" do
49
+ add_index(:users, :using => "hash", :expression => "upper(login)", :where => "deleted_at IS NULL", :name => 'users_login_index')
50
+ @index = User.indexes.detect { |i| i.expression.present? }
51
+ expect(@index.expression).to eq("upper((login)::text)")
52
+ expect(@index.where).to eq("(deleted_at IS NULL)")
53
+ expect(@index.using).to eq(:hash)
54
+ end
55
+
56
+ it "should assign operator_class" do
57
+ add_index(:users, :login, :operator_class => 'varchar_pattern_ops')
58
+ expect(index_for(:login).operator_classes).to eq({"login" => 'varchar_pattern_ops'})
59
+ end
60
+
61
+ it "should assign multiple operator_classes" do
62
+ add_index(:users, [:login, :address], :operator_class => {:login => 'varchar_pattern_ops', :address => 'text_pattern_ops'})
63
+ expect(index_for([:login, :address]).operator_classes).to eq({"login" => 'varchar_pattern_ops', "address" => 'text_pattern_ops'})
64
+ end
65
+
66
+ it "should allow to specify actual expression only" do
67
+ add_index(:users, :expression => "upper(login)", :name => 'users_login_index')
68
+ @index = User.indexes.detect { |i| i.name == 'users_login_index' }
69
+ expect(@index.expression).to eq("upper((login)::text)")
70
+ end
71
+
72
+ it "should raise if no column given and expression is missing" do
73
+ expect { add_index(:users, :name => 'users_login_index') }.to raise_error(ArgumentError, /expression/)
74
+ end
75
+
76
+ it "should raise if expression without name is given" do
77
+ expect { add_index(:users, :expression => "upper(login)") }.to raise_error(ArgumentError, /name/)
78
+ end
79
+
80
+ it "should raise if expression is given and case_sensitive is false" do
81
+ expect { add_index(:users, :name => 'users_login_index', :expression => "upper(login)", :case_sensitive => false) }.to raise_error(ArgumentError, /use LOWER/i)
82
+ end
83
+
84
+ end
85
+
86
+ context "create table" do
87
+ it "defines index with expression only" do
88
+ define_schema do
89
+ create_table :users, :force => true do |t|
90
+ t.string :login
91
+ t.index :expression => "upper(login)", name: "no_column"
92
+ end
93
+ end
94
+ expect(User.indexes.first.expression).to eq("upper((login)::text)")
95
+ end
96
+ end
97
+
98
+ protected
99
+
100
+ def index_for(column_names)
101
+ @index = User.indexes.detect { |i| i.columns == Array(column_names).collect(&:to_s) }
102
+ end
103
+
104
+ end
105
+
106
+ protected
107
+ def add_index(*args)
108
+ migration.suppress_messages do
109
+ migration.add_index(*args)
110
+ end
111
+ User.reset_column_information
112
+ end
113
+
114
+ def remove_index(*args)
115
+ migration.suppress_messages do
116
+ migration.remove_index(*args)
117
+ end
118
+ User.reset_column_information
119
+ end
120
+
121
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe "with multiple schemas" do
4
+ def connection
5
+ ActiveRecord::Base.connection
6
+ end
7
+
8
+ before(:all) do
9
+ newdb = case connection.adapter_name
10
+ when /^mysql/i then "CREATE SCHEMA IF NOT EXISTS schema_plus_pg_indexes_test2"
11
+ when /^postgresql/i then "CREATE SCHEMA schema_plus_pg_indexes_test2"
12
+ when /^sqlite/i then "ATTACH ':memory:' AS schema_plus_pg_indexes_test2"
13
+ end
14
+ begin
15
+ ActiveRecord::Base.connection.execute newdb
16
+ rescue ActiveRecord::StatementInvalid => e
17
+ raise unless e.message =~ /already exists/
18
+ end
19
+
20
+ class User < ::ActiveRecord::Base ; end
21
+ end
22
+
23
+ before(:each) do
24
+ define_schema do
25
+ create_table :users, :force => true do |t|
26
+ t.string :login
27
+ end
28
+ end
29
+
30
+ connection.execute 'DROP TABLE IF EXISTS schema_plus_pg_indexes_test2.users'
31
+ connection.execute 'CREATE TABLE schema_plus_pg_indexes_test2.users (id ' + case connection.adapter_name
32
+ when /^mysql/i then "integer primary key auto_increment"
33
+ when /^postgresql/i then "serial primary key"
34
+ when /^sqlite/i then "integer primary key autoincrement"
35
+ end + ", login varchar(255))"
36
+ end
37
+
38
+ context "with indexes in each schema" do
39
+ before(:each) do
40
+ connection.execute 'CREATE INDEX ' + case connection.adapter_name
41
+ when /^mysql/i then "index_users_on_login ON schema_plus_pg_indexes_test2.users"
42
+ when /^postgresql/i then "index_users_on_login ON schema_plus_pg_indexes_test2.users"
43
+ when /^sqlite/i then "schema_plus_pg_indexes_test2.index_users_on_login ON users"
44
+ end + " (login)"
45
+ end
46
+
47
+ it "should not find indexes in other schema" do
48
+ User.reset_column_information
49
+ expect(User.indexes).to be_empty
50
+ end
51
+
52
+ it "should find index in current schema" do
53
+ connection.execute 'CREATE INDEX index_users_on_login ON users (login)'
54
+ User.reset_column_information
55
+ expect(User.indexes.map(&:name)).to eq(['index_users_on_login'])
56
+ end
57
+ end
58
+
59
+ end
60
+
@@ -0,0 +1,166 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+
4
+ describe "Schema dump" do
5
+
6
+ before(:all) do
7
+ ActiveRecord::Migration.suppress_messages do
8
+ ActiveRecord::Schema.define do
9
+ connection.tables.each do |table| drop_table table, :cascade => true end
10
+
11
+ create_table :users, :force => true do |t|
12
+ t.string :login
13
+ t.datetime :deleted_at
14
+ t.integer :first_post_id
15
+ end
16
+
17
+ create_table :posts, :force => true do |t|
18
+ t.text :body
19
+ t.integer :user_id
20
+ t.integer :first_comment_id
21
+ t.string :string_no_default
22
+ t.integer :short_id
23
+ t.string :str_short
24
+ t.integer :integer_col
25
+ t.float :float_col
26
+ t.decimal :decimal_col
27
+ t.datetime :datetime_col
28
+ t.timestamp :timestamp_col
29
+ t.time :time_col
30
+ t.date :date_col
31
+ t.binary :binary_col
32
+ t.boolean :boolean_col
33
+ end
34
+
35
+ create_table :comments, :force => true do |t|
36
+ t.text :body
37
+ t.integer :post_id
38
+ t.integer :commenter_id
39
+ end
40
+ end
41
+ end
42
+ class ::User < ActiveRecord::Base ; end
43
+ class ::Post < ActiveRecord::Base ; end
44
+ class ::Comment < ActiveRecord::Base ; end
45
+ end
46
+
47
+ context "index extras" do
48
+
49
+ it "should define case insensitive index" do
50
+ with_index Post, [:body, :string_no_default], :case_sensitive => false do
51
+ expect(dump_posts).to match(/"body".*index: {.*with:.*string_no_default.*case_sensitive: false/)
52
+ end
53
+ end
54
+
55
+ it "should define index with type cast" do
56
+ with_index Post, [:integer_col], :name => "index_with_type_cast", :expression => "LOWER(integer_col::text)" do
57
+ expect(dump_posts).to include(%q{t.index name: "index_with_type_cast", expression: "lower((integer_col)::text)"})
58
+ end
59
+ end
60
+
61
+
62
+ it "should define case insensitive index with mixed ids and strings" do
63
+ with_index Post, [:user_id, :str_short, :short_id, :body], :case_sensitive => false do
64
+ expect(dump_posts).to match(/user_id.*index: {.* with: \["str_short", "short_id", "body"\], case_sensitive: false}/)
65
+ end
66
+ end
67
+
68
+ [:integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |col_type|
69
+ col_name = "#{col_type}_col"
70
+ it "should define case insensitive index that includes an #{col_type}" do
71
+ with_index Post, [:user_id, :str_short, col_name, :body], :case_sensitive => false do
72
+ expect(dump_posts).to match(/user_id.*index: {.* with: \["str_short", "#{col_name}", "body"\], case_sensitive: false}/)
73
+ end
74
+ end
75
+ end
76
+
77
+ it "should define where" do
78
+ with_index Post, :user_id, :name => "posts_user_id_index", :where => "user_id IS NOT NULL" do
79
+ expect(dump_posts).to match(/user_id.*index: {.*where: "\(user_id IS NOT NULL\)"}/)
80
+ end
81
+ end
82
+
83
+ it "should define expression" do
84
+ with_index Post, :name => "posts_freaky_index", :expression => "USING hash (least(id, user_id))" do
85
+ expect(dump_posts).to include(%q{t.index name: "posts_freaky_index", using: :hash, expression: "LEAST(id, user_id)"})
86
+ end
87
+ end
88
+
89
+ it "should define operator_class" do
90
+ with_index Post, :body, :operator_class => 'text_pattern_ops' do
91
+ expect(dump_posts).to match(/body.*index:.*operator_class: "text_pattern_ops"/)
92
+ end
93
+ end
94
+
95
+ it "should define multi-column operator classes " do
96
+ with_index Post, [:body, :string_no_default], :operator_class => {body: 'text_pattern_ops', string_no_default: 'varchar_pattern_ops' } do
97
+ expect(dump_posts).to match(/body.*index:.*operator_class: {"body"=>"text_pattern_ops", "string_no_default"=>"varchar_pattern_ops"}/)
98
+ end
99
+ end
100
+
101
+ it "should dump unique: true with expression (Issue #142)" do
102
+ with_index Post, :name => "posts_user_body_index", :unique => true, :expression => "BTRIM(LOWER(body))" do
103
+ expect(dump_posts).to include(%q{t.index name: "posts_user_body_index", unique: true, expression: "btrim(lower(body))"})
104
+ end
105
+ end
106
+
107
+
108
+ it "should not define :case_sensitive => false with non-trivial expression" do
109
+ with_index Post, :name => "posts_user_body_index", :expression => "BTRIM(LOWER(body))" do
110
+ expect(dump_posts).to include(%q{t.index name: "posts_user_body_index", expression: "btrim(lower(body))"})
111
+ end
112
+ end
113
+
114
+ it "should define using" do
115
+ with_index Post, :name => "posts_body_index", :expression => "USING hash (body)" do
116
+ expect(dump_posts).to match(/body.*index:.*using: :hash/)
117
+ end
118
+ end
119
+
120
+ it "should not include index order for non-ordered index types" do
121
+ with_index Post, :user_id, :using => :hash do
122
+ expect(dump_posts).to match(/user_id.*index:.*using: :hash/)
123
+ expect(dump_posts).not_to match(%r{order})
124
+ end
125
+ end
126
+
127
+ end
128
+
129
+ protected
130
+
131
+ def with_index(*args)
132
+ options = args.extract_options!
133
+ model, columns = args
134
+ ActiveRecord::Migration.suppress_messages do
135
+ ActiveRecord::Migration.add_index(model.table_name, columns, options)
136
+ end
137
+ model.reset_column_information
138
+ begin
139
+ yield
140
+ ensure
141
+ ActiveRecord::Migration.suppress_messages do
142
+ ActiveRecord::Migration.remove_index(model.table_name, :name => determine_index_name(model, columns, options))
143
+ end
144
+ end
145
+ end
146
+
147
+ def determine_index_name(model, columns, options)
148
+ name = columns[:name] if columns.is_a?(Hash)
149
+ name ||= options[:name]
150
+ name ||= model.indexes.detect { |index| index.table == model.table_name.to_s && index.columns.sort == Array(columns).collect(&:to_s).sort }.name
151
+ name
152
+ end
153
+
154
+ def dump_schema(opts={})
155
+ stream = StringIO.new
156
+ ActiveRecord::SchemaDumper.ignore_tables = Array.wrap(opts[:ignore]) || []
157
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
158
+ stream.string
159
+ end
160
+
161
+ def dump_posts
162
+ dump_schema(:ignore => %w[users comments])
163
+ end
164
+
165
+ end
166
+