dm-constraints 0.10.2 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,265 +1,48 @@
1
+ require 'dm-migrations/auto_migration'
2
+
1
3
  module DataMapper
2
4
  module Constraints
3
5
  module Migrations
4
- module SingletonMethods
5
- def self.included(base)
6
- # TODO: figure out how to make this work without AMC
7
- base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
8
- alias_method :auto_migrate_down_without_constraints!, :auto_migrate_down!
9
- alias_method :auto_migrate_down!, :auto_migrate_down_with_constraints!
10
-
11
- alias_method :auto_migrate_up_without_constraints!, :auto_migrate_up!
12
- alias_method :auto_migrate_up!, :auto_migrate_up_with_constraints!
13
- RUBY
14
- end
15
-
16
- def auto_migrate_down_with_constraints!(repository_name = nil)
17
- repository_execute(:auto_migrate_down_with_constraints!, repository_name)
18
- auto_migrate_down_without_constraints!(repository_name)
19
- end
20
-
21
- def auto_migrate_up_with_constraints!(repository_name = nil)
22
- auto_migrate_up_without_constraints!(repository_name)
23
- repository_execute(:auto_migrate_up_with_constraints!, repository_name)
24
- end
25
- end
26
-
27
- module DataObjectsAdapter
28
- ##
29
- # Determine if a constraint exists for a table
30
- #
31
- # @param storage_name [Symbol]
32
- # name of table to check constraint on
33
- # @param constraint_name [~String]
34
- # name of constraint to check for
35
- #
36
- # @return [Boolean]
37
- #
38
- # @api private
39
- def constraint_exists?(storage_name, constraint_name)
40
- statement = <<-SQL.compress_lines
41
- SELECT COUNT(*)
42
- FROM "information_schema"."table_constraints"
43
- WHERE "constraint_type" = 'FOREIGN KEY'
44
- AND "table_schema" = ?
45
- AND "table_name" = ?
46
- AND "constraint_name" = ?
47
- SQL
48
-
49
- select(statement, schema_name, storage_name, constraint_name).first > 0
50
- end
51
-
52
- ##
53
- # Create the constraint for a relationship
54
- #
55
- # @param relationship [Relationship]
56
- # the relationship to create the constraint for
57
- #
58
- # @return [true, false]
59
- # true if creating the constraints was successful
60
- #
61
- # @api semipublic
62
- def create_relationship_constraint(relationship)
63
- return false unless valid_relationship_for_constraint?(relationship)
64
-
65
- source_model = relationship.source_model
66
- source_table = source_model.storage_name(name)
67
- source_key = relationship.source_key
68
-
69
- constraint_name = constraint_name(source_table, relationship.name)
70
- return false if constraint_exists?(source_table, constraint_name)
71
-
72
- constraint_type = case relationship.inverse.constraint
73
- when :protect then 'NO ACTION'
74
- when :destroy, :destroy! then 'CASCADE'
75
- when :set_nil then 'SET NULL'
76
- end
77
-
78
- return false if constraint_type.nil?
79
-
80
- storage_name = relationship.source_model.storage_name(name)
81
- reference_storage_name = relationship.target_model.storage_name(name)
82
-
83
- foreign_keys = relationship.source_key.map { |p| property_to_column_name(p, false) }
84
- reference_keys = relationship.target_key.map { |p| property_to_column_name(p, false) }
85
-
86
- execute(create_constraints_statement(storage_name, constraint_name, constraint_type, foreign_keys, reference_storage_name, reference_keys))
87
- end
88
-
89
- ##
90
- # Remove the constraint for a relationship
91
- #
92
- # @param relationship [Relationship]
93
- # the relationship to remove the constraint for
94
- #
95
- # @return [true, false]
96
- # true if destroying the constraint was successful
97
- #
98
- # @api semipublic
99
- def destroy_relationship_constraint(relationship)
100
- return false unless valid_relationship_for_constraint?(relationship)
101
-
102
- source_model = relationship.source_model
103
- source_table = source_model.storage_name(name)
104
6
 
105
- constraint_name = constraint_name(source_table, relationship.name)
106
- return false unless constraint_exists?(source_table, constraint_name)
107
-
108
- execute(destroy_constraints_statement(source_table, constraint_name))
109
- end
110
-
111
- private
7
+ module SingletonMethods
112
8
 
113
- ##
114
- # Check to see if the relationship's constraints can be used
115
- #
116
- # Only one-to-one, one-to-many, and many-to-many relationships
117
- # can be used for constraints. They must also be in the same
118
- # repository as the adapter is connected to.
119
- #
120
- # @param relationship [Relationship]
121
- # the relationship to check
122
- #
123
- # @return [true, false]
124
- # true if a constraint can be established for relationship
125
- #
126
- # @api private
127
- def valid_relationship_for_constraint?(relationship)
128
- return false unless relationship.source_repository_name == name || relationship.source_repository_name.nil?
129
- return false unless relationship.target_repository_name == name || relationship.target_repository_name.nil?
130
- return false unless relationship.kind_of?(Associations::ManyToOne::Relationship)
131
- true
9
+ def auto_migrate!(repository_name = nil)
10
+ repository_execute(:auto_migrate_down_constraints!, repository_name)
11
+ descendants = super
12
+ repository_execute(:auto_migrate_up_constraints!, repository_name)
13
+ descendants
132
14
  end
133
15
 
134
- module SQL
135
- private
136
-
137
- ##
138
- # Generates the SQL statement to create a constraint
139
- #
140
- # @param constraint_name [String]
141
- # name of the foreign key constraint
142
- # @param constraint_type [String]
143
- # type of foreign key constraint to add to the table
144
- # @param storage_name [String]
145
- # name of table to constrain
146
- # @param foreign_keys [Array[String]]
147
- # columns in the table that refer to foreign table
148
- # @param reference_storage_name [String]
149
- # table the foreign key refers to
150
- # @param reference_storage_name [Array[String]]
151
- # columns the foreign table that are referred to
152
- #
153
- # @return [String]
154
- # SQL DDL Statement to create a constraint
155
- #
156
- # @api private
157
- def create_constraints_statement(storage_name, constraint_name, constraint_type, foreign_keys, reference_storage_name, reference_keys)
158
- <<-SQL.compress_lines
159
- ALTER TABLE #{quote_name(storage_name)}
160
- ADD CONSTRAINT #{quote_name(constraint_name)}
161
- FOREIGN KEY (#{foreign_keys.join(', ')})
162
- REFERENCES #{quote_name(reference_storage_name)} (#{reference_keys.join(', ')})
163
- ON DELETE #{constraint_type}
164
- ON UPDATE #{constraint_type}
165
- SQL
166
- end
167
-
168
- ##
169
- # Generates the SQL statement to destroy a constraint
170
- #
171
- # @param storage_name [String]
172
- # name of table to constrain
173
- # @param constraint_name [String]
174
- # name of foreign key constraint
175
- #
176
- # @return [String]
177
- # SQL DDL Statement to destroy a constraint
178
- #
179
- # @api private
180
- def destroy_constraints_statement(storage_name, constraint_name)
181
- <<-SQL.compress_lines
182
- ALTER TABLE #{quote_name(storage_name)}
183
- DROP CONSTRAINT #{quote_name(constraint_name)}
184
- SQL
185
- end
16
+ private
186
17
 
187
- ##
188
- # generates a unique constraint name given a table and a relationships
189
- #
190
- # @param storage_name [String]
191
- # name of table to constrain
192
- # @param relationships_name [String]
193
- # name of the relationship to constrain
194
- #
195
- # @return [String]
196
- # name of the constraint
197
- #
198
- # @api private
199
- def constraint_name(storage_name, relationship_name)
200
- identifier = "#{storage_name}_#{relationship_name}"[0, self.class::IDENTIFIER_MAX_LENGTH - 3]
201
- "#{identifier}_fk"
202
- end
18
+ def auto_migrate_down!(repository_name = nil)
19
+ repository_execute(:auto_migrate_down_constraints!, repository_name)
20
+ super
203
21
  end
204
22
 
205
- include SQL
206
- end
207
-
208
- module MysqlAdapter
209
- module SQL
210
- private
211
-
212
- ##
213
- # MySQL specific query to drop a foreign key
214
- #
215
- # @param storage_name [String]
216
- # name of table to constrain
217
- # @param constraint_name [String]
218
- # name of foreign key constraint
219
- #
220
- # @return [String]
221
- # SQL DDL Statement to destroy a constraint
222
- #
223
- # @api private
224
- def destroy_constraints_statement(storage_name, constraint_name)
225
- <<-SQL.compress_lines
226
- ALTER TABLE #{quote_name(storage_name)}
227
- DROP FOREIGN KEY #{quote_name(constraint_name)}
228
- SQL
229
- end
23
+ def auto_migrate_up!(repository_name = nil)
24
+ descendants = super
25
+ repository_execute(:auto_migrate_up_constraints!, repository_name)
26
+ descendants
230
27
  end
231
28
 
232
- include SQL
233
29
  end
234
30
 
235
- module Sqlite3Adapter
236
- def constraint_exists?(*)
237
- false
238
- end
239
-
240
- def create_relationship_constraint(*)
241
- false
242
- end
31
+ module Model
243
32
 
244
- def destroy_relationship_constraint(*)
245
- false
246
- end
247
- end
33
+ private
248
34
 
249
- module Model
250
- def auto_migrate_down_with_constraints!(repository_name = self.repository_name)
35
+ def auto_migrate_down_constraints!(repository_name = self.repository_name)
251
36
  return unless storage_exists?(repository_name)
252
37
  return if self.respond_to?(:is_remixable?) && self.is_remixable?
253
38
  execute_each_relationship(:destroy_relationship_constraint, repository_name)
254
39
  end
255
40
 
256
- def auto_migrate_up_with_constraints!(repository_name = self.repository_name)
41
+ def auto_migrate_up_constraints!(repository_name = self.repository_name)
257
42
  return if self.respond_to?(:is_remixable?) && self.is_remixable?
258
43
  execute_each_relationship(:create_relationship_constraint, repository_name)
259
44
  end
260
45
 
261
- private
262
-
263
46
  def execute_each_relationship(method, repository_name)
264
47
  adapter = DataMapper.repository(repository_name).adapter
265
48
  return unless adapter.respond_to?(method)
@@ -268,7 +51,10 @@ module DataMapper
268
51
  adapter.send(method, relationship)
269
52
  end
270
53
  end
54
+
271
55
  end
272
- end
273
- end
274
- end
56
+
57
+ end # module Migrations
58
+
59
+ end # module Constraints
60
+ end # module DataMapper
@@ -0,0 +1,41 @@
1
+ require 'dm-core'
2
+ require 'dm-constraints/delete_constraint'
3
+
4
+ module DataMapper
5
+ module Associations
6
+
7
+ class OneToMany::Relationship
8
+
9
+ include DataMapper::Hook
10
+ include Constraints::DeleteConstraint
11
+
12
+ OPTIONS << :constraint
13
+
14
+ attr_reader :constraint
15
+
16
+ # initialize is a private method in Relationship
17
+ # and private methods can not be "advised" (hooked into)
18
+ # in extlib.
19
+ with_changed_method_visibility(:initialize, :private, :public) do
20
+ before :initialize, :add_constraint_option
21
+ end
22
+
23
+ end
24
+
25
+ class ManyToMany::Relationship
26
+
27
+ OPTIONS << :constraint
28
+
29
+ private
30
+
31
+ # TODO: document
32
+ # @api semipublic
33
+ chainable do
34
+ def one_to_many_options
35
+ super.merge(:constraint => @constraint)
36
+ end
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -1,41 +1,21 @@
1
- require 'dm-constraints/delete_constraint'
1
+ require 'dm-core'
2
+
2
3
  require 'dm-constraints/migrations'
4
+ require 'dm-constraints/delete_constraint'
5
+ require 'dm-constraints/relationships'
6
+ require 'dm-constraints/adapters/dm-abstract-adapter'
3
7
 
4
8
  module DataMapper
5
- module Associations
6
- class OneToMany::Relationship
7
- include Extlib::Hook
8
- include Constraints::DeleteConstraint
9
-
10
- OPTIONS << :constraint
11
-
12
- attr_reader :constraint
13
-
14
- # initialize is a private method in Relationship
15
- # and private methods can not be "advised" (hooked into)
16
- # in extlib.
17
- with_changed_method_visibility(:initialize, :private, :public) do
18
- before :initialize, :add_constraint_option
19
- end
20
- end
21
-
22
- class ManyToMany::Relationship
23
-
24
- OPTIONS << :constraint
25
9
 
26
- private
10
+ extend Constraints::Migrations::SingletonMethods
27
11
 
28
- # TODO: document
29
- # @api semipublic
30
- chainable do
31
- def one_to_many_options
32
- super.merge(:constraint => @constraint)
33
- end
34
- end
35
- end
12
+ module Model
13
+ extend DataMapper::Constraints::Migrations::Model
14
+ append_extensions DataMapper::Constraints::Migrations::Model
36
15
  end
37
16
 
38
17
  module Constraints
18
+
39
19
  include DeleteConstraint
40
20
 
41
21
  module ClassMethods
@@ -57,15 +37,61 @@ module DataMapper
57
37
  RUBY
58
38
  end
59
39
 
40
+ def self.include_constraint_api
41
+ DataMapper::Repository.adapters.values.each do |adapter|
42
+ DataMapper::Adapters.include_constraint_api(ActiveSupport::Inflector.demodulize(adapter.class.name))
43
+ end
44
+ end
45
+
60
46
  Model.append_inclusions self
61
- end
62
47
 
63
- module Migrations
64
- constants.each do |const_name|
65
- if Constraints::Migrations.const_defined?(const_name)
66
- mod = const_get(const_name)
67
- mod.send(:include, Constraints::Migrations.const_get(const_name))
48
+ end # module Constraints
49
+
50
+ module Adapters
51
+
52
+ class AbstractAdapter
53
+ include DataMapper::Constraints::Adapters::AbstractAdapter
54
+ end
55
+
56
+ extend Chainable
57
+
58
+ class << self
59
+
60
+ def include_constraint_api(const_name)
61
+ require constraint_extensions(const_name)
62
+ if Constraints::Adapters.const_defined?(const_name)
63
+ adapter = const_get(const_name)
64
+ adapter.send(:include, constraint_module(const_name))
65
+ end
66
+ rescue LoadError
67
+ # do nothing
68
+ end
69
+
70
+ def constraint_module(const_name)
71
+ Constraints::Adapters.const_get(const_name)
68
72
  end
73
+
74
+ private
75
+
76
+ # @api private
77
+ def constraint_extensions(const_name)
78
+ name = adapter_name(const_name)
79
+ name = 'do' if name == 'dataobjects'
80
+ "dm-constraints/adapters/dm-#{name}-adapter"
81
+ end
82
+
69
83
  end
70
- end
71
- end
84
+
85
+ extendable do
86
+ # @api private
87
+ def const_added(const_name)
88
+ include_constraint_api(const_name)
89
+ super
90
+ end
91
+ end
92
+
93
+ end # module Adapters
94
+
95
+ Constraints.include_constraint_api
96
+
97
+ end # module DataMapper
@@ -1,11 +1,15 @@
1
1
  require 'spec_helper'
2
2
 
3
- ADAPTERS.each do |name, connection_uri|
4
- describe 'DataMapper::Constraints', "(with #{name})" do
3
+ describe 'DataMapper::Constraints', "(with #{DataMapper::Spec.adapter_name})" do
4
+ supported_by :all do
5
5
  before :all do
6
- @adapter = DataMapper.setup(:default, connection_uri)
7
- @repository = DataMapper.repository(@adapter.name)
6
+ @in_memory = defined?(DataMapper::Adapters::InMemoryAdapter) && @adapter.kind_of?(DataMapper::Adapters::InMemoryAdapter)
7
+ @yaml = defined?(DataMapper::Adapters::YamlAdapter) && @adapter.kind_of?(DataMapper::Adapters::YamlAdapter)
8
8
 
9
+ @skip = @in_memory || @yaml
10
+ end
11
+
12
+ before :all do
9
13
  class ::Article
10
14
  include DataMapper::Resource
11
15
 
@@ -72,9 +76,11 @@ ADAPTERS.each do |name, connection_uri|
72
76
  @comment = @author.comments.create(:body => 'So true!')
73
77
  end
74
78
 
75
- it 'should not be able to create related objects with a failing foreign key constraint' do
76
- article = Article.create(:title => 'Man on the Moon')
77
- lambda { Comment.create(:body => 'So true!', :article_id => article_id.id + 1) }.should raise_error
79
+ supported_by :postgres, :mysql do
80
+ it 'should not be able to create related objects with a failing foreign key constraint' do
81
+ article = Article.create(:title => 'Man on the Moon')
82
+ lambda { Comment.create(:body => 'So true!', :article_id => article.id + 1) }.should raise_error(DataObjects::IntegrityError)
83
+ end
78
84
  end
79
85
  end
80
86
 
@@ -180,6 +186,10 @@ ADAPTERS.each do |name, connection_uri|
180
186
  end
181
187
 
182
188
  describe 'many-to-many associations' do
189
+ before do
190
+ pending 'The adapter does not support m:m associations yet' if @skip
191
+ end
192
+
183
193
  before do
184
194
  @author = Author.create(:first_name => 'John', :last_name => 'Doe')
185
195
  @another_author = Author.create(:first_name => 'Joe', :last_name => 'Smith')
@@ -275,6 +285,10 @@ ADAPTERS.each do |name, connection_uri|
275
285
  end
276
286
 
277
287
  describe 'many-to-many associations' do
288
+ before do
289
+ pending 'The adapter does not support m:m associations yet' if @skip
290
+ end
291
+
278
292
  before do
279
293
  @article = Article.create(:title => 'Man on the Moon')
280
294
  @other_article = Article.create(:title => 'Dolly cloned')
@@ -370,6 +384,10 @@ ADAPTERS.each do |name, connection_uri|
370
384
  end
371
385
 
372
386
  describe 'many-to-many associations' do
387
+ before do
388
+ pending 'The adapter does not support m:m associations yet' if @skip
389
+ end
390
+
373
391
  before do
374
392
  @article = Article.create(:title => 'Man on the Moon')
375
393
  @other_article = Article.create(:title => 'Dolly cloned')
@@ -538,6 +556,10 @@ ADAPTERS.each do |name, connection_uri|
538
556
  end
539
557
 
540
558
  describe 'many-to-many associations' do
559
+ before do
560
+ pending 'The adapter does not support m:m associations yet' if @skip
561
+ end
562
+
541
563
  before do
542
564
  @article = Article.create(:title => 'Man on the Moon')
543
565
  @other_article = Article.create(:title => 'Dolly cloned')
@@ -0,0 +1,36 @@
1
+ require 'spec'
2
+ require 'isolated/require_spec'
3
+ require 'dm-core/spec/setup'
4
+ require 'dm-core/spec/lib/adapter_helpers'
5
+
6
+ # To really test this behavior, this spec needs to be run in isolation and not
7
+ # as part of the typical rake spec run, which requires dm-transactions upfront
8
+
9
+ Spec::Runner.configure do |config|
10
+ config.extend(DataMapper::Spec::Adapters::Helpers)
11
+ end
12
+
13
+ describe "require 'dm-constraints' after calling DataMapper.setup" do
14
+
15
+ before(:all) do
16
+
17
+ @adapter = DataMapper::Spec.adapter
18
+ require 'dm-constraints'
19
+
20
+ class ::Person
21
+ include DataMapper::Resource
22
+ property :id, Serial
23
+ has n, :tasks
24
+ end
25
+
26
+ class ::Task
27
+ include DataMapper::Resource
28
+ property :id, Serial
29
+ belongs_to :person
30
+ end
31
+
32
+ end
33
+
34
+ it_should_behave_like "require 'dm-constraints'"
35
+
36
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec'
2
+ require 'isolated/require_spec'
3
+ require 'dm-core/spec/setup'
4
+ require 'dm-core/spec/lib/adapter_helpers'
5
+
6
+ # To really test this behavior, this spec needs to be run in isolation and not
7
+ # as part of the typical rake spec run, which requires dm-transactions upfront
8
+
9
+ Spec::Runner.configure do |config|
10
+ config.extend(DataMapper::Spec::Adapters::Helpers)
11
+ end
12
+
13
+ describe "require 'dm-constraints' before calling DataMapper.setup" do
14
+
15
+ before(:all) do
16
+
17
+ require 'dm-constraints'
18
+ @adapter = DataMapper::Spec.adapter
19
+
20
+ class ::Person
21
+ include DataMapper::Resource
22
+ property :id, Serial
23
+ has n, :tasks
24
+ end
25
+
26
+ class ::Task
27
+ include DataMapper::Resource
28
+ property :id, Serial
29
+ belongs_to :person
30
+ end
31
+
32
+ end
33
+
34
+ it_should_behave_like "require 'dm-constraints'"
35
+
36
+ end
@@ -0,0 +1,14 @@
1
+ shared_examples_for "require 'dm-constraints'" do
2
+
3
+ it "should include the constraint api in the DataMapper namespace" do
4
+ DataMapper::Model.respond_to?(:auto_migrate_down_constraints!, true).should be_true
5
+ DataMapper::Model.respond_to?(:auto_migrate_up_constraints!, true).should be_true
6
+ end
7
+
8
+ it "should include the constraint api into the adapter" do
9
+ @adapter.respond_to?(:constraint_exists? ).should be_true
10
+ @adapter.respond_to?(:create_relationship_constraint ).should be_true
11
+ @adapter.respond_to?(:destroy_relationship_constraint).should be_true
12
+ end
13
+
14
+ end