pg_morph 0.3.0 → 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile.lock +38 -2
- data/Guardfile +9 -0
- data/README.md +16 -4
- data/Rakefile +7 -0
- data/lib/pg_morph/adapter.rb +6 -5
- data/lib/pg_morph/naming.rb +0 -8
- data/lib/pg_morph/polymorphic.rb +89 -49
- data/lib/pg_morph/version.rb +1 -1
- data/pg_morph.gemspec +3 -1
- data/spec/dummy/db/schema.rb +11 -14
- data/spec/lib/pg_morph/adapter_integration_spec.rb +263 -0
- data/spec/{pg_morph → lib/pg_morph}/adapter_spec.rb +0 -9
- data/spec/{pg_morph → lib/pg_morph}/naming_spec.rb +0 -4
- data/spec/lib/pg_morph/polymorphic_integration_spec.rb +125 -0
- data/spec/{pg_morph → lib/pg_morph}/polymorphic_spec.rb +44 -34
- data/spec/spec_helper.rb +8 -0
- metadata +65 -51
- data/spec/pg_morph/adapter_integration_spec.rb +0 -89
- data/spec/pg_morph/polymorphic_integration_spec.rb +0 -86
data/pg_morph.gemspec
CHANGED
@@ -23,7 +23,9 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_development_dependency "rails", "~> 3"
|
24
24
|
spec.add_development_dependency "pg", "~> 0.17"
|
25
25
|
|
26
|
-
spec.add_development_dependency
|
26
|
+
spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4"
|
27
|
+
spec.add_development_dependency "rspec-rails", "~> 2.14"
|
28
|
+
spec.add_development_dependency "guard-rspec", "~> 4.2"
|
27
29
|
|
28
30
|
spec.add_development_dependency "rake", "~> 10.3"
|
29
31
|
spec.add_development_dependency "pry", "~> 0.10"
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -9,30 +9,27 @@
|
|
9
9
|
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
10
10
|
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
11
11
|
#
|
12
|
-
# It's strongly recommended
|
12
|
+
# It's strongly recommended to check this file into your version control system.
|
13
13
|
|
14
|
-
ActiveRecord::Schema.define(version
|
14
|
+
ActiveRecord::Schema.define(:version => 20140401124727) do
|
15
15
|
|
16
|
-
|
17
|
-
enable_extension "plpgsql"
|
18
|
-
|
19
|
-
create_table "comments", force: true do |t|
|
16
|
+
create_table "comments", :force => true do |t|
|
20
17
|
t.string "content"
|
21
|
-
t.datetime "created_at"
|
22
|
-
t.datetime "updated_at"
|
18
|
+
t.datetime "created_at", :null => false
|
19
|
+
t.datetime "updated_at", :null => false
|
23
20
|
end
|
24
21
|
|
25
|
-
create_table "likes", force
|
22
|
+
create_table "likes", :force => true do |t|
|
26
23
|
t.string "likeable_type"
|
27
24
|
t.integer "likeable_id"
|
28
|
-
t.datetime "created_at"
|
29
|
-
t.datetime "updated_at"
|
25
|
+
t.datetime "created_at", :null => false
|
26
|
+
t.datetime "updated_at", :null => false
|
30
27
|
end
|
31
28
|
|
32
|
-
create_table "posts", force
|
29
|
+
create_table "posts", :force => true do |t|
|
33
30
|
t.string "content"
|
34
|
-
t.datetime "created_at"
|
35
|
-
t.datetime "updated_at"
|
31
|
+
t.datetime "created_at", :null => false
|
32
|
+
t.datetime "updated_at", :null => false
|
36
33
|
end
|
37
34
|
|
38
35
|
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PgMorph::Adapter do
|
4
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
|
5
|
+
include PgMorph::Adapter
|
6
|
+
|
7
|
+
def pg_views(name = nil)
|
8
|
+
query(%Q{
|
9
|
+
SELECT viewname
|
10
|
+
FROM pg_views
|
11
|
+
WHERE schemaname = ANY (current_schemas(false))
|
12
|
+
}, 'SCHEMA').map { |row| row[0] }
|
13
|
+
end
|
14
|
+
|
15
|
+
def run(query)
|
16
|
+
ActiveRecord::Base.connection.select_value(query)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:comment) { Comment.create(content: 'comment') }
|
21
|
+
|
22
|
+
before do
|
23
|
+
@adapter = ActiveRecord::Base.connection
|
24
|
+
@comments_polymorphic = PgMorph::Polymorphic.new(:likes, :comments, column: :likeable)
|
25
|
+
@posts_polymorphic = PgMorph::Polymorphic.new(:likes, :posts, column: :likeable)
|
26
|
+
begin
|
27
|
+
Like.destroy_all
|
28
|
+
Comment.destroy_all
|
29
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
30
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :posts, column: :likeable)
|
31
|
+
rescue
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
after do
|
36
|
+
begin
|
37
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
38
|
+
rescue
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#add_polymorphic_foreign_key' do
|
43
|
+
it 'renames base table' do
|
44
|
+
expect(@adapter.tables).to include "likes"
|
45
|
+
expect(@adapter.tables).not_to include "likes_base"
|
46
|
+
|
47
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
48
|
+
|
49
|
+
expect(@adapter.tables).not_to include "likes"
|
50
|
+
expect(@adapter.tables).to include "likes_base"
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'creates base table view' do
|
54
|
+
expect(@adapter.pg_views).to be_empty
|
55
|
+
expect(@adapter.tables).to include "likes"
|
56
|
+
|
57
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
58
|
+
|
59
|
+
expect(@adapter.pg_views).to include "likes"
|
60
|
+
expect(@adapter.tables).not_to include "likes"
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'creates proxy table' do
|
64
|
+
expect(@adapter.tables).not_to include "likes_comments"
|
65
|
+
|
66
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
67
|
+
|
68
|
+
expect(@adapter.tables).to include "likes_comments"
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'creates before insert trigger fun' do
|
72
|
+
fun_name = @comments_polymorphic.before_insert_fun_name
|
73
|
+
expect(@adapter.run("select proname from pg_proc where proname = '#{fun_name}'")).
|
74
|
+
to be nil
|
75
|
+
|
76
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
77
|
+
|
78
|
+
expect(@adapter.run("select proname from pg_proc where proname = '#{fun_name}'")).
|
79
|
+
to eq fun_name
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'creates before insert trigger' do
|
83
|
+
trigger_name = @comments_polymorphic.before_insert_trigger_name
|
84
|
+
expect(@adapter.query("SELECT * FROM pg_trigger WHERE tgname = '#{trigger_name}'")).
|
85
|
+
to be_empty
|
86
|
+
|
87
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
88
|
+
|
89
|
+
expect(@adapter.query("SELECT count(*) FROM pg_trigger WHERE tgname = '#{trigger_name}'")).
|
90
|
+
to eq [["1"]]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#remove_polymorphic_foreign_key' do
|
95
|
+
before do
|
96
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'removes before insert trigger' do
|
100
|
+
fun_name = @comments_polymorphic.before_insert_fun_name
|
101
|
+
expect(@adapter.run("select proname from pg_proc where proname = '#{fun_name}'")).
|
102
|
+
to eq fun_name
|
103
|
+
|
104
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
105
|
+
|
106
|
+
expect(@adapter.run("select proname from pg_proc where proname = '#{fun_name}'")).
|
107
|
+
to be nil
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'removes before insert trigger fun' do
|
111
|
+
trigger_name = @comments_polymorphic.before_insert_trigger_name
|
112
|
+
expect(@adapter.query("SELECT count(*) FROM pg_trigger WHERE tgname = '#{trigger_name}'")).
|
113
|
+
to eq [["1"]]
|
114
|
+
|
115
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
116
|
+
|
117
|
+
expect(@adapter.query("SELECT * FROM pg_trigger WHERE tgname = '#{trigger_name}'")).
|
118
|
+
to be_empty
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'removes proxy table' do
|
122
|
+
expect(@adapter.tables).to include "likes_comments"
|
123
|
+
|
124
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
125
|
+
|
126
|
+
expect(@adapter.tables).not_to include "likes_comments"
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'prevents from removing proxy with data' do
|
130
|
+
Like.create(likeable: comment)
|
131
|
+
|
132
|
+
-> { @adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable) }
|
133
|
+
.should raise_error PG::Error
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'removes table view if empty' do
|
137
|
+
expect(@adapter.pg_views).to include "likes"
|
138
|
+
expect(@adapter.tables).not_to include "likes"
|
139
|
+
|
140
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
141
|
+
|
142
|
+
expect(@adapter.pg_views).to be_empty
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'renames base table to original name' do
|
146
|
+
expect(@adapter.pg_views).to include "likes"
|
147
|
+
expect(@adapter.tables).not_to include "likes"
|
148
|
+
|
149
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
150
|
+
|
151
|
+
expect(@adapter.tables).to include "likes"
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'with more than one partitions' do
|
155
|
+
before do
|
156
|
+
@adapter.add_polymorphic_foreign_key(:likes, :posts, column: :likeable)
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'does not rename base table to original name' do
|
160
|
+
expect(@adapter.pg_views).to include "likes"
|
161
|
+
expect(@adapter.tables).not_to include "likes"
|
162
|
+
|
163
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
164
|
+
|
165
|
+
expect(@adapter.tables).not_to include "likes"
|
166
|
+
expect(@adapter.pg_views).to include "likes"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe 'operations on a partition' do
|
172
|
+
let(:comment) { Comment.create(content: 'comment') }
|
173
|
+
let(:post) { Post.create(content: 'content') }
|
174
|
+
|
175
|
+
before do
|
176
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
177
|
+
@comment_like = Like.create(likeable: comment)
|
178
|
+
end
|
179
|
+
|
180
|
+
context "creating records" do
|
181
|
+
it "works with single partition" do
|
182
|
+
expect(Like.count).to eq(1)
|
183
|
+
expect(@comment_like.id).to eq(Like.last.id)
|
184
|
+
end
|
185
|
+
|
186
|
+
it "works with multiple partitions" do
|
187
|
+
@adapter.add_polymorphic_foreign_key(:likes, :posts, column: :likeable)
|
188
|
+
post_like = Like.create(likeable: post)
|
189
|
+
|
190
|
+
expect(Like.count).to eq(2)
|
191
|
+
expect(post_like.id).to eq(Like.last.id)
|
192
|
+
end
|
193
|
+
|
194
|
+
it "raises error for a missing partition" do
|
195
|
+
-> { Like.create(likeable: post) }
|
196
|
+
.should raise_error ActiveRecord::StatementInvalid
|
197
|
+
end
|
198
|
+
|
199
|
+
it "works if no partitions" do
|
200
|
+
@comment_like.destroy
|
201
|
+
expect(Like.count).to eq(0)
|
202
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
203
|
+
like = Like.create(likeable: post)
|
204
|
+
|
205
|
+
expect(Like.count).to eq(1)
|
206
|
+
expect(like.id).to eq(Like.last.id)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context "updating records" do
|
211
|
+
let(:another_comment) { Comment.create(content: 'comment') }
|
212
|
+
|
213
|
+
before do
|
214
|
+
@adapter.add_polymorphic_foreign_key(:likes, :posts, column: :likeable)
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'works within one partition' do
|
218
|
+
expect(@comment_like.likeable).to eq(comment)
|
219
|
+
|
220
|
+
@comment_like.likeable = another_comment
|
221
|
+
@comment_like.save
|
222
|
+
|
223
|
+
@comment_like.reload
|
224
|
+
expect(@comment_like.likeable).to eq(another_comment)
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'does not allow to change associated type' do
|
228
|
+
expect(@comment_like.likeable).to eq(comment)
|
229
|
+
|
230
|
+
@comment_like.likeable = post
|
231
|
+
expect { @comment_like.save }.to raise_error ActiveRecord::StatementInvalid
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
context "deleting records" do
|
236
|
+
before do
|
237
|
+
expect(@adapter.run("SELECT id from likes where id = #{@comment_like.id}"))
|
238
|
+
.to eq @comment_like.id.to_s
|
239
|
+
expect(@adapter.run("SELECT id from likes_comments where id = #{@comment_like.id}"))
|
240
|
+
.to eq @comment_like.id.to_s
|
241
|
+
@comment_like.destroy
|
242
|
+
end
|
243
|
+
|
244
|
+
it "works on a partition" do
|
245
|
+
expect(@adapter.run("SELECT id from likes where id = #{@comment_like.id}")).to eq nil
|
246
|
+
expect(@adapter.run("SELECT id from likes_comments where id = #{@comment_like.id}")).to eq nil
|
247
|
+
end
|
248
|
+
|
249
|
+
context "after removing paritions" do
|
250
|
+
before do
|
251
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
252
|
+
@like = Like.create(likeable: comment)
|
253
|
+
end
|
254
|
+
|
255
|
+
it "works on a master table" do
|
256
|
+
@like.destroy
|
257
|
+
expect(@adapter.run("SELECT id from likes where id = #{@comment_like.id}")).to eq nil
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
@@ -22,8 +22,4 @@ describe PgMorph::Naming do
|
|
22
22
|
|
23
23
|
it { expect(@fake.before_insert_trigger_name).to eq('foos_baz_insert_trigger') }
|
24
24
|
|
25
|
-
it { expect(@fake.after_insert_fun_name).to eq('delete_from_foos_master_fun') }
|
26
|
-
|
27
|
-
it { expect(@fake.after_insert_trigger_name).to eq('foos_after_insert_trigger') }
|
28
|
-
|
29
25
|
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PgMorph::Polymorphic do
|
4
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
|
5
|
+
include PgMorph::Adapter
|
6
|
+
|
7
|
+
def run(query)
|
8
|
+
ActiveRecord::Base.connection.select_value(query)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
before do
|
13
|
+
@adapter = ActiveRecord::Base.connection
|
14
|
+
@comments_polymorphic = PgMorph::Polymorphic.new(:likes, :comments, column: :likeable)
|
15
|
+
@posts_polymorphic = PgMorph::Polymorphic.new(:likes, :posts, column: :likeable)
|
16
|
+
begin
|
17
|
+
Like.destroy_all
|
18
|
+
Comment.destroy_all
|
19
|
+
@adapter.remove_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
20
|
+
rescue
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#can_rename_to_base_table?' do
|
25
|
+
it 'returns true if base table does not exist' do
|
26
|
+
@adapter.stub(:table_exists?).with(@comments_polymorphic.base_table)
|
27
|
+
.and_return(false)
|
28
|
+
|
29
|
+
expect(@comments_polymorphic.can_rename_to_base_table?)
|
30
|
+
.to be_true
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'returns false if there is compatible base table' do
|
34
|
+
@adapter.add_polymorphic_foreign_key(:likes, :posts, column: :likeable)
|
35
|
+
|
36
|
+
expect(@comments_polymorphic.can_rename_to_base_table?)
|
37
|
+
.to be_false
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'raises an exception if existing base table is not compatible table' do
|
41
|
+
@adapter.create_table(@comments_polymorphic.base_table)
|
42
|
+
|
43
|
+
expect { @comments_polymorphic.can_rename_to_base_table? }.
|
44
|
+
to raise_error PgMorph::Exception
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#create_trigger_body' do
|
49
|
+
before do
|
50
|
+
@adapter.stub(:raise_unless_postgres)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'raises error for updating trigger with duplicated partition' do
|
54
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
55
|
+
|
56
|
+
expect { @comments_polymorphic.send(:create_trigger_body) }
|
57
|
+
.to raise_error PG::Error
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'updates trigger with new partition' do
|
61
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
62
|
+
|
63
|
+
expect(@posts_polymorphic.send(:create_trigger_body).squeeze(' ')).to eq %Q{
|
64
|
+
IF (NEW.likeable_type = 'Comment') THEN
|
65
|
+
INSERT INTO likes_comments VALUES (NEW.*);
|
66
|
+
ELSIF (NEW.likeable_type = 'Post') THEN
|
67
|
+
INSERT INTO likes_posts VALUES (NEW.*);
|
68
|
+
}.squeeze(' ')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#create_before_insert_trigger_sql' do
|
73
|
+
it 'returns sql' do
|
74
|
+
expect(@comments_polymorphic.create_before_insert_trigger_sql.squeeze(' ')).to eq %Q{
|
75
|
+
DROP TRIGGER IF EXISTS likes_likeable_insert_trigger ON likes;
|
76
|
+
CREATE TRIGGER likes_likeable_insert_trigger
|
77
|
+
INSTEAD OF INSERT ON likes
|
78
|
+
FOR EACH ROW EXECUTE PROCEDURE likes_likeable_fun();
|
79
|
+
}.squeeze(' ')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#remove_before_insert_trigger_sql' do
|
84
|
+
it 'returns proper sql if more partitions' do
|
85
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
86
|
+
@adapter.add_polymorphic_foreign_key(:likes, :posts, column: :likeable)
|
87
|
+
|
88
|
+
expect(@comments_polymorphic.remove_before_insert_trigger_sql.squeeze(' ')).to eq %Q{
|
89
|
+
CREATE OR REPLACE FUNCTION likes_likeable_fun() RETURNS TRIGGER AS $$
|
90
|
+
BEGIN
|
91
|
+
IF (NEW.#{@posts_polymorphic.column_name}_type = 'Post') THEN
|
92
|
+
INSERT INTO #{@posts_polymorphic.parent_table}_#{@posts_polymorphic.child_table} VALUES (NEW.*);
|
93
|
+
ELSE
|
94
|
+
RAISE EXCEPTION 'Wrong "#{@posts_polymorphic.column_name}_type"="%" used. Create proper partition table and update likes_likeable_fun function', NEW.likeable_type;
|
95
|
+
END IF;
|
96
|
+
RETURN NEW;
|
97
|
+
END; $$ LANGUAGE plpgsql;
|
98
|
+
}.squeeze(' ')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#remove_proxy_table' do
|
103
|
+
it 'returns sql' do
|
104
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
105
|
+
|
106
|
+
expect(@comments_polymorphic.remove_proxy_table.squeeze(' ')).to eq %Q{ DROP TABLE IF EXISTS likes_comments; }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe '#remove_base_table_view_sql' do
|
111
|
+
it 'returns proper sql if no more partitions' do
|
112
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
113
|
+
|
114
|
+
expect(@comments_polymorphic.remove_base_table_view_sql.squeeze(' ')).to eq %Q{ DROP VIEW likes; }
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'returns empty string if there are more partitions' do
|
118
|
+
@adapter.add_polymorphic_foreign_key(:likes, :comments, column: :likeable)
|
119
|
+
@adapter.add_polymorphic_foreign_key(:likes, :posts, column: :likeable)
|
120
|
+
|
121
|
+
expect(@comments_polymorphic.remove_base_table_view_sql.squeeze(' ')).to eq ''
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|