polymorpheus 2.2.0 → 3.3.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 +5 -5
- data/README.md +21 -4
- data/lib/polymorpheus.rb +8 -0
- data/lib/polymorpheus/interface/belongs_to_polymorphic.rb +4 -3
- data/lib/polymorpheus/interface_builder.rb +2 -2
- data/lib/polymorpheus/interface_builder/association.rb +4 -3
- data/lib/polymorpheus/mysql_adapter.rb +15 -25
- data/lib/polymorpheus/mysql_adapter/foreigner_constraints.rb +30 -0
- data/lib/polymorpheus/schema_dumper.rb +3 -2
- data/lib/polymorpheus/trigger.rb +21 -20
- data/lib/polymorpheus/version.rb +1 -1
- data/polymorpheus.gemspec +3 -5
- data/spec/interface/belongs_to_polymorphic_spec.rb +149 -0
- data/spec/interface/has_many_as_polymorph_spec.rb +86 -0
- data/spec/interface/validates_polymorph_spec.rb +37 -0
- data/spec/interface_spec.rb +18 -191
- data/spec/mysql2_adapter_spec.rb +271 -110
- data/spec/schema_dumper_spec.rb +16 -25
- data/spec/spec_helper.rb +34 -2
- data/spec/support/active_record/connection_adapters/abstract_mysql_adapter.rb +9 -0
- data/spec/support/class_defs.rb +32 -0
- data/spec/support/connection_helpers.rb +21 -0
- data/spec/support/custom_matchers.rb +7 -7
- data/spec/support/schema_helpers.rb +17 -0
- data/spec/{sql_logger.rb → support/sql_logger.rb} +1 -1
- data/spec/support/sql_test_helpers.rb +41 -0
- data/spec/trigger_spec.rb +32 -21
- metadata +22 -45
- data/spec/shared_examples.rb +0 -115
- data/spec/support/db_setup.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4ab6587aad06136dbdf95584bba576349fc9731187a60dd6945d74bcb400b025
|
4
|
+
data.tar.gz: b83008d22a38740f1ef2909971e5fd662ed8ac5297449ddd1dd405af43a4d792
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 476c715b239796573c7a2de566a4dd52507445c68227c4559cb7ec9a443ab707f48d31ad1477edfcfa52183e18634eac329bbe7d7c4bdbf006d620544c9d88b8
|
7
|
+
data.tar.gz: a778788d64292837ad5ed6e4a4a4b4fbc9e2fcdd5f38d1c99088c3fa2e7d838b07e20e1df3cae0cb0c7bf6c9e537c0c1d90c3c1ac17fcd8b5db2076def0194b0
|
data/README.md
CHANGED
@@ -5,6 +5,23 @@
|
|
5
5
|
**Polymorphic relationships in Rails that keep your database happy with almost
|
6
6
|
no setup**
|
7
7
|
|
8
|
+
### Installation
|
9
|
+
|
10
|
+
If you are using Bundler, you can add the gem to your Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
# with Rails >= 4.2
|
14
|
+
gem 'polymorpheus'
|
15
|
+
```
|
16
|
+
|
17
|
+
Or:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# with Rails < 4.2
|
21
|
+
gem 'foreigner'
|
22
|
+
gem 'polymorpheus'
|
23
|
+
```
|
24
|
+
|
8
25
|
### Background
|
9
26
|
* **What is polymorphism?** [Rails Guides has a great overview of what
|
10
27
|
polymorphic relationships are and how Rails handles them](
|
@@ -63,12 +80,14 @@ end
|
|
63
80
|
|
64
81
|
```ruby
|
65
82
|
class Picture < ActiveRecord::Base
|
83
|
+
# takes same additional options as belongs_to
|
66
84
|
belongs_to_polymorphic :employee, :product, :as => :imageable
|
67
85
|
validates_polymorph :imageable
|
68
86
|
end
|
69
87
|
|
70
88
|
class Employee < ActiveRecord::Base
|
71
|
-
|
89
|
+
# takes same additional options as has_many
|
90
|
+
has_many_as_polymorph :pictures, inverse_of: employee
|
72
91
|
end
|
73
92
|
|
74
93
|
class Product < ActiveRecord::Base
|
@@ -123,8 +142,6 @@ Now let's review what we've done.
|
|
123
142
|
|
124
143
|
* Currently the gem only supports MySQL. Please feel free to fork and submit a
|
125
144
|
(well-tested) pull request if you want to add Postgres support.
|
126
|
-
* This gem is tested and has been tested for Rails 2.3.8, 3.0.x, 3.1.x, 3.2.x,
|
127
|
-
and 4.0.0
|
128
145
|
* For Rails 3.1+, you'll still need to use `up` and `down` methods in your
|
129
146
|
migrations.
|
130
147
|
|
@@ -196,7 +213,7 @@ pic.polymorpheus.query_condition
|
|
196
213
|
|
197
214
|
* This gem was written by [Barun Singh](https://github.com/barunio)
|
198
215
|
* It uses the [Foreigner gem](https://github.com/matthuhiggins/foreigner) under
|
199
|
-
the hood for
|
216
|
+
the hood for Rails < 4.2.
|
200
217
|
|
201
218
|
polymorpheus is Copyright © 2011-2015 Barun Singh and [WegoWise](
|
202
219
|
http://wegowise.com). It is free software, and may be redistributed under the
|
data/lib/polymorpheus.rb
CHANGED
@@ -23,8 +23,16 @@ module Polymorpheus
|
|
23
23
|
module ConnectionAdapters
|
24
24
|
autoload :SchemaStatements, 'polymorpheus/schema_statements'
|
25
25
|
end
|
26
|
+
|
27
|
+
def self.require_foreigner?
|
28
|
+
ActiveRecord::VERSION::MAJOR < 5 &&
|
29
|
+
!(::ActiveRecord::VERSION::MAJOR >= 4 &&
|
30
|
+
::ActiveRecord::VERSION::MINOR >= 2)
|
31
|
+
end
|
26
32
|
end
|
27
33
|
|
34
|
+
require 'foreigner' if ::Polymorpheus.require_foreigner?
|
35
|
+
|
28
36
|
Polymorpheus::Adapter.register 'mysql2', 'polymorpheus/mysql_adapter'
|
29
37
|
Polymorpheus::Adapter.register 'postgresql', 'polymorpheus/postgresql_adapter'
|
30
38
|
|
@@ -2,9 +2,10 @@ module Polymorpheus
|
|
2
2
|
module Interface
|
3
3
|
module BelongsToPolymorphic
|
4
4
|
def belongs_to_polymorphic(*association_names, options)
|
5
|
-
polymorphic_api = options
|
5
|
+
polymorphic_api = options.delete(:as)
|
6
6
|
builder = Polymorpheus::InterfaceBuilder.new(polymorphic_api,
|
7
|
-
association_names
|
7
|
+
association_names,
|
8
|
+
options)
|
8
9
|
|
9
10
|
# The POLYMORPHEUS_ASSOCIATIONS constant is useful for two reasons:
|
10
11
|
#
|
@@ -20,7 +21,7 @@ module Polymorpheus
|
|
20
21
|
|
21
22
|
# Set belongs_to associations
|
22
23
|
builder.associations.each do |association|
|
23
|
-
belongs_to association.name.to_sym
|
24
|
+
belongs_to association.name.to_sym, association.options
|
24
25
|
end
|
25
26
|
|
26
27
|
# Exposed interface for introspection
|
@@ -6,10 +6,10 @@ module Polymorpheus
|
|
6
6
|
attr_reader :interface_name,
|
7
7
|
:associations
|
8
8
|
|
9
|
-
def initialize(interface_name, association_names)
|
9
|
+
def initialize(interface_name, association_names, options)
|
10
10
|
@interface_name = interface_name
|
11
11
|
@associations = association_names.map do |association_name|
|
12
|
-
Polymorpheus::InterfaceBuilder::Association.new(association_name)
|
12
|
+
Polymorpheus::InterfaceBuilder::Association.new(association_name, options)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
@@ -5,10 +5,11 @@ module Polymorpheus
|
|
5
5
|
include ActiveSupport::Inflector
|
6
6
|
|
7
7
|
attr_reader :name,
|
8
|
-
:key
|
8
|
+
:key,
|
9
|
+
:options
|
9
10
|
|
10
|
-
def initialize(name)
|
11
|
-
@name = name.to_s.downcase
|
11
|
+
def initialize(name, options)
|
12
|
+
@name, @options = name.to_s.downcase, options
|
12
13
|
@key = "#{@name}_id"
|
13
14
|
end
|
14
15
|
|
@@ -66,19 +66,22 @@ module Polymorpheus
|
|
66
66
|
if options[:unique].present?
|
67
67
|
poly_create_indexes(table, column_names, Array(options[:unique]))
|
68
68
|
end
|
69
|
+
|
69
70
|
column_names.each do |col_name|
|
70
71
|
ref_table, ref_col = columns[col_name].to_s.split('.')
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
72
|
+
fk_options = {
|
73
|
+
:column => col_name,
|
74
|
+
:name => "#{table}_#{col_name}_fk",
|
75
|
+
:primary_key => (ref_col || 'id' )
|
76
|
+
}.merge(generate_constraints(options))
|
77
|
+
add_foreign_key(table, ref_table, fk_options)
|
75
78
|
end
|
76
79
|
end
|
77
80
|
|
78
81
|
def remove_polymorphic_constraints(table, columns, options = {})
|
79
82
|
poly_drop_triggers(table, columns.keys.sort)
|
80
83
|
columns.each do |(col, reference)|
|
81
|
-
remove_foreign_key table, :column => col
|
84
|
+
remove_foreign_key table, :column => col, :name => "#{table}_#{col}_fk"
|
82
85
|
end
|
83
86
|
if options[:unique].present?
|
84
87
|
poly_remove_indexes(table, columns.keys, Array(options[:unique]))
|
@@ -86,7 +89,7 @@ module Polymorpheus
|
|
86
89
|
end
|
87
90
|
|
88
91
|
def triggers
|
89
|
-
execute("show triggers").collect {|t| Trigger.new(t) }
|
92
|
+
execute("show triggers").collect {|t| Polymorpheus::Trigger.new(t) }
|
90
93
|
end
|
91
94
|
|
92
95
|
#
|
@@ -191,26 +194,8 @@ module Polymorpheus
|
|
191
194
|
end
|
192
195
|
|
193
196
|
def generate_constraints(options)
|
194
|
-
|
195
|
-
|
196
|
-
['delete', 'update'].each do |event|
|
197
|
-
option = "on_#{event}".to_sym
|
198
|
-
next unless options.has_key?(option) &&
|
199
|
-
options[option].respond_to?(:to_sym)
|
200
|
-
|
201
|
-
action = case options[option].to_sym
|
202
|
-
when :nullify then 'SET NULL'
|
203
|
-
when :cascade then 'CASCADE'
|
204
|
-
when :restrict then 'RESTRICT'
|
205
|
-
end
|
206
|
-
next unless action
|
207
|
-
|
208
|
-
constraints << "ON #{event.upcase} #{action}"
|
209
|
-
end
|
210
|
-
|
211
|
-
constraints.join(' ')
|
197
|
+
options.slice(:on_delete, :on_update)
|
212
198
|
end
|
213
|
-
|
214
199
|
end
|
215
200
|
end
|
216
201
|
end
|
@@ -223,3 +208,8 @@ end
|
|
223
208
|
rescue
|
224
209
|
end
|
225
210
|
end
|
211
|
+
|
212
|
+
if ::Polymorpheus.require_foreigner?
|
213
|
+
require 'foreigner/connection_adapters/mysql2_adapter'
|
214
|
+
require 'polymorpheus/mysql_adapter/foreigner_constraints'
|
215
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Polymorpheus
|
2
|
+
module ConnectionAdapters
|
3
|
+
module MysqlAdapter
|
4
|
+
def generate_constraints(options)
|
5
|
+
constraints = []
|
6
|
+
|
7
|
+
['delete', 'update'].each do |event|
|
8
|
+
option = "on_#{event}".to_sym
|
9
|
+
next unless options.has_key?(option) &&
|
10
|
+
options[option].respond_to?(:to_sym)
|
11
|
+
|
12
|
+
action = case options[option].to_sym
|
13
|
+
when :nullify then 'SET NULL'
|
14
|
+
when :cascade then 'CASCADE'
|
15
|
+
when :restrict then 'RESTRICT'
|
16
|
+
else
|
17
|
+
fail ArgumentError, <<-EOS
|
18
|
+
'#{options[option]}' is not supported for :on_update or :on_delete.
|
19
|
+
Supported values are: :nullify, :cascade, :restrict
|
20
|
+
EOS
|
21
|
+
end
|
22
|
+
|
23
|
+
constraints << "ON #{event.upcase} #{action}"
|
24
|
+
end
|
25
|
+
|
26
|
+
{ :options => constraints.join(' ') }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -3,14 +3,15 @@ module Polymorpheus
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
|
6
|
+
alias_method :tables_without_triggers, :tables
|
7
|
+
alias_method :tables, :tables_with_triggers
|
7
8
|
end
|
8
9
|
|
9
10
|
def tables_with_triggers(stream)
|
10
11
|
tables_without_triggers(stream)
|
11
12
|
|
12
13
|
if @connection.respond_to?(:triggers)
|
13
|
-
@connection.triggers.collect(&:schema_statement).each do |statement|
|
14
|
+
@connection.triggers.collect(&:schema_statement).uniq.each do |statement|
|
14
15
|
stream.puts statement
|
15
16
|
end
|
16
17
|
end
|
data/lib/polymorpheus/trigger.rb
CHANGED
@@ -1,29 +1,30 @@
|
|
1
|
-
|
1
|
+
module Polymorpheus
|
2
|
+
class Trigger
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
attr_accessor :name, :event, :table, :statement, :timing, :created, :sql_mode,
|
5
|
+
:definer, :charset, :collation_connection, :db_collation
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
def initialize(arr)
|
8
|
+
raise ArgumentError unless arr.is_a?(Array) && arr.length == 11
|
9
|
+
[:name, :event, :table, :statement, :timing, :created, :sql_mode,
|
10
|
+
:definer, :charset, :collation_connection, :db_collation].
|
11
|
+
each_with_index do |attr, ind|
|
12
|
+
self.send("#{attr}=", arr[ind])
|
13
|
+
end
|
12
14
|
end
|
13
|
-
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
def columns
|
17
|
+
/IF\((.*)\) \<\> 1/.match(self.statement) do |match|
|
18
|
+
match[1].split(' + ').collect do |submatch|
|
19
|
+
/NEW\.([^ ]*)/.match(submatch)[1]
|
20
|
+
end
|
19
21
|
end
|
20
22
|
end
|
21
|
-
end
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
def schema_statement
|
25
|
+
# note that we don't need to worry about unique indices or foreign keys
|
26
|
+
# because separate schema statements will be generated for them
|
27
|
+
" add_polymorphic_triggers(:#{table}, #{columns.to_s})"
|
28
|
+
end
|
27
29
|
end
|
28
|
-
|
29
30
|
end
|
data/lib/polymorpheus/version.rb
CHANGED
data/polymorpheus.gemspec
CHANGED
@@ -17,10 +17,8 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.extra_rdoc_files = ["README.md", "LICENSE.txt"]
|
18
18
|
s.license = 'MIT'
|
19
19
|
|
20
|
-
s.add_dependency('
|
21
|
-
s.add_dependency('activerecord', '>= 3.2', '< 4.2')
|
20
|
+
s.add_dependency('activerecord', '>= 3.2', '< 6.1')
|
22
21
|
|
23
|
-
s.add_development_dependency('rake', '~>
|
24
|
-
s.add_development_dependency('rspec
|
25
|
-
s.add_development_dependency('mysql2', '~> 0.3.10')
|
22
|
+
s.add_development_dependency('rake', '~> 12.3.3')
|
23
|
+
s.add_development_dependency('rspec', '~> 3.9.0')
|
26
24
|
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Polymorpheus::Interface::BelongsToPolymorphic do
|
4
|
+
let(:hero) { Hero.create! }
|
5
|
+
let(:villain) { Villain.create! }
|
6
|
+
let(:superhero) { Superhero.create! }
|
7
|
+
let(:alien_demigod) { AlienDemigod.create! }
|
8
|
+
|
9
|
+
before do
|
10
|
+
create_table :story_arcs do |t|
|
11
|
+
t.references :hero
|
12
|
+
t.references :villain
|
13
|
+
end
|
14
|
+
create_table :heros
|
15
|
+
create_table :villains
|
16
|
+
end
|
17
|
+
|
18
|
+
specify do
|
19
|
+
expect(StoryArc::POLYMORPHEUS_ASSOCIATIONS).to eq(%w[hero villain])
|
20
|
+
end
|
21
|
+
specify do
|
22
|
+
expect(Superpower::POLYMORPHEUS_ASSOCIATIONS).to eq(%w[superhero supervillain])
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "setter methods for ActiveRecord objects" do
|
26
|
+
let(:story_arc) { StoryArc.new(attributes) }
|
27
|
+
let(:attributes) { {} }
|
28
|
+
|
29
|
+
it "sets the correct attribute value for the setter" do
|
30
|
+
story_arc.character = hero
|
31
|
+
expect(story_arc.hero_id).to eq(hero.id)
|
32
|
+
expect(story_arc.villain_id).to eq(nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "sets competing associations to nil" do
|
36
|
+
story_arc.character = hero
|
37
|
+
expect(story_arc.hero_id).to eq(hero.id)
|
38
|
+
story_arc.character = villain
|
39
|
+
expect(story_arc.villain_id).to eq(villain.id)
|
40
|
+
expect(story_arc.hero_id).to eq(nil)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "throws an error if the assigned object isn't a valid type" do
|
44
|
+
create_table :trees
|
45
|
+
|
46
|
+
tree = Tree.create!
|
47
|
+
expect { story_arc.character = tree }.to raise_error(
|
48
|
+
Polymorpheus::Interface::InvalidTypeError,
|
49
|
+
"Invalid type. Must be one of {hero, villain}"
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "does not throw an error if the assigned object is a subclass of a
|
54
|
+
valid type" do
|
55
|
+
expect { story_arc.character = superhero }.not_to raise_error
|
56
|
+
expect(story_arc.hero_id).to eq(superhero.id)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "does not throw an error if the assigned object is a descendant of a
|
60
|
+
valid type" do
|
61
|
+
expect { story_arc.character = alien_demigod }.not_to raise_error
|
62
|
+
expect(story_arc.hero_id).to eq(alien_demigod.id)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "setter methods for objects inheriting from ActiveRecord objects" do
|
67
|
+
let(:superpower) { Superpower.new }
|
68
|
+
|
69
|
+
before do
|
70
|
+
create_table :superpowers do |t|
|
71
|
+
t.references :superhero
|
72
|
+
t.references :supervillain
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "throws an error if the assigned object is an instance of the parent
|
77
|
+
ActiveRecord class" do
|
78
|
+
expect { superpower.wielder = hero }.to raise_error(
|
79
|
+
Polymorpheus::Interface::InvalidTypeError,
|
80
|
+
"Invalid type. Must be one of {superhero, supervillain}"
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "works if the assigned object is of the specified class" do
|
85
|
+
expect { superpower.wielder = superhero }.not_to raise_error
|
86
|
+
expect(superpower.superhero_id).to eq(superhero.id)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "works if the assigned object is an instance of a child class" do
|
90
|
+
expect { superpower.wielder = alien_demigod }.not_to raise_error
|
91
|
+
expect(superpower.superhero_id).to eq(alien_demigod.id)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe '#polymorpheus exposed interface method' do
|
96
|
+
subject(:interface) { story_arc.polymorpheus }
|
97
|
+
|
98
|
+
context 'when there is no relationship defined' do
|
99
|
+
let(:story_arc) { StoryArc.new }
|
100
|
+
|
101
|
+
specify do
|
102
|
+
expect(interface.associations).to match_associations(:hero, :villain)
|
103
|
+
end
|
104
|
+
specify { expect(interface.active_association).to eq nil }
|
105
|
+
specify { expect(interface.query_condition).to eq nil }
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'when there is are multiple relationships defined' do
|
109
|
+
let(:story_arc) { StoryArc.new(hero_id: hero.id, villain_id: villain.id) }
|
110
|
+
|
111
|
+
specify do
|
112
|
+
expect(interface.associations).to match_associations(:hero, :villain)
|
113
|
+
end
|
114
|
+
specify { expect(interface.active_association).to eq nil }
|
115
|
+
specify { expect(interface.query_condition).to eq nil }
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'when there is one relationship defined through the id value' do
|
119
|
+
let(:story_arc) { StoryArc.new(hero_id: hero.id) }
|
120
|
+
|
121
|
+
specify do
|
122
|
+
expect(interface.associations).to match_associations(:hero, :villain)
|
123
|
+
end
|
124
|
+
specify { expect(interface.active_association).to be_association(:hero) }
|
125
|
+
specify { expect(interface.query_condition).to eq('hero_id' => hero.id) }
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'when there is one relationship defined through the setter' do
|
129
|
+
let(:story_arc) { StoryArc.new(character: hero) }
|
130
|
+
|
131
|
+
specify do
|
132
|
+
expect(interface.associations).to match_associations(:hero, :villain)
|
133
|
+
end
|
134
|
+
specify { expect(interface.active_association).to be_association(:hero) }
|
135
|
+
specify { expect(interface.query_condition).to eq('hero_id' => hero.id) }
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'when there is one association, to a new record' do
|
139
|
+
let(:new_hero) { Hero.new }
|
140
|
+
let(:story_arc) { StoryArc.new(character: new_hero) }
|
141
|
+
|
142
|
+
specify do
|
143
|
+
expect(interface.associations).to match_associations(:hero, :villain)
|
144
|
+
end
|
145
|
+
specify { expect(interface.active_association).to be_association(:hero) }
|
146
|
+
specify { expect(interface.query_condition).to eq nil }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|