dm-constraints 0.9.10 → 0.9.11

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 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: