dm-constraints 0.9.10 → 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,13 @@
1
+ === 0.9.11 / 2009-03-29
2
+
3
+ * 5 major enhancements:
4
+
5
+ * Added :destroy! constraints
6
+ * Added support for 1:1 constraints
7
+ * Added support for M:M constraints
8
+ * Add'l rspecs
9
+ * Updated readme.txt
10
+
1
11
  === 0.9.10 / 2009-01-19
2
12
 
3
13
  * 1 major enhancement:
data/README.txt CHANGED
@@ -2,3 +2,51 @@
2
2
 
3
3
  Plugin that adds foreign key constraints to associations.
4
4
  Currently supports only PostgreSQL and MySQL
5
+
6
+ All constraints are added to the underlying database, but constraining is implemented in
7
+ pure ruby.
8
+
9
+
10
+ === Constraints
11
+
12
+ - :protect returns false on destroy if there are child records
13
+ - :destroy deletes children if present
14
+ - :destroy! deletes children directly without instantiating the resource, bypassing any hooks
15
+ Does not support 1:1 Relationships as #destroy! is not supported on Resource in dm-master
16
+ - :set_nil sets parent id to nil in child associations
17
+ Not valid for M:M relationships as duplicate records could be created (see explanation in specs)
18
+ - :skip Does nothing with children, results in orphaned records
19
+
20
+ By default a relationship will PROTECT its children.
21
+
22
+
23
+ === Cardinality Notes
24
+ * 1:1
25
+ * Applicable constraints: [:set_nil, :skip, :protect, :destroy]
26
+
27
+ * 1:M
28
+ * Applicable constraints: [:set_nil, :skip, :protect, :destroy, :destroy!]
29
+
30
+ * M:M
31
+ * Applicable constraints: [:skip, :protect, :destroy, :destroy!]
32
+
33
+
34
+ === Examples
35
+
36
+ # 1:M Example
37
+ class Farmer
38
+ has n, :pigs #equivalent to: has n, :pigs, :constraint => :protect
39
+ end
40
+
41
+ # M:M Example
42
+ class Articles
43
+ has n, :tags, :through => Resource, :constraint => :destroy
44
+ end
45
+ class Tags
46
+ has n, :articles, :through => Resource, :constraint => :destroy
47
+ end
48
+
49
+ # 1:1 Example
50
+ class Farmer
51
+ has 1, :beloved_sheep, :constraint => :protect
52
+ end
data/Rakefile CHANGED
@@ -12,7 +12,7 @@ AUTHOR = 'Dirkjan Bussink'
12
12
  EMAIL = 'd.bussink [a] gmail [d] com'
13
13
  GEM_NAME = 'dm-constraints'
14
14
  GEM_VERSION = DataMapper::Constraints::VERSION
15
- GEM_DEPENDENCIES = [['dm-core', "~>#{GEM_VERSION}"]]
15
+ GEM_DEPENDENCIES = [['dm-core', GEM_VERSION]]
16
16
  GEM_CLEAN = %w[ log pkg coverage ]
17
17
  GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.txt LICENSE TODO History.txt ] }
18
18
 
@@ -2,8 +2,26 @@ module DataMapper
2
2
  module Constraints
3
3
  module DataObjectsAdapter
4
4
  module SQL
5
+
6
+ ##
7
+ # generates all foreign key create constraint statements for valid relationships
8
+ # given repository and a model
9
+ #
10
+ # This wraps calls to create_constraints_statement
11
+ #
12
+ # @see #create_constraints_statement
13
+ #
14
+ # @param repository_name [Symbol] Name of the repository to constrain
15
+ #
16
+ # @param model [DataMapper::Model] Model to constrain
17
+ #
18
+ # @return [Array[String]] List of statements to create constraints
19
+ #
20
+ #
21
+ # @api public
5
22
  def create_constraints_statements(repository_name, model)
6
23
  model.many_to_one_relationships.map do |relationship|
24
+
7
25
  table_name = model.storage_name(repository_name)
8
26
  constraint_name = constraint_name(table_name, relationship.name)
9
27
  next if constraint_exists?(table_name, constraint_name)
@@ -13,8 +31,16 @@ module DataMapper
13
31
  foreign_table = parent.storage_name(repository_name)
14
32
  foreign_keys = parent.key.map { |key| property_to_column_name(parent.repository(repository_name), key, false) }
15
33
 
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
34
+ #Anonymous relationshps for :through => Resource
35
+ one_to_many_relationship = parent.relationships.values.select { |rel|
36
+ rel.options[:near_relationship_name] == Extlib::Inflection.tableize(model.name).to_sym
37
+ }.first
38
+
39
+ one_to_many_relationship ||= parent.relationships.values.select { |rel|
40
+ rel.child_model == model
41
+ }.first
42
+
43
+ delete_constraint_type = case one_to_many_relationship.nil? ? :protect : one_to_many_relationship.delete_constraint
18
44
  when :protect, nil
19
45
  "NO ACTION"
20
46
  when :destroy, :destroy!
@@ -24,10 +50,27 @@ module DataMapper
24
50
  when :skip
25
51
  nil
26
52
  end
53
+
27
54
  create_constraints_statement(table_name, constraint_name, keys, foreign_table, foreign_keys, delete_constraint_type) if delete_constraint_type
28
55
  end.compact
29
56
  end
30
57
 
58
+ ##
59
+ # generates all foreign key destroy constraint statements for valid relationships
60
+ # given repository and a model
61
+ #
62
+ # This wraps calls to destroy_constraints_statement
63
+ #
64
+ # @see #destroy_constraints_statement
65
+ #
66
+ # @param repository_name [Symbol] Name of the repository to constrain
67
+ #
68
+ # @param model [DataMapper::Model] Model to constrain
69
+ #
70
+ # @return [Array[String]] List of statements to destroy constraints
71
+ #
72
+ #
73
+ # @api public
31
74
  def destroy_constraints_statements(repository_name, model)
32
75
  model.many_to_one_relationships.map do |relationship|
33
76
  table_name = model.storage_name(repository_name)
@@ -35,11 +78,30 @@ module DataMapper
35
78
  next unless constraint_exists?(table_name, constraint_name)
36
79
 
37
80
  destroy_constraints_statement(table_name, constraint_name)
81
+
38
82
  end.compact
39
83
  end
40
84
 
41
85
  private
42
86
 
87
+ ##
88
+ # Generates the SQL statement to create a constraint
89
+ #
90
+ # @param table_name [String] name of table to constrain
91
+ #
92
+ # @param constraint_name [String] name of foreign key constraint
93
+ #
94
+ # @param keys [Array[String]] keys that refer to another table
95
+ #
96
+ # @param foreign_table [String] table fk refers to
97
+ #
98
+ # @param foreign_keys [Array[String]] keys on foreign table that constraint refers to
99
+ #
100
+ # @param delete_constraint_type [String] the constraint to add to the table
101
+ #
102
+ # @return [String] SQL DDL Statement to create a constraint
103
+ #
104
+ # @api private
43
105
  def create_constraints_statement(table_name, constraint_name, keys, foreign_table, foreign_keys, delete_constraint_type)
44
106
  <<-EOS.compress_lines
45
107
  ALTER TABLE #{quote_table_name(table_name)}
@@ -51,6 +113,16 @@ module DataMapper
51
113
  EOS
52
114
  end
53
115
 
116
+ ##
117
+ # Generates the SQL statement to destroy a constraint
118
+ #
119
+ # @param table_name [String] name of table to constrain
120
+ #
121
+ # @param constraint_name [String] name of foreign key constraint
122
+ #
123
+ # @return [String] SQL DDL Statement to destroy a constraint
124
+ #
125
+ # @api private
54
126
  def destroy_constraints_statement(table_name, constraint_name)
55
127
  <<-EOS.compress_lines
56
128
  ALTER TABLE #{quote_table_name(table_name)}
@@ -58,10 +130,30 @@ module DataMapper
58
130
  EOS
59
131
  end
60
132
 
133
+ ##
134
+ # generates a unique constraint name given a table and a relationships
135
+ #
136
+ # @param table_name [String] name of table to constrain
137
+ #
138
+ # @param relationships_name [String] name of the relationship to constrain
139
+ #
140
+ # @return [String] name of the constraint
141
+ #
142
+ # @api private
61
143
  def constraint_name(table_name, relationship_name)
62
144
  "#{table_name}_#{relationship_name}_fk"
63
145
  end
64
146
 
147
+ ##
148
+ # SQL quotes a foreign key constraint name
149
+ #
150
+ # @see #quote_table_name
151
+ #
152
+ # @param foreign_key [String] SQL quotes a foreign key name
153
+ #
154
+ # @return [String] quoted constraint name
155
+ #
156
+ # @api private
65
157
  def quote_constraint_name(foreign_key)
66
158
  quote_table_name(foreign_key)
67
159
  end
@@ -78,6 +170,7 @@ module DataMapper
78
170
  def auto_migrate_constraints_down(repository_name, *descendants)
79
171
  descendants = DataMapper::Resource.descendants.to_a if descendants.empty?
80
172
  descendants.each do |model|
173
+ repository_name ||= model.repository(repository_name).name
81
174
  if model.storage_exists?(repository_name)
82
175
  adapter = model.repository(repository_name).adapter
83
176
  next unless adapter.respond_to?(:destroy_constraints_statements)
@@ -90,6 +183,7 @@ module DataMapper
90
183
  def auto_migrate_constraints_up(retval, repository_name, *descendants)
91
184
  descendants = DataMapper::Resource.descendants.to_a if descendants.empty?
92
185
  descendants.each do |model|
186
+ repository_name ||= model.repository(repository_name).name
93
187
  adapter = model.repository(repository_name).adapter
94
188
  next unless adapter.respond_to?(:create_constraints_statements)
95
189
  statements = adapter.create_constraints_statements(repository_name, model)
@@ -8,16 +8,57 @@ module DataMapper
8
8
 
9
9
  module ClassMethods
10
10
  DELETE_CONSTRAINT_OPTIONS = [:protect, :destroy, :destroy!, :set_nil, :skip]
11
+
12
+ ##
13
+ # Checks that the constraint type is appropriate to the relationship
14
+ #
15
+ # @param cardinality [Fixnum] cardinality of relationship
16
+ #
17
+ # @param name [Symbol] name of relationship to evaluate constraint of
18
+ #
19
+ # @param options [Hash] options hash
20
+ #
21
+ # @raises ArgumentError
22
+ #
23
+ # @return [nil]
24
+ #
25
+ # @api semi-public
11
26
  def check_delete_constraint_type(cardinality, name, options = {})
27
+ #Make sure options contains :constraint key, whether nil or not
28
+ options[:constraint] ||= nil
12
29
  constraint_type = options[:constraint]
13
30
  return if constraint_type.nil?
31
+
14
32
  delete_constraint_options = DELETE_CONSTRAINT_OPTIONS.map { |o| ":#{o}" }
15
33
  if !DELETE_CONSTRAINT_OPTIONS.include?(constraint_type)
16
34
  raise ArgumentError, ":constraint option must be one of #{delete_constraint_options * ', '}"
17
35
  end
36
+
37
+ if constraint_type == :set_nil && self.relationships[name].is_a?(DataMapper::Associations::RelationshipChain)
38
+ raise ArgumentError, "Constraint type :set_nil is not valid for M:M relationships"
39
+ end
40
+
41
+ if cardinality == 1 && constraint_type == :destroy!
42
+ raise ArgumentError, "Constraint type :destroy! is not valid for 1:1 relationships"
43
+ end
18
44
  end
19
45
 
20
- # TODO: that should be moved to a 'util-like' module
46
+ ##
47
+ # Temporarily changes the visibility of a method so a block can be evaluated against it
48
+ #
49
+ # @param method [Symobl] method to change visibility of
50
+ #
51
+ # @param from_visibility [Symbol] original visibility
52
+ #
53
+ # @param to_visibility [Symbol] temporary visibility
54
+ #
55
+ # @param block [Proc] proc to run
56
+ #
57
+ # @notes TODO: this should be moved to a 'util-like' module
58
+ #
59
+ # @return [nil]
60
+ #
61
+ # @api semi-public
21
62
  def with_changed_method_visibility(method, from_visibility, to_visibility, &block)
22
63
  send(to_visibility, method)
23
64
  yield
@@ -26,33 +67,119 @@ module DataMapper
26
67
 
27
68
  end
28
69
 
29
- def add_delete_constraint_option(name, repository_name, child_model, parent_model, options = {})
30
- @delete_constraint = options[:constraint]
70
+ ##
71
+ # Addes the delete constraint options to a relationship
72
+ #
73
+ # @param params [*ARGS] Arguments passed to Relationship#initialize or RelationshipChain#initialize
74
+ #
75
+ # @notes This takes *params because it runs before the initializer for Relationships and RelationshipChains
76
+ # which have different method signatures
77
+ #
78
+ # @return [nil]
79
+ #
80
+ # @api semi-public
81
+ def add_delete_constraint_option(*params)
82
+ opts = params.last
83
+
84
+ if opts.is_a?(Hash)
85
+ #if it is a chain, set the constraint on the 1:M near relationship(anonymous)
86
+ if self.is_a?(DataMapper::Associations::RelationshipChain)
87
+ opts = params.last
88
+ near_rel = opts[:parent_model].relationships[opts[:near_relationship_name]]
89
+ near_rel.options[:constraint] = opts[:constraint]
90
+ near_rel.instance_variable_set "@delete_constraint", opts[:constraint]
91
+ end
92
+
93
+ @delete_constraint = params.last[:constraint]
94
+ end
31
95
  end
32
96
 
97
+ ##
98
+ # Checks delete constraints prior to destroying a dm resource or collection
99
+ #
100
+ # @throws :halt
101
+ #
102
+ # @notes
103
+ # - It only considers a relationship's constraints if this is the parent model (ie a child shouldn't delete a parent)
104
+ # - RelationshipChains are skipped, as they are evaluated by their underlying 1:M relationships
105
+ #
106
+ # @returns [nil]
107
+ #
108
+ # @api semi-public
33
109
  def check_delete_constraints
34
110
  model.relationships.each do |rel_name, rel|
111
+ #Only look at relationships where this model is the parent
112
+ next if rel.parent_model != model
113
+
114
+ #Don't delete across M:M relationships, instead use their anonymous 1:M Relationships
115
+ next if rel.is_a?(DataMapper::Associations::RelationshipChain)
116
+
35
117
  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
118
+ if children.kind_of?(DataMapper::Collection)
119
+ check_collection_delete_constraints(rel,children)
120
+ elsif children
121
+ check_resource_delete_constraints(rel,children)
122
+ end
53
123
  end # relationships
54
124
  end # check_delete_constraints
55
125
 
126
+ ##
127
+ # Performs the meat of the check_delete_constraints method for a collection of resources
128
+ #
129
+ # @param rel [DataMapper::Associations::Relationship] relationship being evaluated
130
+ #
131
+ # @param children [~DataMapper::Collection] child records to constrain
132
+ #
133
+ # @see #check_delete_constraints
134
+ #
135
+ # @api semi-public
136
+ def check_collection_delete_constraints(rel, children)
137
+ case rel.delete_constraint
138
+ when nil, :protect
139
+ unless children.empty?
140
+ DataMapper.logger.error("Could not delete #{self.class} a child #{children.first.class} exists")
141
+ throw(:halt,false)
142
+ end
143
+ when :destroy
144
+ children.each{|child| child.destroy}
145
+ when :destroy!
146
+ children.destroy!
147
+ when :set_nil
148
+ children.each do |child|
149
+ child.class.many_to_one_relationships.each do |mto_rel|
150
+ child.send("#{mto_rel.name}=", nil) if child.send(mto_rel.name).eql?(self)
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ ##
157
+ # Performs the meat of check_delete_constraints method for a single resource
158
+ #
159
+ # @param rel [DataMapper::Associations::Relationship] the relationship to evaluate
160
+ #
161
+ # @param child [~DataMapper::Model] the model to constrain
162
+ #
163
+ # @see #check_delete_constraints
164
+ #
165
+ # @api semi-public
166
+ def check_resource_delete_constraints(rel, child)
167
+ case rel.delete_constraint
168
+ when nil, :protect
169
+ unless child.nil?
170
+ DataMapper.logger.error("Could not delete #{self.class} a child #{child.class} exists")
171
+ throw(:halt,false)
172
+ end
173
+ when :destroy
174
+ child.destroy
175
+ when :destroy!
176
+ #not supported in dm-master, an exception should have been raised on class load
177
+ when :set_nil
178
+ child.class.many_to_one_relationships.each do |mto_rel|
179
+ child.send("#{mto_rel.name}=", nil) if child.send(mto_rel.name).eql?(self)
180
+ end
181
+ end
182
+ end
56
183
 
57
184
  end # DeleteConstraint
58
185
  end # Constraints
@@ -6,6 +6,16 @@ module DataMapper
6
6
 
7
7
  private
8
8
 
9
+ ##
10
+ # MySQL specific query to determine to drop a foreign key
11
+ #
12
+ # @param table_name [Symbol] name of table to check constraint on
13
+ #
14
+ # @param constraint_name [~String] name of constraint to check for
15
+ #
16
+ # @return [String] SQL DDL to destroy a constraint
17
+ #
18
+ # @api private
9
19
  def destroy_constraints_statement(table_name, constraint_name)
10
20
  <<-EOS.compress_lines
11
21
  ALTER TABLE #{quote_table_name(table_name)}
@@ -13,7 +23,17 @@ module DataMapper
13
23
  EOS
14
24
  end
15
25
 
16
- def constraint_exists?(storage_name, constraint_name)
26
+ ##
27
+ # MySQL specific query to determine if a constraint exists
28
+ #
29
+ # @param table_name [Symbol] name of table to check constraint on
30
+ #
31
+ # @param constraint_name [~String] name of constraint to check for
32
+ #
33
+ # @return [Boolean]
34
+ #
35
+ # @api private
36
+ def constraint_exists?(table_name, constraint_name)
17
37
  statement = <<-EOS.compress_lines
18
38
  SELECT COUNT(*)
19
39
  FROM `information_schema`.`table_constraints`
@@ -22,7 +42,7 @@ module DataMapper
22
42
  AND `table_name` = ?
23
43
  AND `constraint_name` = ?
24
44
  EOS
25
- query(statement, db_name, storage_name, constraint_name).first > 0
45
+ query(statement, db_name, table_name, constraint_name).first > 0
26
46
  end
27
47
  end
28
48
  end
@@ -6,7 +6,17 @@ module DataMapper
6
6
 
7
7
  private
8
8
 
9
- def constraint_exists?(storage_name, constraint_name)
9
+ ##
10
+ # Postgres specific query to determine if a constraint exists
11
+ #
12
+ # @param table_name [Symbol] name of table to check constraint on
13
+ #
14
+ # @param constraint_name [~String] name of constraint to check for
15
+ #
16
+ # @return [Boolean]
17
+ #
18
+ # @api private
19
+ def constraint_exists?(table_name, constraint_name)
10
20
  statement = <<-EOS.compress_lines
11
21
  SELECT COUNT(*)
12
22
  FROM "information_schema"."table_constraints"
@@ -14,7 +24,7 @@ module DataMapper
14
24
  AND "table_name" = ?
15
25
  AND "constraint_name" = ?
16
26
  EOS
17
- query(statement, storage_name, constraint_name).first > 0
27
+ query(statement, table_name, constraint_name).first > 0
18
28
  end
19
29
  end
20
30
  end
@@ -1,5 +1,5 @@
1
1
  module DataMapper
2
2
  module Constraints
3
- VERSION = '0.9.10'
3
+ VERSION = '0.9.11'
4
4
  end
5
5
  end
@@ -3,7 +3,7 @@ require 'rubygems'
3
3
  require 'pathname'
4
4
 
5
5
  # Add all external dependencies for the plugin here
6
- gem 'dm-core', '~>0.9.10'
6
+ gem 'dm-core', '0.9.11'
7
7
  require 'dm-core'
8
8
 
9
9
  # Require plugin-files
@@ -12,6 +12,25 @@ require Pathname(__FILE__).dirname.expand_path / 'dm-constraints' / 'postgres_ad
12
12
  require Pathname(__FILE__).dirname.expand_path / 'dm-constraints' / 'mysql_adapter'
13
13
  require Pathname(__FILE__).dirname.expand_path / 'dm-constraints' / 'delete_constraint'
14
14
 
15
+ module DataMapper
16
+ module Associations
17
+ class RelationshipChain
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
+
15
34
  module DataMapper
16
35
  module Associations
17
36
  class Relationship
@@ -40,6 +59,10 @@ module DataMapper
40
59
  include DeleteConstraint::ClassMethods
41
60
  end
42
61
 
62
+ ##
63
+ # Add before hooks to #has to check for proper constraint definitions
64
+ # Add before hooks to #destroy to properly constrain children
65
+ #
43
66
  def self.included(model)
44
67
  model.extend(ClassMethods)
45
68
  model.class_eval do
@@ -2,11 +2,7 @@ require 'pathname'
2
2
  require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
3
 
4
4
  ADAPTERS.each do |adapter|
5
-
6
5
  describe 'DataMapper::Constraints' do
7
-
8
- # load_models_for_metaphor :stable, :farmer, :cow
9
-
10
6
  before do
11
7
  DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[adapter]
12
8
 
@@ -43,10 +39,43 @@ ADAPTERS.each do |adapter|
43
39
  belongs_to :farmer
44
40
  end
45
41
 
42
+ #Used to test a belongs_to association with no has() association
43
+ #on the other end
44
+ class ::Pig
45
+ include DataMapper::Resource
46
+ include DataMapper::Constraints
47
+
48
+ property :id, Serial
49
+ property :name, String
50
+
51
+ belongs_to :farmer
52
+ end
53
+
54
+ #Used to test M:M :through => Resource relationships
55
+ class ::Chicken
56
+ include DataMapper::Resource
57
+ include DataMapper::Constraints
58
+
59
+ property :id, Serial
60
+ property :name, String
61
+
62
+ has n, :tags, :through => Resource
63
+ end
64
+
65
+ class ::Tag
66
+ include DataMapper::Resource
67
+ include DataMapper::Constraints
68
+
69
+ property :id, Serial
70
+ property :phrase, String
71
+
72
+ has n, :chickens, :through => Resource
73
+ end
74
+
46
75
  DataMapper.auto_migrate!
47
- end
76
+ end # before
48
77
 
49
- it "is included when DataMapper::Searchable is loaded" do
78
+ it "is included when DataMapper::Constraints is loaded" do
50
79
  Cow.new.should be_kind_of(DataMapper::Constraints)
51
80
  end
52
81
 
@@ -65,16 +94,24 @@ ADAPTERS.each do |adapter|
65
94
  lambda { @c1 = Cow.create(:name => "Bea", :stable_id => s.id + 1) }.should raise_error
66
95
  end
67
96
 
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)
97
+ describe "belongs_to without matching has association" do
98
+ before do
99
+ @f1 = Farmer.create(:first_name => "John", :last_name => "Doe")
100
+ @f2 = Farmer.create(:first_name => "Some", :last_name => "Body")
101
+ @p = Pig.create(:name => "Bea", :farmer => @f2)
102
+ end
103
+ it "should destroy the parent if there are no children in the association" do
104
+ @f1.destroy.should == true
105
+ end
106
+
107
+ it "the child should be destroyable" do
108
+ @p.destroy.should == true
109
+ end
110
+
111
+ end
76
112
 
77
113
  describe "constraint options" do
114
+
78
115
  describe "when no constraint options are given" do
79
116
 
80
117
  it "should destroy the parent if there are no children in the association" do
@@ -96,128 +133,424 @@ ADAPTERS.each do |adapter|
96
133
  before do
97
134
  class ::Farmer
98
135
  has n, :cows, :constraint => :protect
136
+ has 1, :pig, :constraint => :protect
137
+ end
138
+ class ::Pig
139
+ belongs_to :farmer
99
140
  end
100
141
  class ::Cow
101
142
  belongs_to :farmer
102
143
  end
144
+ class ::Chicken
145
+ has n, :tags, :through => Resource, :constraint => :protect
146
+ end
147
+ class ::Tag
148
+ has n, :chickens, :through => Resource, :constraint => :protect
149
+ end
103
150
  end
104
151
 
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
152
+ describe "one-to-one associations" do
153
+ before do
154
+ @f1 = Farmer.create(:first_name => "Mary", :last_name => "Smith")
155
+ @p1 = Pig.create(:name => "Morton",:farmer => @f1)
156
+ end
157
+
158
+ it "should not destroy the parent if there are children in the association" do
159
+ @f1.destroy.should == false
160
+ end
161
+
162
+ it "the child should be destroyable" do
163
+ @p1.destroy.should == true
164
+ end
110
165
  end
111
166
 
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
167
+ describe "one-to-many associations" do
168
+ before do
169
+ @f1 = Farmer.create(:first_name => "John", :last_name => "Doe")
170
+ @f2 = Farmer.create(:first_name => "Some", :last_name => "Body")
171
+ @c1 = Cow.create(:name => "Bea", :farmer => @f2)
172
+ end
173
+
174
+ it "should destroy the parent if there are no children in the association" do
175
+ @f1.destroy.should == true
176
+ end
177
+
178
+ it "should not destroy the parent if there are children in the association" do
179
+ @f2.destroy.should == false
180
+ end
181
+
182
+ it "the child should be destroyable" do
183
+ @c1.destroy.should == true
184
+ end
116
185
  end
117
186
 
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
187
+ describe "many-to-many associations" do
188
+ before do
189
+ @t1 = Tag.create(:phrase => "silly chicken")
190
+ @t2 = Tag.create(:phrase => "serious chicken")
191
+ @chk1 = Chicken.create(:name =>"Frank the Chicken", :tags => [@t2])
192
+ end
193
+
194
+ it "should destroy the parent if there are no children in the association" do
195
+ @t1.destroy.should == true
196
+ end
197
+
198
+ it "should not destroy the parent if there are children in the association" do
199
+ @t2.destroy.should == false
200
+ end
201
+
202
+ it "the child should be destroyable" do
203
+ @chk1.tags.clear
204
+ @chk1.save.should == true
205
+ @chk1.tags.should be_empty
206
+ end
122
207
  end
123
208
 
124
- end
209
+ end # when constraint protect is given
210
+
211
+ describe "when :constraint => :destroy! is given" do
212
+ before do
213
+ class ::Farmer
214
+ has n, :cows, :constraint => :destroy!
215
+ end
216
+ class ::Cow
217
+ belongs_to :farmer
218
+ end
219
+ class ::Chicken
220
+ has n, :tags, :through => Resource, :constraint => :destroy!
221
+ end
222
+ class ::Tag
223
+ has n, :chickens, :through => Resource, :constraint => :destroy!
224
+ end
225
+
226
+ DataMapper.auto_migrate!
227
+ end
228
+
229
+ describe "one-to-many associations" do
230
+ before(:each) do
231
+ @f = Farmer.create(:first_name => "John", :last_name => "Doe")
232
+ @c1 = Cow.create(:name => "Bea", :farmer => @f)
233
+ @c2 = Cow.create(:name => "Riksa", :farmer => @f)
234
+ end
235
+
236
+ it "should let the parent to be destroyed" do
237
+ @f.destroy.should == true
238
+ @f.should be_new_record
239
+ end
240
+
241
+ it "should destroy the children" do
242
+ @f.destroy
243
+ @f.cows.all? { |c| c.should be_new_record }
244
+ end
245
+
246
+ it "the child should be destroyable" do
247
+ @c1.destroy.should == true
248
+ end
249
+
250
+ end
251
+
252
+ describe "many-to-many associations" do
253
+ before do
254
+ @t1 = Tag.create(:phrase => "floozy")
255
+ @t2 = Tag.create(:phrase => "dirty")
256
+ @chk1 = Chicken.create(:name => "Nancy Chicken", :tags => [@t1, @t2])
257
+ end
258
+
259
+ it "should destroy! the parent and the children, too" do
260
+ @chk1.destroy.should == true
261
+ @chk1.should be_new_record
262
+
263
+ # @t1 & @t2 should still exist, the chicken_tags should have been deleted
264
+ ChickenTag.all.should be_empty
265
+ @t1.should_not be_new_record
266
+ @t2.should_not be_new_record
267
+ end
268
+
269
+ it "the child should be destroyable" do
270
+ @chk1.destroy.should == true
271
+ end
272
+ end
273
+
274
+ end # when :constraint => :destroy! is given
125
275
 
126
276
  describe "when :constraint => :destroy is given" do
127
277
  before do
128
278
  class ::Farmer
129
279
  has n, :cows, :constraint => :destroy
280
+ has 1, :pig, :constraint => :destroy
130
281
  end
131
282
  class ::Cow
132
283
  belongs_to :farmer
133
284
  end
285
+ class ::Chicken
286
+ has n, :tags, :through => Resource, :constraint => :destroy
287
+ end
288
+ class ::Tag
289
+ has n, :chickens, :through => Resource, :constraint => :destroy
290
+ end
291
+
134
292
  DataMapper.auto_migrate!
135
293
  end
136
294
 
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
295
+ describe "one-to-one associations" do
296
+ before do
297
+ @f = Farmer.create(:first_name => "Ted", :last_name => "Cornhusker")
298
+ @p = Pig.create(:name => "BaconBits", :farmer => @f)
299
+ end
300
+
301
+ it "should let the parent to be destroyed" do
302
+ @f.destroy.should == true
303
+ @f.should be_new_record
304
+ end
305
+
306
+ it "should destroy the children" do
307
+ pig = @f.pig
308
+ @f.destroy
309
+ pig.should be_new_record
310
+ end
311
+
312
+ it "the child should be destroyable" do
313
+ @p.destroy.should == true
314
+ end
315
+ end
316
+
317
+ describe "one-to-many associations" do
318
+ before(:each) do
142
319
  @f = Farmer.create(:first_name => "John", :last_name => "Doe")
143
320
  @c1 = Cow.create(:name => "Bea", :farmer => @f)
144
321
  @c2 = Cow.create(:name => "Riksa", :farmer => @f)
322
+ end
323
+
324
+ it "should let the parent to be destroyed" do
145
325
  @f.destroy.should == true
146
326
  @f.should be_new_record
147
- @c1.should be_new_record
148
- @c2.should be_new_record
149
327
  end
328
+
329
+ it "should destroy the children" do
330
+ @f.destroy
331
+ @f.cows.all? { |c| c.should be_new_record }
332
+ @f.should be_new_record
333
+ end
334
+
335
+ it "the child should be destroyable" do
336
+ @c1.destroy.should == true
337
+ end
338
+
150
339
  end
151
340
 
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
341
+ describe "many-to-many associations" do
342
+ before do
343
+ @t1 = Tag.create :phrase => "floozy"
344
+ @t2 = Tag.create :phrase => "dirty"
345
+ @chk1 = Chicken.create :name => "Nancy Chicken", :tags => [@t1,@t2]
346
+ end
347
+
348
+ it "should destroy the parent and the children, too" do
349
+ @chk1.destroy.should == true
350
+ @chk1.should be_new_record
351
+
352
+ #@t1 & @t2 should still exist, the chicken_tags should have been deleted
353
+ ChickenTag.all.should be_empty
354
+ @t1.should_not be_new_record
355
+ @t2.should_not be_new_record
356
+ end
357
+
358
+ it "the child should be destroyable" do
359
+ @chk1.destroy.should == true
360
+ end
156
361
  end
157
362
 
158
- end
363
+ end # when :constraint => :destroy is given
159
364
 
160
365
  describe "when :constraint => :set_nil is given" do
161
366
  before do
162
367
  class ::Farmer
163
368
  has n, :cows, :constraint => :set_nil
369
+ has 1, :pig, :constraint => :set_nil
164
370
  end
165
371
  class ::Cow
166
372
  belongs_to :farmer
167
373
  end
374
+ # NOTE: M:M Relationships are not supported,
375
+ # see "when checking constraint types" tests at bottom
168
376
  DataMapper.auto_migrate!
169
377
  end
170
378
 
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 }
379
+ describe "one-to-one associations" do
380
+ before do
381
+ @f = Farmer.create(:first_name => "Mr", :last_name => "Hands")
382
+ @p = Pig.create(:name => "Greasy", :farmer => @f)
383
+ end
384
+
385
+ it "should let the parent to be destroyed" do
386
+ @f.destroy.should == true
387
+ end
388
+
389
+ it "should set the child's foreign_key id to nil" do
390
+ pig = @f.pig
391
+ @f.destroy.should == true
392
+ pig.farmer.should be_nil
393
+ end
394
+
395
+ it "the child should be destroyable" do
396
+ @p.destroy.should == true
397
+ end
398
+
178
399
  end
179
400
 
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
401
+ describe "one-to-many associations" do
402
+ before(:each) do
403
+ @f = Farmer.create(:first_name => "John", :last_name => "Doe")
404
+ @c1 = Cow.create(:name => "Bea", :farmer => @f)
405
+ @c2 = Cow.create(:name => "Riksa", :farmer => @f)
406
+ end
407
+
408
+ it "should let the parent to be destroyed" do
409
+ @f.destroy.should == true
410
+ @f.should be_new_record
411
+ end
412
+
413
+ it "should set the foreign_key ids of children to nil" do
414
+ @f.destroy
415
+ @f.cows.all? { |c| c.farmer.should be_nil }
416
+ end
417
+
418
+ it "the children should be destroyable" do
419
+ @c1.destroy.should == true
420
+ @c2.destroy.should == true
421
+ end
422
+
184
423
  end
185
424
 
186
- end # describe
425
+ end # describe "when :constraint => :set_nil is given" do
187
426
 
188
427
  describe "when :constraint => :skip is given" do
189
428
  before do
190
429
  class ::Farmer
191
430
  has n, :cows, :constraint => :skip
431
+ has 1, :pig, :constraint => :skip
192
432
  end
193
433
  class ::Cow
194
434
  belongs_to :farmer
195
435
  end
436
+ class ::Chicken
437
+ has n, :tags, :through => Resource, :constraint => :skip
438
+ end
439
+ class ::Tag
440
+ has n, :chickens, :through => Resource, :constraint => :skip
441
+ end
196
442
  DataMapper.auto_migrate!
197
443
  end
198
444
 
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
445
+ describe "one-to-one associations" do
446
+ before do
447
+ @f = Farmer.create(:first_name => "William", :last_name => "Shepard")
448
+ @p = Pig.create(:name => "Jiggles The Pig", :farmer => @f)
449
+ end
450
+
451
+ it "should let the parent be destroyed" do
452
+ @f.destroy.should == true
453
+ @f.should be_new_record
454
+ # @p.farmer.should be_new_record
455
+ end
456
+
457
+ it "should let the children become orphan records" do
458
+ @f.destroy
459
+ @p.farmer.should be_new_record
460
+ end
461
+
462
+ it "the child should be destroyable" do
463
+ @p.destroy.should == true
464
+ end
465
+
206
466
  end
207
467
 
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
468
+ describe "one-to-many associations" do
469
+ before do
470
+ @f = Farmer.create(:first_name => "John", :last_name => "Doe")
471
+ @c1 = Cow.create(:name => "Bea", :farmer => @f)
472
+ @c2 = Cow.create(:name => "Riksa", :farmer => @f)
473
+ end
474
+
475
+ it "should let the parent to be destroyed" do
476
+ @f.destroy.should == true
477
+ @f.should be_new_record
478
+ end
479
+
480
+ it "should let the children become orphan records" do
481
+ @f.destroy
482
+ @c1.farmer.should be_new_record
483
+ @c2.farmer.should be_new_record
484
+ end
485
+
486
+ it "the children should be destroyable" do
487
+ @c1.destroy.should == true
488
+ @c2.destroy.should == true
489
+ end
490
+
491
+ end
492
+
493
+ describe "many-to-many associations" do
494
+ before do
495
+ @t = Tag.create(:phrase => "Richard Pryor's Chicken")
496
+ @chk = Chicken.create(:name => "Delicious", :tags => [@t])
497
+ end
498
+
499
+ it "the children should be destroyable" do
500
+ @chk.destroy.should == true
501
+ end
212
502
  end
213
503
 
214
- end # describe
504
+ end # describe "when :constraint => :skip is given"
505
+
506
+ describe "when checking constraint types" do
507
+
508
+ #M:M relationships results in a join table composed of a two part primary key
509
+ # setting a portion of the primary key is not possible for two reasons:
510
+ # 1. the columns are defined as :nullable => false
511
+ # 2. there could be duplicate rows if more than one of either of the types
512
+ # was deleted while being associated to the same type on the other side of the relationshp
513
+ # Given
514
+ # Turkey(Name: Ted, ID: 1) =>
515
+ # Tags[Tag(Phrase: awesome, ID: 1), Tag(Phrase: fat, ID: 2)]
516
+ # Turkey(Name: Steve, ID: 2) =>
517
+ # Tags[Tag(Phrase: awesome, ID: 1), Tag(Phrase: flamboyant, ID: 3)]
518
+ #
519
+ # Table turkeys_tags would look like (turkey_id, tag_id)
520
+ # (1, 1)
521
+ # (1, 2)
522
+ # (2, 1)
523
+ # (2, 3)
524
+ #
525
+ # If both turkeys were deleted and pk was set null
526
+ # (null, 1)
527
+ # (null, 2)
528
+ # (null, 1) #at this time there would be a duplicate row error
529
+ # (null, 3)
530
+ #
531
+ # I would suggest setting :constraint to :skip in this scenario which will leave
532
+ # you with orphaned rows.
533
+ it "should raise an error if :set_nil is given for a M:M relationship" do
534
+ lambda{
535
+ class ::Chicken
536
+ has n, :tags, :through => Resource, :constraint => :set_nil
537
+ end
538
+ class ::Tag
539
+ has n, :chickens, :through => Resource, :constraint => :set_nil
540
+ end
541
+ }.should raise_error(ArgumentError)
542
+ end
215
543
 
216
- describe "when an invalid option is given" do
217
- before do
544
+ # Resource#destroy! is not suppored in dm-core
545
+ it "should raise an error if :destroy! is given for a 1:1 relationship" do
546
+ lambda do
547
+ class ::Farmer
548
+ has 1, :pig, :constraint => :destroy!
549
+ end
550
+ end.should raise_error(ArgumentError)
218
551
  end
219
552
 
220
- it "should raise an error" do
553
+ it "should raise an error if an unknown type is given" do
221
554
  lambda do
222
555
  class ::Farmer
223
556
  has n, :cows, :constraint => :chocolate
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  require 'pathname'
2
2
  require 'rubygems'
3
3
 
4
- gem 'rspec', '~>1.1.11'
4
+ gem 'rspec', '~>1.2'
5
5
  require 'spec'
6
6
 
7
- gem 'dm-core', '~>0.9.10'
7
+ gem 'dm-core', '0.9.11'
8
8
  require 'dm-core'
9
9
 
10
10
  ADAPTERS = []
data/tasks/spec.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  begin
2
- gem 'rspec', '~>1.1.11'
2
+ gem 'rspec', '~>1.2'
3
3
  require 'spec'
4
4
  require 'spec/rake/spectask'
5
5
 
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.10
4
+ version: 0.9.11
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-19 00:00:00 -08:00
12
+ date: 2009-03-29 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -18,9 +18,9 @@ dependencies:
18
18
  version_requirement:
19
19
  version_requirements: !ruby/object:Gem::Requirement
20
20
  requirements:
21
- - - ~>
21
+ - - "="
22
22
  - !ruby/object:Gem::Version
23
- version: 0.9.10
23
+ version: 0.9.11
24
24
  version:
25
25
  description: DataMapper plugin constraining relationships
26
26
  email: