dm-constraints 0.9.9 → 0.9.10

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ === 0.9.10 / 2009-01-19
2
+
3
+ * 1 major enhancement:
4
+
5
+ * Add :protect, :destroy, :set_nil and :skip constraints
6
+
1
7
  === 0.9.9 / 2009-01-04
2
8
 
3
9
  * No changes this version
data/Manifest.txt CHANGED
@@ -6,6 +6,7 @@ Rakefile
6
6
  TODO
7
7
  lib/dm-constraints.rb
8
8
  lib/dm-constraints/data_objects_adapter.rb
9
+ lib/dm-constraints/delete_constraint.rb
9
10
  lib/dm-constraints/mysql_adapter.rb
10
11
  lib/dm-constraints/postgres_adapter.rb
11
12
  lib/dm-constraints/version.rb
@@ -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
- create_constraints_statement(table_name, constraint_name, keys, foreign_table, foreign_keys)
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 NO ACTION
39
- ON UPDATE NO ACTION
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
@@ -1,5 +1,5 @@
1
1
  module DataMapper
2
2
  module Constraints
3
- VERSION = '0.9.9'
3
+ VERSION = '0.9.10'
4
4
  end
5
5
  end
@@ -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.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
- end
65
- end
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
@@ -4,7 +4,7 @@ require 'rubygems'
4
4
  gem 'rspec', '~>1.1.11'
5
5
  require 'spec'
6
6
 
7
- gem 'dm-core', '~>0.9.9'
7
+ gem 'dm-core', '~>0.9.10'
8
8
  require 'dm-core'
9
9
 
10
10
  ADAPTERS = []
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.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-04 00:00:00 -08:00
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.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