sbf-dm-constraints 1.3.0.beta
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.
- 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
|