dm-constraints 0.10.2 → 1.0.0.rc1

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.
@@ -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