schema_plus_pg_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,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
+