dm-constraints 0.9.9 → 0.9.10
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.
- data/History.txt +6 -0
- data/Manifest.txt +1 -0
- data/lib/dm-constraints/data_objects_adapter.rb +15 -4
- data/lib/dm-constraints/delete_constraint.rb +59 -0
- data/lib/dm-constraints/version.rb +1 -1
- data/lib/dm-constraints.rb +41 -1
- data/spec/integration/constraints_spec.rb +173 -5
- data/spec/spec_helper.rb +1 -1
- data/tasks/spec.rb +1 -1
- metadata +4 -3
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
@@ -13,7 +13,18 @@ module DataMapper
|
|
13
13
|
foreign_table = parent.storage_name(repository_name)
|
14
14
|
foreign_keys = parent.key.map { |key| property_to_column_name(parent.repository(repository_name), key, false) }
|
15
15
|
|
16
|
-
|
16
|
+
one_to_many_relationship = parent.relationships.values.select { |rel| rel.child_model == model }.first
|
17
|
+
delete_constraint_type = case one_to_many_relationship.delete_constraint
|
18
|
+
when :protect, nil
|
19
|
+
"NO ACTION"
|
20
|
+
when :destroy, :destroy!
|
21
|
+
"CASCADE"
|
22
|
+
when :set_nil
|
23
|
+
"SET NULL"
|
24
|
+
when :skip
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
create_constraints_statement(table_name, constraint_name, keys, foreign_table, foreign_keys, delete_constraint_type) if delete_constraint_type
|
17
28
|
end.compact
|
18
29
|
end
|
19
30
|
|
@@ -29,14 +40,14 @@ module DataMapper
|
|
29
40
|
|
30
41
|
private
|
31
42
|
|
32
|
-
def create_constraints_statement(table_name, constraint_name, keys, foreign_table, foreign_keys)
|
43
|
+
def create_constraints_statement(table_name, constraint_name, keys, foreign_table, foreign_keys, delete_constraint_type)
|
33
44
|
<<-EOS.compress_lines
|
34
45
|
ALTER TABLE #{quote_table_name(table_name)}
|
35
46
|
ADD CONSTRAINT #{quote_constraint_name(constraint_name)}
|
36
47
|
FOREIGN KEY (#{keys * ', '})
|
37
48
|
REFERENCES #{quote_table_name(foreign_table)} (#{foreign_keys * ', '})
|
38
|
-
ON DELETE
|
39
|
-
ON UPDATE
|
49
|
+
ON DELETE #{delete_constraint_type}
|
50
|
+
ON UPDATE #{delete_constraint_type}
|
40
51
|
EOS
|
41
52
|
end
|
42
53
|
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Constraints
|
3
|
+
module DeleteConstraint
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
DELETE_CONSTRAINT_OPTIONS = [:protect, :destroy, :destroy!, :set_nil, :skip]
|
11
|
+
def check_delete_constraint_type(cardinality, name, options = {})
|
12
|
+
constraint_type = options[:constraint]
|
13
|
+
return if constraint_type.nil?
|
14
|
+
delete_constraint_options = DELETE_CONSTRAINT_OPTIONS.map { |o| ":#{o}" }
|
15
|
+
if !DELETE_CONSTRAINT_OPTIONS.include?(constraint_type)
|
16
|
+
raise ArgumentError, ":constraint option must be one of #{delete_constraint_options * ', '}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO: that should be moved to a 'util-like' module
|
21
|
+
def with_changed_method_visibility(method, from_visibility, to_visibility, &block)
|
22
|
+
send(to_visibility, method)
|
23
|
+
yield
|
24
|
+
send(from_visibility, method)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_delete_constraint_option(name, repository_name, child_model, parent_model, options = {})
|
30
|
+
@delete_constraint = options[:constraint]
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_delete_constraints
|
34
|
+
model.relationships.each do |rel_name, rel|
|
35
|
+
children = self.send(rel_name)
|
36
|
+
case rel.delete_constraint
|
37
|
+
when nil, :protect
|
38
|
+
# only prevent deletion if the resource is a parent in a relationship and has children
|
39
|
+
throw(:halt, false) if children && children.respond_to?(:empty?) && !children.empty?
|
40
|
+
when :destroy
|
41
|
+
if children && children.respond_to?(:each)
|
42
|
+
children.each { |child| child.destroy }
|
43
|
+
end
|
44
|
+
when :set_nil
|
45
|
+
if children && children.respond_to?(:each)
|
46
|
+
children.each do |child|
|
47
|
+
child.class.many_to_one_relationships.each do |mto_rel|
|
48
|
+
child.send("#{mto_rel.name}=", nil) if child.send(mto_rel.name).eql?(self)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end # case
|
53
|
+
end # relationships
|
54
|
+
end # check_delete_constraints
|
55
|
+
|
56
|
+
|
57
|
+
end # DeleteConstraint
|
58
|
+
end # Constraints
|
59
|
+
end # DataMapper
|
data/lib/dm-constraints.rb
CHANGED
@@ -3,15 +3,55 @@ require 'rubygems'
|
|
3
3
|
require 'pathname'
|
4
4
|
|
5
5
|
# Add all external dependencies for the plugin here
|
6
|
-
gem 'dm-core', '~>0.9.
|
6
|
+
gem 'dm-core', '~>0.9.10'
|
7
7
|
require 'dm-core'
|
8
8
|
|
9
9
|
# Require plugin-files
|
10
10
|
require Pathname(__FILE__).dirname.expand_path / 'dm-constraints' / 'data_objects_adapter'
|
11
11
|
require Pathname(__FILE__).dirname.expand_path / 'dm-constraints' / 'postgres_adapter'
|
12
12
|
require Pathname(__FILE__).dirname.expand_path / 'dm-constraints' / 'mysql_adapter'
|
13
|
+
require Pathname(__FILE__).dirname.expand_path / 'dm-constraints' / 'delete_constraint'
|
13
14
|
|
14
15
|
module DataMapper
|
16
|
+
module Associations
|
17
|
+
class Relationship
|
18
|
+
include Extlib::Hook
|
19
|
+
include DataMapper::Constraints::DeleteConstraint
|
20
|
+
|
21
|
+
attr_reader :delete_constraint
|
22
|
+
OPTIONS << :constraint
|
23
|
+
|
24
|
+
# initialize is a private method in Relationship
|
25
|
+
# and private methods can not be "advised" (hooked into)
|
26
|
+
# in extlib.
|
27
|
+
with_changed_method_visibility(:initialize, :private, :public) do
|
28
|
+
before :initialize, :add_delete_constraint_option
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module DataMapper
|
35
|
+
module Constraints
|
36
|
+
|
37
|
+
include DeleteConstraint
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
include DeleteConstraint::ClassMethods
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.included(model)
|
44
|
+
model.extend(ClassMethods)
|
45
|
+
model.class_eval do
|
46
|
+
before_class_method :has, :check_delete_constraint_type
|
47
|
+
if method_defined?(:destroy)
|
48
|
+
before :destroy, :check_delete_constraints
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
15
55
|
class AutoMigrator
|
16
56
|
include Extlib::Hook
|
17
57
|
include DataMapper::Constraints::DataObjectsAdapter::Migration
|
@@ -5,11 +5,14 @@ ADAPTERS.each do |adapter|
|
|
5
5
|
|
6
6
|
describe 'DataMapper::Constraints' do
|
7
7
|
|
8
|
+
# load_models_for_metaphor :stable, :farmer, :cow
|
9
|
+
|
8
10
|
before do
|
9
11
|
DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[adapter]
|
10
12
|
|
11
|
-
class Stable
|
13
|
+
class ::Stable
|
12
14
|
include DataMapper::Resource
|
15
|
+
include DataMapper::Constraints
|
13
16
|
|
14
17
|
property :id, Serial
|
15
18
|
property :location, String
|
@@ -18,8 +21,9 @@ ADAPTERS.each do |adapter|
|
|
18
21
|
has n, :cows
|
19
22
|
end
|
20
23
|
|
21
|
-
class Farmer
|
24
|
+
class ::Farmer
|
22
25
|
include DataMapper::Resource
|
26
|
+
include DataMapper::Constraints
|
23
27
|
|
24
28
|
property :first_name, String, :key => true
|
25
29
|
property :last_name, String, :key => true
|
@@ -27,7 +31,7 @@ ADAPTERS.each do |adapter|
|
|
27
31
|
has n, :cows
|
28
32
|
end
|
29
33
|
|
30
|
-
class Cow
|
34
|
+
class ::Cow
|
31
35
|
include DataMapper::Resource
|
32
36
|
include DataMapper::Constraints
|
33
37
|
|
@@ -61,5 +65,169 @@ ADAPTERS.each do |adapter|
|
|
61
65
|
lambda { @c1 = Cow.create(:name => "Bea", :stable_id => s.id + 1) }.should raise_error
|
62
66
|
end
|
63
67
|
|
64
|
-
|
65
|
-
|
68
|
+
# :constraint associations
|
69
|
+
# value | on deletion of parent...
|
70
|
+
# ---------------------------------
|
71
|
+
# :protect | raises exception if there are child records
|
72
|
+
# :destroy | deletes children
|
73
|
+
# :destroy! | deletes children directly without instantiating the resource, bypassing any hooks
|
74
|
+
# :set_nil | sets parent id to nil in child associations
|
75
|
+
# :skip | does not do anything with children (they'll become orphan records)
|
76
|
+
|
77
|
+
describe "constraint options" do
|
78
|
+
describe "when no constraint options are given" do
|
79
|
+
|
80
|
+
it "should destroy the parent if there are no children in the association" do
|
81
|
+
@f1 = Farmer.create(:first_name => "John", :last_name => "Doe")
|
82
|
+
@f2 = Farmer.create(:first_name => "Some", :last_name => "Body")
|
83
|
+
@c1 = Cow.create(:name => "Bea", :farmer => @f2)
|
84
|
+
@f1.destroy.should == true
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should not destroy the parent if there are children in the association" do
|
88
|
+
@f = Farmer.create(:first_name => "John", :last_name => "Doe")
|
89
|
+
@c1 = Cow.create(:name => "Bea", :farmer => @f)
|
90
|
+
@f.destroy.should == false
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "when :constraint => :protect is given" do
|
96
|
+
before do
|
97
|
+
class ::Farmer
|
98
|
+
has n, :cows, :constraint => :protect
|
99
|
+
end
|
100
|
+
class ::Cow
|
101
|
+
belongs_to :farmer
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should destroy the parent if there are no children in the association" do
|
106
|
+
@f1 = Farmer.create(:first_name => "John", :last_name => "Doe")
|
107
|
+
@f2 = Farmer.create(:first_name => "Some", :last_name => "Body")
|
108
|
+
@c1 = Cow.create(:name => "Bea", :farmer => @f2)
|
109
|
+
@f1.destroy.should == true
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should not destroy the parent if there are children in the association" do
|
113
|
+
@f = Farmer.create(:first_name => "John", :last_name => "Doe")
|
114
|
+
@c1 = Cow.create(:name => "Bea", :farmer => @f)
|
115
|
+
@f.destroy.should == false
|
116
|
+
end
|
117
|
+
|
118
|
+
it "the child should be destroyable" do
|
119
|
+
@f = Farmer.create(:first_name => "John", :last_name => "Doe")
|
120
|
+
@c = Cow.create(:name => "Bea", :farmer => @f)
|
121
|
+
@c.destroy.should == true
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "when :constraint => :destroy is given" do
|
127
|
+
before do
|
128
|
+
class ::Farmer
|
129
|
+
has n, :cows, :constraint => :destroy
|
130
|
+
end
|
131
|
+
class ::Cow
|
132
|
+
belongs_to :farmer
|
133
|
+
end
|
134
|
+
DataMapper.auto_migrate!
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should destroy the parent and the children, too" do
|
138
|
+
#NOTE: the repository wrapper is needed in order for
|
139
|
+
# the identity map to work (otherwise @c1 in the below two calls
|
140
|
+
# would refer to different instances)
|
141
|
+
repository do
|
142
|
+
@f = Farmer.create(:first_name => "John", :last_name => "Doe")
|
143
|
+
@c1 = Cow.create(:name => "Bea", :farmer => @f)
|
144
|
+
@c2 = Cow.create(:name => "Riksa", :farmer => @f)
|
145
|
+
@f.destroy.should == true
|
146
|
+
@f.should be_new_record
|
147
|
+
@c1.should be_new_record
|
148
|
+
@c2.should be_new_record
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
it "the child should be destroyable" do
|
153
|
+
@f = Farmer.create(:first_name => "John", :last_name => "Doe")
|
154
|
+
@c = Cow.create(:name => "Bea", :farmer => @f)
|
155
|
+
@c.destroy.should == true
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
describe "when :constraint => :set_nil is given" do
|
161
|
+
before do
|
162
|
+
class ::Farmer
|
163
|
+
has n, :cows, :constraint => :set_nil
|
164
|
+
end
|
165
|
+
class ::Cow
|
166
|
+
belongs_to :farmer
|
167
|
+
end
|
168
|
+
DataMapper.auto_migrate!
|
169
|
+
end
|
170
|
+
|
171
|
+
it "destroying the parent should set children foreign keys to nil" do
|
172
|
+
@f = Farmer.create(:first_name => "John", :last_name => "Doe")
|
173
|
+
@c1 = Cow.create(:name => "Bea", :farmer => @f)
|
174
|
+
@c2 = Cow.create(:name => "Riksa", :farmer => @f)
|
175
|
+
cows = @f.cows
|
176
|
+
@f.destroy.should == true
|
177
|
+
cows.all? { |cow| cow.farmer.should be_nil }
|
178
|
+
end
|
179
|
+
|
180
|
+
it "the child should be destroyable" do
|
181
|
+
@f = Farmer.create(:first_name => "John", :last_name => "Doe")
|
182
|
+
@c = Cow.create(:name => "Bea", :farmer => @f)
|
183
|
+
@c.destroy.should == true
|
184
|
+
end
|
185
|
+
|
186
|
+
end # describe
|
187
|
+
|
188
|
+
describe "when :constraint => :skip is given" do
|
189
|
+
before do
|
190
|
+
class ::Farmer
|
191
|
+
has n, :cows, :constraint => :skip
|
192
|
+
end
|
193
|
+
class ::Cow
|
194
|
+
belongs_to :farmer
|
195
|
+
end
|
196
|
+
DataMapper.auto_migrate!
|
197
|
+
end
|
198
|
+
|
199
|
+
it "destroying the parent should be allowed, children should become orphan records" do
|
200
|
+
@f = Farmer.create(:first_name => "John", :last_name => "Doe")
|
201
|
+
@c1 = Cow.create(:name => "Bea", :farmer => @f)
|
202
|
+
@c2 = Cow.create(:name => "Riksa", :farmer => @f)
|
203
|
+
@f.destroy.should == true
|
204
|
+
@c1.farmer.should be_new_record
|
205
|
+
@c2.farmer.should be_new_record
|
206
|
+
end
|
207
|
+
|
208
|
+
it "the child should be destroyable" do
|
209
|
+
@f = Farmer.create(:first_name => "John", :last_name => "Doe")
|
210
|
+
@c = Cow.create(:name => "Bea", :farmer => @f)
|
211
|
+
@c.destroy.should == true
|
212
|
+
end
|
213
|
+
|
214
|
+
end # describe
|
215
|
+
|
216
|
+
describe "when an invalid option is given" do
|
217
|
+
before do
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should raise an error" do
|
221
|
+
lambda do
|
222
|
+
class ::Farmer
|
223
|
+
has n, :cows, :constraint => :chocolate
|
224
|
+
end
|
225
|
+
end.should raise_error(ArgumentError)
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
end # describe 'constraint options'
|
231
|
+
|
232
|
+
end # DataMapper::Constraints
|
233
|
+
end # ADAPTERS.each
|
data/spec/spec_helper.rb
CHANGED
data/tasks/spec.rb
CHANGED
@@ -8,7 +8,7 @@ begin
|
|
8
8
|
desc 'Run specifications'
|
9
9
|
Spec::Rake::SpecTask.new(:spec) do |t|
|
10
10
|
t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
|
11
|
-
t.spec_files = Pathname.glob((ROOT + 'spec/**/*_spec.rb').to_s)
|
11
|
+
t.spec_files = Pathname.glob((ROOT + 'spec/**/*_spec.rb').to_s).map { |f| f.to_s }
|
12
12
|
|
13
13
|
begin
|
14
14
|
gem 'rcov', '~>0.8'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dm-constraints
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dirkjan Bussink
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-01-
|
12
|
+
date: 2009-01-19 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -20,7 +20,7 @@ dependencies:
|
|
20
20
|
requirements:
|
21
21
|
- - ~>
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version: 0.9.
|
23
|
+
version: 0.9.10
|
24
24
|
version:
|
25
25
|
description: DataMapper plugin constraining relationships
|
26
26
|
email:
|
@@ -43,6 +43,7 @@ files:
|
|
43
43
|
- TODO
|
44
44
|
- lib/dm-constraints.rb
|
45
45
|
- lib/dm-constraints/data_objects_adapter.rb
|
46
|
+
- lib/dm-constraints/delete_constraint.rb
|
46
47
|
- lib/dm-constraints/mysql_adapter.rb
|
47
48
|
- lib/dm-constraints/postgres_adapter.rb
|
48
49
|
- lib/dm-constraints/version.rb
|