sbf-dm-constraints 1.3.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +39 -0
- data/.rspec +5 -0
- data/.rubocop.yml +468 -0
- data/Gemfile +70 -0
- data/LICENSE +20 -0
- data/README.rdoc +58 -0
- data/Rakefile +4 -0
- data/dm-constraints.gemspec +21 -0
- data/lib/data_mapper/constraints/adapters/abstract_adapter.rb +33 -0
- data/lib/data_mapper/constraints/adapters/do_adapter.rb +192 -0
- data/lib/data_mapper/constraints/adapters/extension.rb +45 -0
- data/lib/data_mapper/constraints/adapters/mysql_adapter.rb +26 -0
- data/lib/data_mapper/constraints/adapters/oracle_adapter.rb +53 -0
- data/lib/data_mapper/constraints/adapters/postgres_adapter.rb +13 -0
- data/lib/data_mapper/constraints/adapters/sqlite_adapter.rb +22 -0
- data/lib/data_mapper/constraints/adapters/sqlserver_adapter.rb +11 -0
- data/lib/data_mapper/constraints/migrations/model.rb +34 -0
- data/lib/data_mapper/constraints/migrations/relationship.rb +41 -0
- data/lib/data_mapper/constraints/migrations/singleton_methods.rb +44 -0
- data/lib/data_mapper/constraints/relationship/many_to_many.rb +48 -0
- data/lib/data_mapper/constraints/relationship/one_to_many.rb +79 -0
- data/lib/data_mapper/constraints/resource.rb +30 -0
- data/lib/data_mapper/constraints/version.rb +5 -0
- data/lib/dm-constraints.rb +19 -0
- data/spec/integration/constraints_spec.rb +630 -0
- data/spec/isolated/require_after_setup_spec.rb +36 -0
- data/spec/isolated/require_before_setup_spec.rb +35 -0
- data/spec/isolated/require_spec.rb +15 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec_helper.rb +26 -0
- data/tasks/spec.rake +21 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +94 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'data_mapper/constraints/adapters/extension'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
module Constraints
|
5
|
+
module Adapters
|
6
|
+
module AbstractAdapter
|
7
|
+
# @api private
|
8
|
+
def constraint_exists?(*)
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
def create_relationship_constraint(*)
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
def destroy_relationship_constraint(*)
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Adapters::AbstractAdapter.class_eval do
|
26
|
+
include Constraints::Adapters::AbstractAdapter
|
27
|
+
end
|
28
|
+
|
29
|
+
Adapters::AbstractAdapter.descendants.each do |adapter_class|
|
30
|
+
const_name = DataMapper::Inflector.demodulize(adapter_class.name)
|
31
|
+
Adapters.include_constraint_api(const_name)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Constraints
|
3
|
+
module Adapters
|
4
|
+
module DataObjectsAdapter
|
5
|
+
##
|
6
|
+
# Determine if a constraint exists for a table
|
7
|
+
#
|
8
|
+
# @param storage_name [Symbol]
|
9
|
+
# name of table to check constraint on
|
10
|
+
# @param constraint_name [~String]
|
11
|
+
# name of constraint to check for
|
12
|
+
#
|
13
|
+
# @return [Boolean]
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
def constraint_exists?(storage_name, constraint_name)
|
17
|
+
statement = DataMapper::Ext::String.compress_lines(<<-SQL)
|
18
|
+
SELECT COUNT(*)
|
19
|
+
FROM #{quote_name('information_schema')}.#{quote_name('table_constraints')}
|
20
|
+
WHERE #{quote_name('constraint_type')} = 'FOREIGN KEY'
|
21
|
+
AND #{quote_name('table_schema')} = ?
|
22
|
+
AND #{quote_name('table_name')} = ?
|
23
|
+
AND #{quote_name('constraint_name')} = ?
|
24
|
+
SQL
|
25
|
+
|
26
|
+
select(statement, schema_name, storage_name, constraint_name).first > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Create the constraint for a relationship
|
31
|
+
#
|
32
|
+
# @param relationship [Relationship]
|
33
|
+
# the relationship to create the constraint for
|
34
|
+
#
|
35
|
+
# @return [true, false]
|
36
|
+
# true if creating the constraints was successful
|
37
|
+
#
|
38
|
+
# @api semipublic
|
39
|
+
def create_relationship_constraint(relationship)
|
40
|
+
return false unless valid_relationship_for_constraint?(relationship)
|
41
|
+
|
42
|
+
source_storage_name = relationship.source_model.storage_name(name)
|
43
|
+
target_storage_name = relationship.target_model.storage_name(name)
|
44
|
+
constraint_name = constraint_name(source_storage_name, relationship.name)
|
45
|
+
|
46
|
+
return false if constraint_exists?(source_storage_name, constraint_name)
|
47
|
+
|
48
|
+
constraint_type =
|
49
|
+
case relationship.inverse.constraint
|
50
|
+
when :protect then 'NO ACTION'
|
51
|
+
# TODO: support :cascade as an option:
|
52
|
+
# (destroy doesn't communicate the UPDATE constraint)
|
53
|
+
when :destroy, :destroy! then 'CASCADE'
|
54
|
+
when :set_nil then 'SET NULL'
|
55
|
+
end
|
56
|
+
|
57
|
+
return false if constraint_type.nil?
|
58
|
+
|
59
|
+
source_keys = relationship.source_key.map { |p| property_to_column_name(p, false) }
|
60
|
+
target_keys = relationship.target_key.map { |p| property_to_column_name(p, false) }
|
61
|
+
|
62
|
+
create_constraints_statement = create_constraints_statement(
|
63
|
+
constraint_name,
|
64
|
+
constraint_type,
|
65
|
+
source_storage_name,
|
66
|
+
source_keys,
|
67
|
+
target_storage_name,
|
68
|
+
target_keys
|
69
|
+
)
|
70
|
+
|
71
|
+
execute(create_constraints_statement)
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Remove the constraint for a relationship
|
76
|
+
#
|
77
|
+
# @param relationship [Relationship]
|
78
|
+
# the relationship to remove the constraint for
|
79
|
+
#
|
80
|
+
# @return [true, false]
|
81
|
+
# true if destroying the constraint was successful
|
82
|
+
#
|
83
|
+
# @api semipublic
|
84
|
+
def destroy_relationship_constraint(relationship)
|
85
|
+
return false unless valid_relationship_for_constraint?(relationship)
|
86
|
+
|
87
|
+
storage_name = relationship.source_model.storage_name(name)
|
88
|
+
constraint_name = constraint_name(storage_name, relationship.name)
|
89
|
+
|
90
|
+
return false unless constraint_exists?(storage_name, constraint_name)
|
91
|
+
|
92
|
+
destroy_constraints_statement =
|
93
|
+
destroy_constraints_statement(storage_name, constraint_name)
|
94
|
+
|
95
|
+
execute(destroy_constraints_statement)
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Check to see if the relationship's constraints can be used
|
100
|
+
#
|
101
|
+
# Only one-to-one, one-to-many, and many-to-many relationships
|
102
|
+
# can be used for constraints. They must also be in the same
|
103
|
+
# repository as the adapter is connected to.
|
104
|
+
#
|
105
|
+
# @param relationship [Relationship]
|
106
|
+
# the relationship to check
|
107
|
+
#
|
108
|
+
# @return [true, false]
|
109
|
+
# true if a constraint can be established for relationship
|
110
|
+
#
|
111
|
+
# @api private
|
112
|
+
private def valid_relationship_for_constraint?(relationship)
|
113
|
+
return false unless relationship.source_repository_name == name || relationship.source_repository_name.nil?
|
114
|
+
return false unless relationship.target_repository_name == name || relationship.target_repository_name.nil?
|
115
|
+
return false unless relationship.is_a?(Associations::ManyToOne::Relationship)
|
116
|
+
|
117
|
+
true
|
118
|
+
end
|
119
|
+
|
120
|
+
module SQL
|
121
|
+
# Generates the SQL statement to create a constraint
|
122
|
+
#
|
123
|
+
# @param [String] constraint_name
|
124
|
+
# name of the foreign key constraint
|
125
|
+
# @param [String] constraint_type
|
126
|
+
# type of constraint to ALTER source_storage_name with
|
127
|
+
# @param [String] source_storage_name
|
128
|
+
# name of table to ALTER with constraint
|
129
|
+
# @param [Array(String)] source_keys
|
130
|
+
# columns in source_storage_name that refer to foreign table
|
131
|
+
# @param [String] target_storage_name
|
132
|
+
# target table of the constraint
|
133
|
+
# @param [Array(String)] target_keys
|
134
|
+
# columns the target table that are referred to
|
135
|
+
#
|
136
|
+
# @return [String]
|
137
|
+
# SQL DDL Statement to create a constraint
|
138
|
+
#
|
139
|
+
# @api private
|
140
|
+
private def create_constraints_statement(constraint_name, constraint_type, source_storage_name, source_keys, target_storage_name, target_keys)
|
141
|
+
DataMapper::Ext::String.compress_lines(<<-SQL)
|
142
|
+
ALTER TABLE #{quote_name(source_storage_name)}
|
143
|
+
ADD CONSTRAINT #{quote_name(constraint_name)}
|
144
|
+
FOREIGN KEY (#{source_keys.join(', ')})
|
145
|
+
REFERENCES #{quote_name(target_storage_name)} (#{target_keys.join(', ')})
|
146
|
+
ON DELETE #{constraint_type}
|
147
|
+
ON UPDATE #{constraint_type}
|
148
|
+
SQL
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Generates the SQL statement to destroy a constraint
|
153
|
+
#
|
154
|
+
# @param [String] storage_name
|
155
|
+
# name of table to constrain
|
156
|
+
# @param [String] constraint_name
|
157
|
+
# name of foreign key constraint
|
158
|
+
#
|
159
|
+
# @return [String]
|
160
|
+
# SQL DDL Statement to destroy a constraint
|
161
|
+
#
|
162
|
+
# @api private
|
163
|
+
private def destroy_constraints_statement(storage_name, constraint_name)
|
164
|
+
DataMapper::Ext::String.compress_lines(<<-SQL)
|
165
|
+
ALTER TABLE #{quote_name(storage_name)}
|
166
|
+
DROP CONSTRAINT #{quote_name(constraint_name)}
|
167
|
+
SQL
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# generates a unique constraint name given a table and a relationships
|
172
|
+
#
|
173
|
+
# @param [String] storage_name
|
174
|
+
# name of table to constrain
|
175
|
+
# @param [String] relationship_name
|
176
|
+
# name of the relationship to constrain
|
177
|
+
#
|
178
|
+
# @return [String]
|
179
|
+
# name of the constraint
|
180
|
+
#
|
181
|
+
# @api private
|
182
|
+
private def constraint_name(storage_name, relationship_name)
|
183
|
+
identifier = "#{storage_name}_#{relationship_name}"[0, self.class::IDENTIFIER_MAX_LENGTH - 3]
|
184
|
+
"#{identifier}_fk"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
include SQL
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Constraints
|
3
|
+
module Adapters
|
4
|
+
module Extension
|
5
|
+
# Include the corresponding Constraints module into a adapter class
|
6
|
+
#
|
7
|
+
# @param [Symbol] const_name
|
8
|
+
# demodulized name of the adapter class to include corresponding
|
9
|
+
# constraints module into
|
10
|
+
#
|
11
|
+
# TODO: come up with a better way to include modules
|
12
|
+
# into all currently loaded and subsequently loaded Adapters
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
def include_constraint_api(const_name)
|
16
|
+
require constraint_extensions(const_name)
|
17
|
+
|
18
|
+
if Constraints::Adapters.const_defined?(const_name)
|
19
|
+
adapter = const_get(const_name)
|
20
|
+
constraint_module = Constraints::Adapters.const_get(const_name)
|
21
|
+
adapter.class_eval { include constraint_module }
|
22
|
+
end
|
23
|
+
rescue LoadError
|
24
|
+
# Silently ignore the fact that no adapter extensions could be required
|
25
|
+
# This means that the adapter in use doesn't support constraints
|
26
|
+
end
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
private def constraint_extensions(const_name)
|
30
|
+
name = adapter_name(const_name)
|
31
|
+
name = 'do' if name == 'dataobjects'
|
32
|
+
"data_mapper/constraints/adapters/#{name}_adapter"
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
private def const_added(const_name)
|
37
|
+
include_constraint_api(const_name)
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Adapters.extend Constraints::Adapters::Extension
|
45
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'data_mapper/constraints/adapters/do_adapter'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
module Constraints
|
5
|
+
module Adapters
|
6
|
+
module MysqlAdapter
|
7
|
+
include SQL, DataObjectsAdapter
|
8
|
+
|
9
|
+
module SQL
|
10
|
+
##
|
11
|
+
# MySQL specific query to drop a foreign key
|
12
|
+
#
|
13
|
+
# @see DataMapper::Constraints::Adapters::DataObjectsAdapter#destroy_constraints_statement
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
private def destroy_constraints_statement(storage_name, constraint_name)
|
17
|
+
DataMapper::Ext::String.compress_lines(<<-SQL)
|
18
|
+
ALTER TABLE #{quote_name(storage_name)}
|
19
|
+
DROP FOREIGN KEY #{quote_name(constraint_name)}
|
20
|
+
SQL
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'data_mapper/constraints/adapters/do_adapter'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
module Constraints
|
5
|
+
module Adapters
|
6
|
+
module OracleAdapter
|
7
|
+
include DataObjectsAdapter
|
8
|
+
|
9
|
+
# oracle does not provide the information_schema table
|
10
|
+
# To question internal state like postgres or mysql
|
11
|
+
#
|
12
|
+
# @see DataMapper::Constraints::Adapters::DataObjectsAdapter
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
def constraint_exists?(storage_name, constraint_name)
|
16
|
+
statement = DataMapper::Ext::String.compress_lines(<<-SQL)
|
17
|
+
SELECT COUNT(*)
|
18
|
+
FROM USER_CONSTRAINTS
|
19
|
+
WHERE table_name = ?
|
20
|
+
AND constraint_name = ?
|
21
|
+
SQL
|
22
|
+
|
23
|
+
select(statement, oracle_upcase(storage_name)[0, self.class::IDENTIFIER_MAX_LENGTH].gsub('"', '_'), oracle_upcase(constraint_name)[0, self.class::IDENTIFIER_MAX_LENGTH].gsub('"', '_')).first > 0
|
24
|
+
end
|
25
|
+
|
26
|
+
# @see DataMapper::Constraints::Adapters::DataObjectsAdapter#create_constraints_statement
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
#
|
30
|
+
# TODO: is it desirable to always set `INITIALLY DEFERRED DEFERRABLE`?
|
31
|
+
def create_constraints_statement(constraint_name, constraint_type, source_storage_name, source_keys, target_storage_name, target_keys)
|
32
|
+
DataMapper::Ext::String.compress_lines(<<-SQL)
|
33
|
+
ALTER TABLE #{quote_name(source_storage_name)}
|
34
|
+
ADD CONSTRAINT #{quote_name(constraint_name)}
|
35
|
+
FOREIGN KEY (#{source_keys.join(', ')})
|
36
|
+
REFERENCES #{quote_name(target_storage_name)} (#{target_keys.join(', ')})
|
37
|
+
#{"ON DELETE #{constraint_type}" if constraint_type && constraint_type != 'NO ACTION'}
|
38
|
+
INITIALLY DEFERRED DEFERRABLE
|
39
|
+
SQL
|
40
|
+
end
|
41
|
+
|
42
|
+
# @api private
|
43
|
+
def destroy_constraints_statement(storage_name, constraint_name)
|
44
|
+
DataMapper::Ext::String.compress_lines(<<-SQL)
|
45
|
+
ALTER TABLE #{quote_name(storage_name)}
|
46
|
+
DROP CONSTRAINT #{quote_name(constraint_name)}
|
47
|
+
CASCADE
|
48
|
+
SQL
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Constraints
|
3
|
+
module Adapters
|
4
|
+
module SqliteAdapter
|
5
|
+
# @api private
|
6
|
+
def constraint_exists?(*)
|
7
|
+
false
|
8
|
+
end
|
9
|
+
|
10
|
+
# @api private
|
11
|
+
def create_relationship_constraint(*)
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
# @api private
|
16
|
+
def destroy_relationship_constraint(*)
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# TODO: figure out some other (less tightly coupled) way to ensure that
|
2
|
+
# dm-migrations' method implementations are loaded before this file
|
3
|
+
require 'dm-migrations/auto_migration'
|
4
|
+
|
5
|
+
module DataMapper
|
6
|
+
module Constraints
|
7
|
+
module Migrations
|
8
|
+
module Model
|
9
|
+
# @api private
|
10
|
+
def auto_migrate_constraints_up(repository_name = self.repository_name)
|
11
|
+
# TODO: this check should not be here
|
12
|
+
return if respond_to?(:is_remixable?) && is_remixable?
|
13
|
+
|
14
|
+
relationships(repository_name).each do |relationship|
|
15
|
+
relationship.auto_migrate_constraints_up(repository_name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
def auto_migrate_constraints_down(repository_name = self.repository_name)
|
21
|
+
return unless storage_exists?(repository_name)
|
22
|
+
# TODO: this check should not be here
|
23
|
+
return if respond_to?(:is_remixable?) && is_remixable?
|
24
|
+
|
25
|
+
relationships(repository_name).each do |relationship|
|
26
|
+
relationship.auto_migrate_constraints_down(repository_name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Model.append_extensions Constraints::Migrations::Model
|
34
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Constraints
|
3
|
+
module Migrations
|
4
|
+
module Relationship
|
5
|
+
# @api private
|
6
|
+
def auto_migrate_constraints_up(_repository_name)
|
7
|
+
# no-op
|
8
|
+
end
|
9
|
+
|
10
|
+
# @api private
|
11
|
+
def auto_migrate_constraints_down(_repository_name)
|
12
|
+
# no-op
|
13
|
+
end
|
14
|
+
|
15
|
+
module ManyToOne
|
16
|
+
# @api private
|
17
|
+
def auto_migrate_constraints_up(repository_name)
|
18
|
+
adapter = DataMapper.repository(repository_name)&.adapter
|
19
|
+
adapter&.create_relationship_constraint(self)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
def auto_migrate_constraints_down(repository_name)
|
25
|
+
adapter = DataMapper.repository(repository_name)&.adapter
|
26
|
+
adapter&.destroy_relationship_constraint(self)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Associations::Relationship.class_eval do
|
35
|
+
include Constraints::Migrations::Relationship
|
36
|
+
end
|
37
|
+
|
38
|
+
Associations::ManyToOne::Relationship.class_eval do
|
39
|
+
include Constraints::Migrations::Relationship::ManyToOne
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Constraints
|
3
|
+
module Migrations
|
4
|
+
module SingletonMethods
|
5
|
+
def auto_migrate!(repository_name = nil)
|
6
|
+
auto_migrate_constraints_down(repository_name)
|
7
|
+
# TODO: Model#auto_migrate! drops and adds constraints, as well.
|
8
|
+
# is that an avoidable duplication?
|
9
|
+
super
|
10
|
+
auto_migrate_constraints_up(repository_name)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
private def auto_migrate_down!(repository_name = nil)
|
15
|
+
auto_migrate_constraints_down(repository_name)
|
16
|
+
super
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
private def auto_migrate_up!(repository_name = nil)
|
21
|
+
super
|
22
|
+
auto_migrate_constraints_up(repository_name)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
private def auto_migrate_constraints_up(repository_name = nil)
|
28
|
+
DataMapper::Model.descendants.each do |model|
|
29
|
+
model.auto_migrate_constraints_up(repository_name || model.default_repository_name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
private def auto_migrate_constraints_down(repository_name = nil)
|
35
|
+
DataMapper::Model.descendants.each do |model|
|
36
|
+
model.auto_migrate_constraints_down(repository_name || model.default_repository_name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
extend Constraints::Migrations::SingletonMethods
|
44
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'data_mapper/constraints/relationship/one_to_many'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
module Constraints
|
5
|
+
module Relationship
|
6
|
+
module ManyToMany
|
7
|
+
|
8
|
+
private def one_to_many_options
|
9
|
+
super.merge(constraint: @constraint)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Checks that the constraint type is appropriate to the relationship
|
13
|
+
#
|
14
|
+
# @param [Fixnum] cardinality
|
15
|
+
# cardinality of relationship
|
16
|
+
# @param [Symbol] name
|
17
|
+
# name of relationship to evaluate constraint of
|
18
|
+
# @param [Hash] options
|
19
|
+
# options hash
|
20
|
+
#
|
21
|
+
# @option *args :constraint[Symbol]
|
22
|
+
# one of VALID_CONSTRAINT_VALUES
|
23
|
+
#
|
24
|
+
# @raise ArgumentError
|
25
|
+
# if @option :constraint is not one of VALID_CONSTRAINT_TYPES
|
26
|
+
#
|
27
|
+
# @return [Undefined]
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
private def assert_valid_constraint
|
31
|
+
super
|
32
|
+
|
33
|
+
# TODO: is any constraint valid for a m:m relationship?
|
34
|
+
return unless @constraint == :set_nil
|
35
|
+
|
36
|
+
raise ArgumentError, "#{@constraint} is not a valid constraint type for #{self.class}"
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Associations::ManyToMany::Relationship::OPTIONS << :constraint
|
44
|
+
|
45
|
+
Associations::ManyToMany::Relationship.class_eval do
|
46
|
+
include Constraints::Relationship::ManyToMany
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Constraints
|
3
|
+
module Relationship
|
4
|
+
module OneToMany
|
5
|
+
attr_reader :constraint
|
6
|
+
|
7
|
+
# @api private
|
8
|
+
def enforce_destroy_constraint(resource)
|
9
|
+
return true unless (association = get(resource))
|
10
|
+
|
11
|
+
constraint = self.constraint
|
12
|
+
|
13
|
+
case constraint
|
14
|
+
when :protect
|
15
|
+
Array(association).empty?
|
16
|
+
when :destroy, :destroy!
|
17
|
+
association.__send__(constraint)
|
18
|
+
when :set_nil
|
19
|
+
Array(association).all? do |r|
|
20
|
+
r.update(inverse => nil)
|
21
|
+
end
|
22
|
+
when :skip
|
23
|
+
true # do nothing
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Adds the delete constraint options to a relationship
|
29
|
+
#
|
30
|
+
# @param args [*args] Arguments passed to Relationship#initialize
|
31
|
+
#
|
32
|
+
# @return [nil]
|
33
|
+
#
|
34
|
+
# @api private
|
35
|
+
private def initialize(*args)
|
36
|
+
super
|
37
|
+
set_constraint
|
38
|
+
assert_valid_constraint
|
39
|
+
end
|
40
|
+
|
41
|
+
private def set_constraint
|
42
|
+
@constraint = @options.fetch(:constraint, :protect) || :skip
|
43
|
+
end
|
44
|
+
|
45
|
+
# Checks that the constraint type is appropriate to the relationship
|
46
|
+
#
|
47
|
+
# @param [Fixnum] cardinality
|
48
|
+
# cardinality of relationship
|
49
|
+
# @param [Symbol] name
|
50
|
+
# name of relationship to evaluate constraint of
|
51
|
+
# @param [Hash] options
|
52
|
+
# options hash
|
53
|
+
#
|
54
|
+
# @option *args :constraint[Symbol]
|
55
|
+
# one of VALID_CONSTRAINT_VALUES
|
56
|
+
#
|
57
|
+
# @raise ArgumentError
|
58
|
+
# if @option :constraint is not one of VALID_CONSTRAINT_VALUES
|
59
|
+
#
|
60
|
+
# @return [Undefined]
|
61
|
+
#
|
62
|
+
# @api semipublic
|
63
|
+
private def assert_valid_constraint
|
64
|
+
return unless @constraint
|
65
|
+
|
66
|
+
return if VALID_CONSTRAINT_VALUES.include?(@constraint)
|
67
|
+
|
68
|
+
raise ArgumentError, ":constraint option must be one of #{VALID_CONSTRAINT_VALUES.to_a.join(', ')}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
Associations::OneToMany::Relationship::OPTIONS << :constraint
|
75
|
+
|
76
|
+
Associations::OneToMany::Relationship.class_eval do
|
77
|
+
include Constraints::Relationship::OneToMany
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Constraints
|
3
|
+
module Resource
|
4
|
+
def before_destroy_hook
|
5
|
+
enforce_destroy_constraints
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
# Check delete constraints prior to destroying a dm resource or collection
|
10
|
+
#
|
11
|
+
# @note
|
12
|
+
# - It only considers a relationship's constraints if this is the parent model (ie a child shouldn't delete a parent)
|
13
|
+
# - Many to Many Relationships are skipped, as they are evaluated by their underlying 1:M relationships
|
14
|
+
#
|
15
|
+
# @return [nil]
|
16
|
+
#
|
17
|
+
# @api semi-public
|
18
|
+
private def enforce_destroy_constraints
|
19
|
+
relationships.each do |relationship|
|
20
|
+
next unless relationship.respond_to?(:enforce_destroy_constraint)
|
21
|
+
|
22
|
+
constraint_satisfied = relationship.enforce_destroy_constraint(self)
|
23
|
+
|
24
|
+
throw(:halt, false) unless constraint_satisfied
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
Model.append_inclusions Constraints::Resource
|
30
|
+
end
|