pg_morph 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cef0e13fb0248806b82ff74afe1306cf9fa80260
|
4
|
+
data.tar.gz: 617f2272d8aa391e06daca0ada34805c64d447d7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 15155e9dbf5d53573b9eee074d94090a7489a3b3603a81bca75928554cb8908884013eebf403049ca69e1bfac9b886753ae90aaecfb00725b08aaed732b94551
|
7
|
+
data.tar.gz: 4837b92da2446cdd827cc31c83f65c17b340c6ef83beb04591b589948b1276d78c1f9a94aad5a22c3b88593fddbb583b12d06c62b874922ba1fefc3f419298e3
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.1
|
4
|
+
before_script:
|
5
|
+
- psql -c 'create database pg_morph_test;' -U postgres
|
6
|
+
script:
|
7
|
+
- bundle exec rake db:migrate RAILS_ENV=test
|
8
|
+
- CODECLIMATE_REPO_TOKEN=60392238805181ab599e01b29566617c47bffed0083cbfd119aa3c230f9d611d bundle exec rake
|
9
|
+
addons:
|
10
|
+
postgresql: 9.3
|
11
|
+
notifications:
|
12
|
+
email:
|
13
|
+
- hanka@lunarlogic.io
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
### v1.0.0
|
2
|
+
|
3
|
+
This version adds additional layer on top of all tables and partitions, which is table view. Thanks to that there is no need for any after insert triggers and ActiveRecord don't have any problems with missing id after creating a new record.
|
4
|
+
|
5
|
+
This version is not compatible with previous one.
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pg_morph (
|
4
|
+
pg_morph (1.0.0)
|
5
5
|
activerecord (~> 3)
|
6
6
|
|
7
7
|
GEM
|
@@ -37,13 +37,34 @@ GEM
|
|
37
37
|
arel (3.0.3)
|
38
38
|
awesome_print (1.2.0)
|
39
39
|
builder (3.0.4)
|
40
|
+
celluloid (0.15.2)
|
41
|
+
timers (~> 1.1.0)
|
42
|
+
codeclimate-test-reporter (0.4.6)
|
43
|
+
simplecov (>= 0.7.1, < 1.0.0)
|
40
44
|
coderay (1.1.0)
|
41
45
|
diff-lcs (1.2.5)
|
46
|
+
docile (1.1.5)
|
42
47
|
erubis (2.7.0)
|
48
|
+
ffi (1.9.3)
|
49
|
+
formatador (0.2.4)
|
50
|
+
guard (2.2.4)
|
51
|
+
formatador (>= 0.2.4)
|
52
|
+
listen (~> 2.1)
|
53
|
+
lumberjack (~> 1.0)
|
54
|
+
pry (>= 0.9.12)
|
55
|
+
thor (>= 0.18.1)
|
56
|
+
guard-rspec (4.2.0)
|
57
|
+
guard (>= 2.1.1)
|
58
|
+
rspec (>= 2.14, < 4.0)
|
43
59
|
hike (1.2.3)
|
44
60
|
i18n (0.6.11)
|
45
61
|
journey (1.0.4)
|
46
62
|
json (1.8.1)
|
63
|
+
listen (2.3.1)
|
64
|
+
celluloid (>= 0.15.2)
|
65
|
+
rb-fsevent (>= 0.9.3)
|
66
|
+
rb-inotify (>= 0.9)
|
67
|
+
lumberjack (1.0.4)
|
47
68
|
mail (2.5.4)
|
48
69
|
mime-types (~> 1.16)
|
49
70
|
treetop (~> 1.4.8)
|
@@ -79,8 +100,15 @@ GEM
|
|
79
100
|
rdoc (~> 3.4)
|
80
101
|
thor (>= 0.14.6, < 2.0)
|
81
102
|
rake (10.3.2)
|
103
|
+
rb-fsevent (0.9.3)
|
104
|
+
rb-inotify (0.9.2)
|
105
|
+
ffi (>= 0.5.0)
|
82
106
|
rdoc (3.12.2)
|
83
107
|
json (~> 1.4)
|
108
|
+
rspec (2.14.1)
|
109
|
+
rspec-core (~> 2.14.0)
|
110
|
+
rspec-expectations (~> 2.14.0)
|
111
|
+
rspec-mocks (~> 2.14.0)
|
84
112
|
rspec-core (2.14.8)
|
85
113
|
rspec-expectations (2.14.5)
|
86
114
|
diff-lcs (>= 1.1.3, < 2.0)
|
@@ -93,6 +121,11 @@ GEM
|
|
93
121
|
rspec-core (~> 2.14.0)
|
94
122
|
rspec-expectations (~> 2.14.0)
|
95
123
|
rspec-mocks (~> 2.14.0)
|
124
|
+
simplecov (0.9.1)
|
125
|
+
docile (~> 1.1.0)
|
126
|
+
multi_json (~> 1.0)
|
127
|
+
simplecov-html (~> 0.8.0)
|
128
|
+
simplecov-html (0.8.0)
|
96
129
|
slop (3.6.0)
|
97
130
|
sprockets (2.2.2)
|
98
131
|
hike (~> 1.2)
|
@@ -101,6 +134,7 @@ GEM
|
|
101
134
|
tilt (~> 1.1, != 1.3.0)
|
102
135
|
thor (0.19.1)
|
103
136
|
tilt (1.4.1)
|
137
|
+
timers (1.1.0)
|
104
138
|
treetop (1.4.15)
|
105
139
|
polyglot
|
106
140
|
polyglot (>= 0.3.1)
|
@@ -111,9 +145,11 @@ PLATFORMS
|
|
111
145
|
|
112
146
|
DEPENDENCIES
|
113
147
|
awesome_print (~> 1.2)
|
148
|
+
codeclimate-test-reporter (~> 0.4)
|
149
|
+
guard-rspec (~> 4.2)
|
114
150
|
pg (~> 0.17)
|
115
151
|
pg_morph!
|
116
152
|
pry (~> 0.10)
|
117
153
|
rails (~> 3)
|
118
154
|
rake (~> 10.3)
|
119
|
-
rspec-rails
|
155
|
+
rspec-rails (~> 2.14)
|
data/Guardfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :rspec, cmd: 'bundle exec rspec --color --format documentation' do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| ["spec/lib/#{m[1]}_spec.rb", "spec/lib/#{m[1]}_integration_spec.rb"] }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
end
|
9
|
+
|
data/README.md
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/LunarLogic/pg_morph.svg?branch=view)](https://travis-ci.org/LunarLogic/pg_morph)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/LunarLogic/pg_morph/badges/gpa.svg)](https://codeclimate.com/github/LunarLogic/pg_morph)
|
3
|
+
[![Test Coverage](https://codeclimate.com/github/LunarLogic/pg_morph/badges/coverage.svg)](https://codeclimate.com/github/LunarLogic/pg_morph)
|
4
|
+
|
1
5
|
# PgMorph
|
2
6
|
# ![PgMorph logo](docs/pg_morph.png)
|
3
7
|
|
4
8
|
PgMorph gives you a way to handle DB consistency for polymorphic relations and is based on postgreSQL inheritance and partitioning features.
|
5
9
|
|
10
|
+
## Requirements
|
11
|
+
|
12
|
+
postgresql >= 9.2
|
13
|
+
|
6
14
|
## Installation
|
7
15
|
|
8
16
|
Add this line to your application's Gemfile:
|
@@ -33,7 +41,8 @@ By adding migration:
|
|
33
41
|
add_polymorphic_foreign_key :likes, :comments, column: :likeable
|
34
42
|
```
|
35
43
|
|
36
|
-
|
44
|
+
At first PgMorph wants to use `likes` table view instead of normal table as a master one. To prevent braking all potential relations with other tables it renames `likes` table to `likes_base` and then creates a view of `likes_base` named `likes`.
|
45
|
+
PgMorph then creates a partition table named `likes_comments` which inherits from `likes_base` table, sets foreign key on it and redirects all inserts to `likes` - since this is the table name AR knows aobut - to this partition table if `likeable_type` is `Comment`. It's done by using before insert trigger.
|
37
46
|
|
38
47
|
You will have to add polymorphic foreign key on all related tables and each time new relation is added, before insert trigger function will be updated to reflect all defined relations and redirect new records to proper partitions.
|
39
48
|
|
@@ -47,11 +56,14 @@ remove_polymorphic_foreign_key :likes, :comments, column: :likeable
|
|
47
56
|
|
48
57
|
Because it means that whole partition table would be removed, you will be forbidden to do that if partition table contains any data.
|
49
58
|
|
50
|
-
##
|
59
|
+
## Caveats
|
60
|
+
|
61
|
+
While updating records check constraints which are set on each partition does not allow to change association type, so if in examplary model some like is for a comment, it can't be reassigned to a post, postgres will raise an exception.
|
51
62
|
|
52
|
-
|
63
|
+
## Development plan
|
53
64
|
|
54
|
-
|
65
|
+
- support moving records between partitions while updating relation type
|
66
|
+
- your suggestions?
|
55
67
|
|
56
68
|
## Contributing
|
57
69
|
|
data/Rakefile
CHANGED
data/lib/pg_morph/adapter.rb
CHANGED
@@ -7,11 +7,11 @@ module PgMorph
|
|
7
7
|
|
8
8
|
polymorphic = PgMorph::Polymorphic.new(parent_table, child_table, options)
|
9
9
|
|
10
|
-
sql =
|
10
|
+
sql = polymorphic.rename_base_table_sql
|
11
|
+
sql << polymorphic.create_base_table_view_sql
|
12
|
+
sql << polymorphic.create_proxy_table_sql
|
11
13
|
sql << polymorphic.create_before_insert_trigger_fun_sql
|
12
14
|
sql << polymorphic.create_before_insert_trigger_sql
|
13
|
-
sql << polymorphic.create_after_insert_trigger_fun_sql
|
14
|
-
sql << polymorphic.create_after_insert_trigger_sql
|
15
15
|
|
16
16
|
execute(sql)
|
17
17
|
end
|
@@ -22,8 +22,9 @@ module PgMorph
|
|
22
22
|
polymorphic = PgMorph::Polymorphic.new(parent_table, child_table, options)
|
23
23
|
|
24
24
|
sql = polymorphic.remove_before_insert_trigger_sql
|
25
|
-
sql << polymorphic.
|
26
|
-
sql << polymorphic.
|
25
|
+
sql << polymorphic.remove_proxy_table
|
26
|
+
sql << polymorphic.remove_base_table_view_sql
|
27
|
+
sql << polymorphic.rename_base_table_back_sql
|
27
28
|
|
28
29
|
execute(sql)
|
29
30
|
end
|
data/lib/pg_morph/naming.rb
CHANGED
@@ -25,13 +25,5 @@ module PgMorph
|
|
25
25
|
"#{parent_table}_#{column_name}_insert_trigger"
|
26
26
|
end
|
27
27
|
|
28
|
-
def after_insert_fun_name
|
29
|
-
"delete_from_#{parent_table}_master_fun"
|
30
|
-
end
|
31
|
-
|
32
|
-
def after_insert_trigger_name
|
33
|
-
"#{parent_table}_after_insert_trigger"
|
34
|
-
end
|
35
|
-
|
36
28
|
end
|
37
29
|
end
|
data/lib/pg_morph/polymorphic.rb
CHANGED
@@ -1,30 +1,63 @@
|
|
1
1
|
module PgMorph
|
2
2
|
|
3
3
|
class Polymorphic
|
4
|
+
BASE_TABLE_SUFIX = :base
|
5
|
+
|
4
6
|
include PgMorph::Naming
|
5
|
-
attr_reader :parent_table, :child_table, :column_name
|
7
|
+
attr_reader :parent_table, :child_table, :column_name, :base_table
|
6
8
|
|
7
9
|
def initialize(parent_table, child_table, options)
|
8
10
|
@parent_table = parent_table
|
9
11
|
@child_table = child_table
|
10
12
|
@column_name = options[:column]
|
13
|
+
@base_table = options[:base_table] || :"#{parent_table}_#{BASE_TABLE_SUFIX}"
|
11
14
|
|
12
15
|
raise PgMorph::Exception.new("Column not specified") unless @column_name
|
13
16
|
end
|
14
17
|
|
18
|
+
def rename_base_table_sql
|
19
|
+
return '' unless can_rename_to_base_table?
|
20
|
+
%Q{
|
21
|
+
ALTER TABLE #{parent_table} RENAME TO #{base_table};
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def can_rename_to_base_table?
|
26
|
+
return true unless ActiveRecord::Base.connection.table_exists? base_table
|
27
|
+
|
28
|
+
parent_table_set = ActiveRecord::Base.connection.columns(parent_table).
|
29
|
+
map{|column| column.as_json.except('null')}
|
30
|
+
base_table_set = ActiveRecord::Base.connection.columns(base_table).
|
31
|
+
map{|column| column.as_json.except('null')}
|
32
|
+
|
33
|
+
return false if parent_table_set == base_table_set
|
34
|
+
raise PgMorph::Exception.new('table name mismatch!')
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_base_table_view_sql
|
38
|
+
%Q{
|
39
|
+
CREATE OR REPLACE VIEW #{parent_table} AS SELECT * FROM #{base_table};
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
15
43
|
def create_proxy_table_sql
|
16
44
|
%Q{
|
17
45
|
CREATE TABLE #{proxy_table} (
|
18
46
|
CHECK (#{column_name_type} = '#{type}'),
|
19
47
|
PRIMARY KEY (id),
|
20
48
|
FOREIGN KEY (#{column_name_id}) REFERENCES #{child_table}(id)
|
21
|
-
) INHERITS (#{
|
49
|
+
) INHERITS (#{base_table});
|
22
50
|
}
|
23
51
|
end
|
24
52
|
|
25
53
|
def create_before_insert_trigger_fun_sql
|
26
54
|
before_insert_trigger_content do
|
27
|
-
|
55
|
+
%Q{
|
56
|
+
IF NEW.id IS NULL THEN
|
57
|
+
NEW.id := nextval('#{parent_table}_id_seq');
|
58
|
+
END IF;
|
59
|
+
#{create_trigger_body.strip}
|
60
|
+
}
|
28
61
|
end
|
29
62
|
end
|
30
63
|
|
@@ -32,44 +65,29 @@ module PgMorph
|
|
32
65
|
fun_name = before_insert_fun_name
|
33
66
|
trigger_name = before_insert_trigger_name
|
34
67
|
|
35
|
-
create_trigger_sql(parent_table, trigger_name, fun_name, '
|
36
|
-
end
|
37
|
-
|
38
|
-
def create_after_insert_trigger_sql
|
39
|
-
fun_name = after_insert_fun_name
|
40
|
-
trigger_name = after_insert_trigger_name
|
41
|
-
|
42
|
-
create_trigger_sql(parent_table, trigger_name, fun_name, 'AFTER INSERT')
|
43
|
-
end
|
44
|
-
|
45
|
-
def create_after_insert_trigger_fun_sql
|
46
|
-
fun_name = after_insert_fun_name
|
47
|
-
create_trigger_fun(fun_name) do
|
48
|
-
%Q{DELETE FROM ONLY #{parent_table} WHERE id = NEW.id;}
|
49
|
-
end
|
68
|
+
create_trigger_sql(parent_table, trigger_name, fun_name, 'INSTEAD OF INSERT')
|
50
69
|
end
|
51
70
|
|
52
71
|
def remove_before_insert_trigger_sql
|
53
72
|
trigger_name = before_insert_trigger_name
|
54
73
|
fun_name = before_insert_fun_name
|
55
|
-
|
56
|
-
prosrc = get_function(fun_name)
|
57
|
-
raise PG::Error.new("There is no such function #{fun_name}()\n") unless prosrc
|
58
|
-
|
59
|
-
scan = prosrc.scan(/(( +(ELS)?IF.+\n)(\s+INSERT INTO.+;\n))/)
|
60
|
-
cleared = scan.reject { |x| x[0].match("#{proxy_table}") }
|
74
|
+
cleared = check_more_partitions
|
61
75
|
|
62
76
|
if cleared.present?
|
63
|
-
cleared
|
64
|
-
before_insert_trigger_content do
|
65
|
-
cleared.map { |m| m[0] }.join('').strip
|
66
|
-
end
|
77
|
+
update_before_insert_trigger_sql(cleared)
|
67
78
|
else
|
68
79
|
drop_trigger_and_fun_sql(trigger_name, parent_table, fun_name)
|
69
80
|
end
|
70
81
|
end
|
71
82
|
|
72
|
-
def
|
83
|
+
def update_before_insert_trigger_sql(cleared)
|
84
|
+
cleared[0][0].sub!('ELSIF', 'IF')
|
85
|
+
before_insert_trigger_content do
|
86
|
+
cleared.map { |m| m[0] }.join('').strip
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def remove_proxy_table
|
73
91
|
table_empty = ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM #{parent_table}_#{child_table}").to_i.zero?
|
74
92
|
if table_empty
|
75
93
|
%Q{ DROP TABLE IF EXISTS #{proxy_table}; }
|
@@ -78,20 +96,34 @@ module PgMorph
|
|
78
96
|
end
|
79
97
|
end
|
80
98
|
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
trigger_name = after_insert_trigger_name
|
99
|
+
def remove_base_table_view_sql
|
100
|
+
if check_more_partitions.present?
|
101
|
+
''
|
102
|
+
else
|
103
|
+
%Q{ DROP VIEW #{parent_table}; }
|
104
|
+
end
|
105
|
+
end
|
89
106
|
|
90
|
-
|
107
|
+
def rename_base_table_back_sql
|
108
|
+
if check_more_partitions.present?
|
109
|
+
''
|
110
|
+
else
|
111
|
+
%Q{ ALTER TABLE #{base_table} RENAME TO #{parent_table}; }
|
112
|
+
end
|
91
113
|
end
|
92
114
|
|
93
115
|
private
|
94
116
|
|
117
|
+
def check_more_partitions
|
118
|
+
fun_name = before_insert_fun_name
|
119
|
+
|
120
|
+
prosrc = get_function(fun_name)
|
121
|
+
raise PG::Error.new("There is no such function #{fun_name}()\n") unless prosrc
|
122
|
+
|
123
|
+
scan = prosrc.scan(/(( +(ELS)?IF.+\n)(\s+INSERT INTO.+;\n))/)
|
124
|
+
scan.reject { |x| x[0].match("#{proxy_table}") }
|
125
|
+
end
|
126
|
+
|
95
127
|
def create_trigger_fun(fun_name, &block)
|
96
128
|
%Q{
|
97
129
|
CREATE OR REPLACE FUNCTION #{fun_name}() RETURNS TRIGGER AS $$
|
@@ -115,21 +147,29 @@ module PgMorph
|
|
115
147
|
prosrc = get_function(before_insert_fun_name)
|
116
148
|
|
117
149
|
if prosrc
|
118
|
-
|
119
|
-
raise PG::Error.new("Condition for #{proxy_table} table already exists in trigger function") if scan[0][0].match proxy_table
|
120
|
-
%Q{
|
121
|
-
#{scan.map { |m| m[0] }.join.strip}
|
122
|
-
ELSIF (NEW.#{column_name}_type = '#{child_table.to_s.singularize.camelize}') THEN
|
123
|
-
INSERT INTO #{parent_table}_#{child_table} VALUES (NEW.*);
|
124
|
-
}
|
150
|
+
update_existing_trigger_body(prosrc)
|
125
151
|
else
|
126
|
-
|
127
|
-
IF (NEW.#{column_name}_type = '#{child_table.to_s.singularize.camelize}') THEN
|
128
|
-
INSERT INTO #{parent_table}_#{child_table} VALUES (NEW.*);
|
129
|
-
}
|
152
|
+
create_new_trigger_body
|
130
153
|
end
|
131
154
|
end
|
132
155
|
|
156
|
+
def update_existing_trigger_body(prosrc)
|
157
|
+
scan = prosrc.scan(/(( +(ELS)?IF.+\n)(\s+INSERT INTO.+;\n))/)
|
158
|
+
raise PG::Error.new("Condition for #{proxy_table} table already exists in trigger function") if scan[0][0].match proxy_table
|
159
|
+
%Q{
|
160
|
+
#{scan.map { |m| m[0] }.join.strip}
|
161
|
+
ELSIF (NEW.#{column_name}_type = '#{child_table.to_s.singularize.camelize}') THEN
|
162
|
+
INSERT INTO #{parent_table}_#{child_table} VALUES (NEW.*);
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def create_new_trigger_body
|
167
|
+
%Q{
|
168
|
+
IF (NEW.#{column_name}_type = '#{child_table.to_s.singularize.camelize}') THEN
|
169
|
+
INSERT INTO #{parent_table}_#{child_table} VALUES (NEW.*);
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
133
173
|
def create_trigger_sql(parent_table, trigger_name, fun_name, when_to_call)
|
134
174
|
%Q{
|
135
175
|
DROP TRIGGER IF EXISTS #{trigger_name} ON #{parent_table};
|
data/lib/pg_morph/version.rb
CHANGED