dm-core 0.10.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. data/.autotest +29 -0
  2. data/.document +5 -0
  3. data/.gitignore +27 -0
  4. data/LICENSE +20 -0
  5. data/{README.txt → README.rdoc} +14 -3
  6. data/Rakefile +23 -22
  7. data/VERSION +1 -0
  8. data/dm-core.gemspec +201 -10
  9. data/lib/dm-core.rb +32 -23
  10. data/lib/dm-core/adapters.rb +0 -1
  11. data/lib/dm-core/adapters/data_objects_adapter.rb +230 -151
  12. data/lib/dm-core/adapters/mysql_adapter.rb +7 -8
  13. data/lib/dm-core/adapters/oracle_adapter.rb +39 -59
  14. data/lib/dm-core/adapters/postgres_adapter.rb +0 -1
  15. data/lib/dm-core/adapters/sqlite3_adapter.rb +5 -0
  16. data/lib/dm-core/adapters/sqlserver_adapter.rb +114 -0
  17. data/lib/dm-core/adapters/yaml_adapter.rb +0 -5
  18. data/lib/dm-core/associations/many_to_many.rb +118 -56
  19. data/lib/dm-core/associations/many_to_one.rb +48 -21
  20. data/lib/dm-core/associations/one_to_many.rb +8 -30
  21. data/lib/dm-core/associations/one_to_one.rb +1 -5
  22. data/lib/dm-core/associations/relationship.rb +89 -97
  23. data/lib/dm-core/collection.rb +299 -184
  24. data/lib/dm-core/core_ext/enumerable.rb +28 -0
  25. data/lib/dm-core/core_ext/kernel.rb +0 -2
  26. data/lib/dm-core/migrations.rb +314 -170
  27. data/lib/dm-core/model.rb +97 -66
  28. data/lib/dm-core/model/descendant_set.rb +1 -1
  29. data/lib/dm-core/model/hook.rb +0 -3
  30. data/lib/dm-core/model/property.rb +7 -10
  31. data/lib/dm-core/model/relationship.rb +79 -26
  32. data/lib/dm-core/model/scope.rb +3 -4
  33. data/lib/dm-core/property.rb +152 -90
  34. data/lib/dm-core/property_set.rb +18 -37
  35. data/lib/dm-core/query.rb +452 -153
  36. data/lib/dm-core/query/conditions/comparison.rb +266 -173
  37. data/lib/dm-core/query/conditions/operation.rb +499 -57
  38. data/lib/dm-core/query/direction.rb +0 -3
  39. data/lib/dm-core/query/operator.rb +0 -4
  40. data/lib/dm-core/query/path.rb +10 -12
  41. data/lib/dm-core/query/sort.rb +4 -10
  42. data/lib/dm-core/repository.rb +10 -6
  43. data/lib/dm-core/resource.rb +343 -148
  44. data/lib/dm-core/spec/adapter_shared_spec.rb +17 -1
  45. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +277 -17
  46. data/lib/dm-core/support/chainable.rb +0 -2
  47. data/lib/dm-core/support/equalizer.rb +27 -3
  48. data/lib/dm-core/transaction.rb +75 -75
  49. data/lib/dm-core/type.rb +19 -5
  50. data/lib/dm-core/types/discriminator.rb +4 -4
  51. data/lib/dm-core/types/object.rb +2 -7
  52. data/lib/dm-core/types/paranoid_boolean.rb +8 -2
  53. data/lib/dm-core/types/paranoid_datetime.rb +8 -2
  54. data/lib/dm-core/version.rb +1 -1
  55. data/script/performance.rb +7 -7
  56. data/script/profile.rb +6 -6
  57. data/spec/lib/collection_helpers.rb +2 -2
  58. data/spec/lib/pending_helpers.rb +22 -3
  59. data/spec/lib/rspec_immediate_feedback_formatter.rb +1 -0
  60. data/spec/public/associations/many_to_many_spec.rb +6 -4
  61. data/spec/public/associations/many_to_one_spec.rb +10 -1
  62. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +39 -0
  63. data/spec/public/associations/one_to_many_spec.rb +4 -3
  64. data/spec/public/associations/one_to_one_spec.rb +19 -1
  65. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +45 -0
  66. data/spec/public/collection_spec.rb +4 -3
  67. data/spec/public/migrations_spec.rb +144 -0
  68. data/spec/public/model/relationship_spec.rb +115 -55
  69. data/spec/public/model_spec.rb +13 -13
  70. data/spec/public/property/object_spec.rb +106 -0
  71. data/spec/public/property_spec.rb +18 -14
  72. data/spec/public/resource_spec.rb +10 -1
  73. data/spec/public/sel_spec.rb +16 -49
  74. data/spec/public/setup_spec.rb +1 -1
  75. data/spec/public/shared/association_collection_shared_spec.rb +6 -14
  76. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  77. data/spec/public/shared/collection_shared_spec.rb +214 -217
  78. data/spec/public/shared/finder_shared_spec.rb +259 -365
  79. data/spec/public/shared/resource_shared_spec.rb +524 -248
  80. data/spec/public/transaction_spec.rb +27 -3
  81. data/spec/public/types/discriminator_spec.rb +1 -1
  82. data/spec/rcov.opts +6 -0
  83. data/spec/semipublic/adapters/sqlserver_adapter_spec.rb +17 -0
  84. data/spec/semipublic/associations/many_to_one_spec.rb +3 -20
  85. data/spec/semipublic/associations_spec.rb +2 -2
  86. data/spec/semipublic/collection_spec.rb +0 -32
  87. data/spec/semipublic/model_spec.rb +96 -0
  88. data/spec/semipublic/property_spec.rb +3 -3
  89. data/spec/semipublic/query/conditions/comparison_spec.rb +1719 -0
  90. data/spec/semipublic/query/conditions/operation_spec.rb +1292 -0
  91. data/spec/semipublic/query_spec.rb +1285 -144
  92. data/spec/semipublic/resource_spec.rb +0 -24
  93. data/spec/semipublic/shared/resource_shared_spec.rb +103 -38
  94. data/spec/spec.opts +1 -1
  95. data/spec/spec_helper.rb +15 -6
  96. data/tasks/ci.rake +1 -0
  97. data/tasks/metrics.rake +37 -0
  98. data/tasks/spec.rake +41 -0
  99. data/tasks/yard.rake +9 -0
  100. data/tasks/yardstick.rake +19 -0
  101. metadata +99 -29
  102. data/CONTRIBUTING +0 -51
  103. data/FAQ +0 -93
  104. data/History.txt +0 -27
  105. data/MIT-LICENSE +0 -22
  106. data/Manifest.txt +0 -121
  107. data/QUICKLINKS +0 -11
  108. data/SPECS +0 -35
  109. data/TODO +0 -1
  110. data/spec/semipublic/query/conditions_spec.rb +0 -528
  111. data/tasks/ci.rb +0 -24
  112. data/tasks/dm.rb +0 -58
  113. data/tasks/doc.rb +0 -17
  114. data/tasks/gemspec.rb +0 -23
  115. data/tasks/hoe.rb +0 -45
  116. data/tasks/install.rb +0 -18
@@ -10,25 +10,24 @@ module DataMapper
10
10
 
11
11
  private
12
12
 
13
- # TODO: document
14
13
  # @api private
15
14
  def supports_default_values? #:nodoc:
16
15
  false
17
16
  end
18
17
 
19
- # TODO: document
20
18
  # @api private
21
- def regexp_operator(operand)
22
- 'REGEXP'
19
+ def supports_subquery?(query, source_key, target_key, qualify)
20
+ # TODO: renable once query does not include target_model for deletes and updates
21
+ # query.limit.nil?
22
+
23
+ false
23
24
  end
24
25
 
25
- # TODO: document
26
26
  # @api private
27
- def not_regexp_operator(operand)
28
- 'NOT REGEXP'
27
+ def regexp_operator(operand)
28
+ 'REGEXP'
29
29
  end
30
30
 
31
- # TODO: document
32
31
  # @api private
33
32
  def quote_name(name)
34
33
  "`#{name[0, self.class::IDENTIFIER_MAX_LENGTH].gsub('`', '``')}`"
@@ -24,10 +24,12 @@ module DataMapper
24
24
  def insert_statement(model, properties, serial)
25
25
  statement = "INSERT INTO #{quote_name(model.storage_name(name))} "
26
26
 
27
+ no_properties = properties.empty?
27
28
  custom_sequence = serial && serial.options[:sequence]
29
+ serial_field = serial && quote_name(serial.field)
28
30
 
29
- if supports_default_values? && properties.empty? && !custom_sequence
30
- statement << "(#{quote_name(serial.field)}) " if serial
31
+ if supports_default_values? && no_properties && !custom_sequence
32
+ statement << "(#{serial_field}) " if serial
31
33
  statement << default_values_clause
32
34
  else
33
35
  # do not use custom sequence if identity field was assigned a value
@@ -36,14 +38,14 @@ module DataMapper
36
38
  end
37
39
  statement << "("
38
40
  if custom_sequence
39
- statement << "#{quote_name(serial.field)}"
40
- statement << ", " unless properties.empty?
41
+ statement << "#{serial_field}"
42
+ statement << ", " unless no_properties
41
43
  end
42
- statement << "#{properties.map { |p| quote_name(p.field) }.join(', ')}) "
44
+ statement << "#{properties.map { |property| quote_name(property.field) }.join(', ')}) "
43
45
  statement << "VALUES ("
44
46
  if custom_sequence
45
47
  statement << "#{quote_name(custom_sequence)}.NEXTVAL"
46
- statement << ", " unless properties.empty?
48
+ statement << ", " unless no_properties
47
49
  end
48
50
  statement << "#{(['?'] * properties.size).join(', ')})"
49
51
  end
@@ -60,7 +62,6 @@ module DataMapper
60
62
  'VALUES (DEFAULT)'
61
63
  end
62
64
 
63
- # TODO: document
64
65
  # @api private
65
66
  def supports_returning?
66
67
  true
@@ -78,6 +79,7 @@ module DataMapper
78
79
  #
79
80
  # @api private
80
81
  def select_statement(query)
82
+ name = self.name
81
83
  model = query.model
82
84
  fields = query.fields
83
85
  conditions = query.conditions
@@ -96,14 +98,16 @@ module DataMapper
96
98
  qualify = query.links.any?
97
99
 
98
100
  if query.unique?
99
- group_by = fields.select { |p| p.kind_of?(Property) }
101
+ group_by = fields.select { |property| property.kind_of?(Property) }
100
102
  end
101
103
 
102
104
  # create subquery to find all valid keys and then use these keys to retrive all other columns
103
105
  use_subquery = qualify
106
+ no_group_by = group_by.blank?
107
+ no_order = order.blank?
104
108
 
105
109
  # when we can include ROWNUM condition in main WHERE clause
106
- use_simple_rownum_limit = limit && (offset||0 == 0) && group_by.blank? && order.blank?
110
+ use_simple_rownum_limit = limit && (offset||0 == 0) && no_group_by && no_order
107
111
 
108
112
  unless (limit && limit > 1) || offset > 0 || qualify
109
113
  # TODO: move this method to Query, so that it walks the conditions
@@ -114,25 +118,31 @@ module DataMapper
114
118
 
115
119
  # if a unique property is used, and there is no OR operator, then an ORDER
116
120
  # and LIMIT are unecessary because it should only return a single row
117
- if conditions.kind_of?(Query::Conditions::AndOperation) &&
118
- conditions.any? { |operand| operand.kind_of?(Query::Conditions::EqualToComparison) && operand.subject.respond_to?(:unique?) && operand.subject.unique? } &&
119
- !conditions.any? { |operand| operand.kind_of?(Query::Conditions::OrOperation) }
121
+ if conditions.respond_to?(:slug) && conditions.slug == :and &&
122
+ conditions.any? { |operand| operand.respond_to?(:slug) && operand.slug == :eql && operand.subject.respond_to?(:unique?) && operand.subject.unique? } &&
123
+ !conditions.any? { |operand| operand.respond_to?(:slug) && operand.slug == :or }
120
124
  order = nil
125
+ no_order = true
121
126
  limit = nil
122
127
  end
123
128
  end
124
129
 
125
130
  conditions_statement, bind_values = conditions_statement(conditions, qualify)
126
131
 
132
+ model_key_column = columns_statement(model.key(name), qualify)
133
+ from_statement = " FROM #{quote_name(model.storage_name(name))}"
134
+
127
135
  statement = "SELECT #{columns_statement(fields, qualify)}"
128
136
  if use_subquery
129
- statement << " FROM #{quote_name(model.storage_name(name))}"
130
- statement << " WHERE (#{columns_statement(model.key, qualify)}) IN"
131
- statement << " (SELECT DISTINCT #{columns_statement(model.key, qualify)}"
137
+ statement << from_statement
138
+ statement << " WHERE (#{model_key_column}) IN"
139
+ statement << " (SELECT DISTINCT #{model_key_column}"
140
+ # do not need to do group by for uniqueness as just one row per primary key will be returned
141
+ no_group_by = true
132
142
  end
133
- statement << " FROM #{quote_name(model.storage_name(name))}"
134
- statement << join_statement(query, qualify) if qualify
135
- statement << " WHERE (#{conditions_statement})" unless conditions_statement.blank?
143
+ statement << from_statement
144
+ statement << join_statement(query, qualify) if qualify
145
+ statement << " WHERE (#{conditions_statement})" unless conditions_statement.blank?
136
146
  if use_subquery
137
147
  statement << ")"
138
148
  end
@@ -140,8 +150,8 @@ module DataMapper
140
150
  statement << " AND rownum <= ?"
141
151
  bind_values << limit
142
152
  end
143
- statement << " GROUP BY #{columns_statement(group_by, qualify)}" unless group_by.blank?
144
- statement << " ORDER BY #{order_statement(order, qualify)}" unless order.blank?
153
+ statement << " GROUP BY #{columns_statement(group_by, qualify)}" unless no_group_by
154
+ statement << " ORDER BY #{order_statement(order, qualify)}" unless no_order
145
155
 
146
156
  add_limit_offset!(statement, limit, offset, bind_values) unless use_simple_rownum_limit
147
157
 
@@ -152,19 +162,20 @@ module DataMapper
152
162
  # Functionality is mimiced through the use of nested selects.
153
163
  # See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064
154
164
  def add_limit_offset!(statement, limit, offset, bind_values)
155
- if limit && offset > 0
165
+ positive_offset = offset > 0
166
+
167
+ if limit && positive_offset
156
168
  statement.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{statement}) raw_sql_ where rownum <= ?) where raw_rnum_ > ?"
157
169
  bind_values << offset + limit << offset
158
170
  elsif limit
159
171
  statement.replace "select raw_sql_.* from (#{statement}) raw_sql_ where rownum <= ?"
160
172
  bind_values << limit
161
- elsif offset > 0
173
+ elsif positive_offset
162
174
  statement.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{statement}) raw_sql_) where raw_rnum_ > ?"
163
175
  bind_values << offset
164
176
  end
165
177
  end
166
178
 
167
- # TODO: document
168
179
  # @api private
169
180
  # Oracle does not allow " in table or column names therefore substitute them with underscore
170
181
  def quote_name(name)
@@ -181,25 +192,15 @@ module DataMapper
181
192
  # NOTE: just first 32767 bytes will be compared!
182
193
  # @api private
183
194
  def equality_operator(property, operand)
184
- if property.type == Types::Text
185
- operand.nil? ? 'IS' : 'DBMS_LOB.SUBSTR(%s) = ?'
186
- else
187
- operand.nil? ? 'IS' : '='
188
- end
189
- end
190
-
191
- # CLOB value should be compared using DBMS_LOB.SUBSTR function
192
- # NOTE: just first 32767 bytes will be compared!
193
- # @api private
194
- def inequality_operator(property, operand)
195
- if property.type == Types::Text
196
- operand.nil? ? 'IS NOT' : 'DBMS_LOB.SUBSTR(%s) <> ?'
195
+ if operand.nil?
196
+ 'IS'
197
+ elsif property.type == Types::Text
198
+ 'DBMS_LOB.SUBSTR(%s) = ?'
197
199
  else
198
- operand.nil? ? 'IS NOT' : '<>'
200
+ '='
199
201
  end
200
202
  end
201
203
 
202
- # TODO: document
203
204
  # @api private
204
205
  def include_operator(property, operand)
205
206
  operator = case operand
@@ -213,32 +214,11 @@ module DataMapper
213
214
  end
214
215
  end
215
216
 
216
- # TODO: document
217
- # @api private
218
- def exclude_operator(property, operand)
219
- operator = case operand
220
- when Array then 'NOT IN'
221
- when Range then 'NOT BETWEEN'
222
- end
223
- if property.type == Types::Text
224
- "DBMS_LOB.SUBSTR(%s) #{operator} ?"
225
- else
226
- operator
227
- end
228
- end
229
-
230
- # TODO: document
231
217
  # @api private
232
218
  def regexp_operator(operand)
233
219
  'REGEXP_LIKE(%s, ?)'
234
220
  end
235
221
 
236
- # TODO: document
237
- # @api private
238
- def not_regexp_operator(operand)
239
- 'NOT REGEXP_LIKE(%s, ?)'
240
- end
241
-
242
222
  end #module SQL
243
223
 
244
224
  include SQL
@@ -8,7 +8,6 @@ module DataMapper
8
8
  module SQL #:nodoc:
9
9
  private
10
10
 
11
- # TODO: document
12
11
  # @api private
13
12
  def supports_returning?
14
13
  true
@@ -5,6 +5,11 @@ require 'do_sqlite3'
5
5
  module DataMapper
6
6
  module Adapters
7
7
  class Sqlite3Adapter < DataObjectsAdapter
8
+ # @api private
9
+ def supports_subquery?(query, source_key, target_key, qualify)
10
+ # SQLite3 cannot match a subquery against more than one column
11
+ source_key.size == 1 && target_key.size == 1
12
+ end
8
13
  end # class Sqlite3Adapter
9
14
 
10
15
  const_added(:Sqlite3Adapter)
@@ -0,0 +1,114 @@
1
+ require DataMapper.root / 'lib' / 'dm-core' / 'adapters' / 'data_objects_adapter'
2
+
3
+ require 'do_sqlserver'
4
+
5
+ DataObjects::Sqlserver = DataObjects::SqlServer
6
+
7
+ module DataMapper
8
+ module Adapters
9
+ class SqlserverAdapter < DataObjectsAdapter
10
+ module SQL #:nodoc:
11
+ private
12
+
13
+ # Constructs INSERT statement for given query,
14
+ #
15
+ # @return [String] INSERT statement as a string
16
+ #
17
+ # @api private
18
+ def insert_statement(model, properties, serial)
19
+ statement = ""
20
+ # Check if there is a serial property being set directly
21
+ require_identity_insert = !properties.empty? && properties.any? { |property| property.serial? }
22
+ set_identity_insert(model, statement, true) if require_identity_insert
23
+ statement << super
24
+ set_identity_insert(model, statement, false) if require_identity_insert
25
+ statement
26
+ end
27
+
28
+ def set_identity_insert(model, statement, enable = true)
29
+ statement << " SET IDENTITY_INSERT #{quote_name(model.storage_name(name))} #{enable ? 'ON' : 'OFF'} "
30
+ end
31
+
32
+ def select_statement(query)
33
+ name = self.name
34
+ qualify = query.links.any?
35
+ fields = query.fields
36
+ offset = query.offset
37
+ limit = query.limit
38
+ order_by = query.order
39
+ group_by = if qualify || query.unique?
40
+ fields.select { |property| property.kind_of?(Property) }
41
+ end
42
+
43
+ conditions_statement, bind_values = conditions_statement(query.conditions, qualify)
44
+
45
+ use_limit_offset_subquery = limit && offset > 0
46
+
47
+ columns_statement = columns_statement(fields, qualify)
48
+ from_statement = " FROM #{quote_name(query.model.storage_name(name))}"
49
+ where_statement = " WHERE #{conditions_statement}" unless conditions_statement.blank?
50
+ join_statement = join_statement(query, qualify)
51
+ order_statement = order_statement(order_by, qualify)
52
+ no_group_by = group_by ? group_by.empty? : true
53
+ no_order_by = order_by ? order_by.empty? : true
54
+
55
+ if use_limit_offset_subquery
56
+ # If using qualifiers, we must qualify elements outside the subquery
57
+ # with 'RowResults' -- this is a different scope to the subquery.
58
+ # Otherwise, we hit upon "multi-part identifier cannot be bound"
59
+ # error from SQL Server.
60
+ statement = "SELECT #{columns_statement(fields, qualify, 'RowResults')}"
61
+ statement << " FROM ( SELECT Row_Number() OVER (ORDER BY #{order_statement}) AS RowID,"
62
+ statement << " #{columns_statement}"
63
+ statement << from_statement
64
+ statement << join_statement if qualify
65
+ statement << where_statement if where_statement
66
+ statement << ") AS RowResults"
67
+ statement << " WHERE RowId > #{offset} AND RowId <= #{offset + limit}"
68
+ statement << " GROUP BY #{columns_statement(group_by, qualify, 'RowResults')}" unless no_group_by
69
+ statement << " ORDER BY #{order_statement(order_by, qualify, 'RowResults')}" unless no_order_by
70
+ else
71
+ statement = "SELECT #{columns_statement}"
72
+ statement << from_statement
73
+ statement << join_statement if qualify
74
+ statement << where_statement if where_statement
75
+ statement << " GROUP BY #{columns_statement(group_by, qualify)}" unless no_group_by
76
+ statement << " ORDER BY #{order_statement}" unless no_order_by
77
+ end
78
+
79
+ add_limit_offset!(statement, limit, offset, bind_values) unless use_limit_offset_subquery
80
+
81
+ return statement, bind_values
82
+ end
83
+
84
+ # SQL Server does not support LIMIT and OFFSET
85
+ # Functionality therefore must be mimicked through the use of nested selects.
86
+ # See also:
87
+ # - http://stackoverflow.com/questions/2840/paging-sql-server-2005-results
88
+ # - http://stackoverflow.com/questions/216673/emulate-mysql-limit-clause-in-microsoft-sql-server-2000
89
+ #
90
+ def add_limit_offset!(statement, limit, offset, bind_values)
91
+ # Limit and offset is handled by subqueries (see #select_statement).
92
+ if limit
93
+ # If there is just a limit on rows to return, but no offset, then we
94
+ # can use TOP clause.
95
+ statement.sub!(/^\s*SELECT(\s+DISTINCT)?/i) { "SELECT#{$1} TOP #{limit}" }
96
+ # bind_values << limit
97
+ end
98
+ end
99
+
100
+ # @api private
101
+ # TODO: Not actually supported out of the box. Is theoretically possible
102
+ # via CLR integration, custom functions.
103
+ def regexp_operator(operand)
104
+ 'REGEXP'
105
+ end
106
+
107
+ end #module SQL
108
+
109
+ include SQL
110
+ end # class SqlserverAdapter
111
+
112
+ const_added(:SqlserverAdapter)
113
+ end # module Adapters
114
+ end # module DataMapper
@@ -4,7 +4,6 @@ require 'yaml'
4
4
  module DataMapper
5
5
  module Adapters
6
6
  class YamlAdapter < AbstractAdapter
7
- # TODO: document
8
7
  # @api semipublic
9
8
  def create(resources)
10
9
  update_records(resources.first.model) do |records|
@@ -15,13 +14,11 @@ module DataMapper
15
14
  end
16
15
  end
17
16
 
18
- # TODO: document
19
17
  # @api semipublic
20
18
  def read(query)
21
19
  query.filter_records(records_for(query.model).dup)
22
20
  end
23
21
 
24
- # TODO: document
25
22
  # @api semipublic
26
23
  def update(attributes, collection)
27
24
  attributes = attributes_as_fields(attributes)
@@ -32,7 +29,6 @@ module DataMapper
32
29
  end
33
30
  end
34
31
 
35
- # TODO: document
36
32
  # @api semipublic
37
33
  def delete(collection)
38
34
  update_records(collection.model) do |records|
@@ -44,7 +40,6 @@ module DataMapper
44
40
 
45
41
  private
46
42
 
47
- # TODO: document
48
43
  # @api semipublic
49
44
  def initialize(name, options = {})
50
45
  super
@@ -26,7 +26,6 @@ module DataMapper
26
26
  end
27
27
  end
28
28
 
29
- # TODO: document
30
29
  # @api semipublic
31
30
  alias target_key child_key
32
31
 
@@ -52,17 +51,20 @@ module DataMapper
52
51
  def through
53
52
  return @through if defined?(@through)
54
53
 
55
- if options[:through].kind_of?(Associations::Relationship)
56
- return @through = options[:through]
54
+ @through = options[:through]
55
+
56
+ if @through.kind_of?(Associations::Relationship)
57
+ return @through
57
58
  end
58
59
 
60
+ model = source_model
59
61
  repository_name = source_repository_name
60
- relationships = source_model.relationships(repository_name)
62
+ relationships = model.relationships(repository_name)
61
63
  name = through_relationship_name
62
64
 
63
65
  @through = relationships[name] ||
64
66
  DataMapper.repository(repository_name) do
65
- source_model.has(min..max, name, through_model, one_to_many_options)
67
+ model.has(min..max, name, through_model, one_to_many_options)
66
68
  end
67
69
 
68
70
  @through.child_key
@@ -70,22 +72,25 @@ module DataMapper
70
72
  @through
71
73
  end
72
74
 
73
- # TODO: document
74
75
  # @api semipublic
75
76
  def via
76
77
  return @via if defined?(@via)
77
78
 
78
- if options[:via].kind_of?(Associations::Relationship)
79
- return @via = options[:via]
79
+ @via = options[:via]
80
+
81
+ if @via.kind_of?(Associations::Relationship)
82
+ return @via
80
83
  end
81
84
 
85
+ name = self.name
86
+ through = self.through
82
87
  repository_name = through.relative_target_repository_name
83
88
  through_model = through.target_model
84
89
  relationships = through_model.relationships(repository_name)
85
90
  singular_name = name.to_s.singularize.to_sym
86
91
 
87
- @via = relationships[options[:via]] ||
88
- relationships[name] ||
92
+ @via = relationships[@via] ||
93
+ relationships[name] ||
89
94
  relationships[singular_name]
90
95
 
91
96
  @via ||= if anonymous_through_model?
@@ -101,7 +106,6 @@ module DataMapper
101
106
  @via
102
107
  end
103
108
 
104
- # TODO: document
105
109
  # @api semipublic
106
110
  def links
107
111
  return @links if defined?(@links)
@@ -120,13 +124,11 @@ module DataMapper
120
124
  @links.freeze
121
125
  end
122
126
 
123
- # TODO: document
124
127
  # @api private
125
128
  def source_scope(source)
126
129
  { through.inverse => source }
127
130
  end
128
131
 
129
- # TODO: document
130
132
  # @api private
131
133
  def query
132
134
  # TODO: consider making this a query_for method, so that ManyToMany::Relationship#query only
@@ -152,7 +154,6 @@ module DataMapper
152
154
 
153
155
  private
154
156
 
155
- # TODO: document
156
157
  # @api private
157
158
  def through_model
158
159
  namespace, name = through_model_namespace_name
@@ -173,7 +174,6 @@ module DataMapper
173
174
  end
174
175
  end
175
176
 
176
- # TODO: document
177
177
  # @api private
178
178
  def through_model_namespace_name
179
179
  target_parts = target_model.base_model.name.split('::')
@@ -192,7 +192,6 @@ module DataMapper
192
192
  return namespace, name
193
193
  end
194
194
 
195
- # TODO: document
196
195
  # @api private
197
196
  def through_relationship_name
198
197
  if anonymous_through_model?
@@ -218,7 +217,39 @@ module DataMapper
218
217
  options[:through] == Resource
219
218
  end
220
219
 
221
- # TODO: document
220
+ # @api private
221
+ def nearest_relationship
222
+ return @nearest_relationship if defined?(@nearest_relationship)
223
+
224
+ nearest_relationship = self
225
+
226
+ while nearest_relationship.respond_to?(:through)
227
+ nearest_relationship = nearest_relationship.through
228
+ end
229
+
230
+ @nearest_relationship = nearest_relationship
231
+ end
232
+
233
+ # @api private
234
+ def valid_target?(target)
235
+ relationship = via
236
+ source_key = relationship.source_key
237
+ target_key = relationship.target_key
238
+
239
+ target.kind_of?(target_model) &&
240
+ source_key.valid?(target_key.get(target))
241
+ end
242
+
243
+ # @api private
244
+ def valid_source?(source)
245
+ relationship = nearest_relationship
246
+ source_key = relationship.source_key
247
+ target_key = relationship.target_key
248
+
249
+ source.kind_of?(source_model) &&
250
+ target_key.valid?(source_key.get(source))
251
+ end
252
+
222
253
  # @api semipublic
223
254
  chainable do
224
255
  def many_to_one_options
@@ -226,7 +257,6 @@ module DataMapper
226
257
  end
227
258
  end
228
259
 
229
- # TODO: document
230
260
  # @api semipublic
231
261
  chainable do
232
262
  def one_to_many_options
@@ -241,13 +271,11 @@ module DataMapper
241
271
  self.class
242
272
  end
243
273
 
244
- # TODO: document
245
274
  # @api private
246
275
  def invert
247
276
  inverse_class.new(inverse_name, parent_model, child_model, inverted_options)
248
277
  end
249
278
 
250
- # TODO: document
251
279
  # @api private
252
280
  def inverted_options
253
281
  links = self.links.dup
@@ -264,6 +292,8 @@ module DataMapper
264
292
  )
265
293
  end
266
294
 
295
+ options = self.options
296
+
267
297
  options.only(*OPTIONS - [ :min, :max ]).update(
268
298
  :through => through,
269
299
  :child_key => options[:parent_key],
@@ -312,7 +342,7 @@ module DataMapper
312
342
  # the intermediaries are removed
313
343
  lazy_load
314
344
 
315
- unless intermediaries.destroy
345
+ unless intermediaries.all(via => self).destroy
316
346
  return false
317
347
  end
318
348
 
@@ -332,45 +362,71 @@ module DataMapper
332
362
  def destroy!
333
363
  assert_source_saved 'The source must be saved before mass-deleting the collection'
334
364
 
335
- # make sure the records are loaded so they can be found when
336
- # the intermediaries are removed
337
- lazy_load
365
+ model = self.model
366
+ key = model.key(repository_name)
367
+ conditions = Query.target_conditions(self, key, key)
338
368
 
339
- unless intermediaries.destroy!
369
+ unless intermediaries.all(via => self).destroy!
340
370
  return false
341
371
  end
342
372
 
343
- super
373
+ unless model.all(:repository => repository, :conditions => conditions).destroy!
374
+ return false
375
+ end
376
+
377
+ each { |resource| resource.reset }
378
+ clear
379
+
380
+ true
344
381
  end
345
382
 
346
- # Return the intermediaries between the source and the targets
383
+ # Return the intermediaries linking the source to the targets
347
384
  #
348
385
  # @return [Collection]
349
386
  # the intermediary collection
350
387
  #
351
388
  # @api public
352
389
  def intermediaries
353
- return @intermediaries if @intermediaries
390
+ through = self.through
391
+ source = self.source
354
392
 
355
- intermediaries = if through.loaded?(source)
393
+ @intermediaries ||= if through.loaded?(source)
356
394
  through.get!(source)
357
395
  else
358
- through.set!(source, through.collection_for(source))
396
+ reset_intermediaries
359
397
  end
398
+ end
360
399
 
361
- scoped = intermediaries.all(via => self)
400
+ protected
362
401
 
363
- @intermediaries = scoped.query == intermediaries.query ? intermediaries : scoped
402
+ # Map the resources in the collection to the intermediaries
403
+ #
404
+ # @return [Hash]
405
+ # the map of resources to their intermediaries
406
+ #
407
+ # @api private
408
+ def intermediary_for
409
+ @intermediary_for ||= {}
410
+ end
411
+
412
+ # @api private
413
+ def through
414
+ relationship.through
415
+ end
416
+
417
+ # @api private
418
+ def via
419
+ relationship.via
364
420
  end
365
421
 
366
422
  private
367
423
 
368
- # TODO: document
369
424
  # @api private
370
425
  def _create(safe, attributes)
426
+ via = self.via
371
427
  if via.respond_to?(:resource_for)
372
428
  resource = super
373
- if create_intermediary(safe, via => resource)
429
+ if create_intermediary(safe, resource)
374
430
  resource
375
431
  end
376
432
  else
@@ -380,21 +436,23 @@ module DataMapper
380
436
  end
381
437
  end
382
438
 
383
- # TODO: document
384
439
  # @api private
385
440
  def _save(safe)
441
+ via = self.via
442
+
386
443
  if @removed.any?
387
444
  # delete only intermediaries linked to the removed targets
388
- removed_intermediaries = intermediaries.all(via => @removed).each do |resource|
389
- intermediaries.delete(resource)
390
- end
445
+ return false unless intermediaries.all(via => @removed).send(safe ? :destroy : :destroy!)
391
446
 
392
- return false unless removed_intermediaries.send(safe ? :destroy : :destroy!)
447
+ # reset the intermediaries so that it reflects the current state of the datastore
448
+ reset_intermediaries
393
449
  end
394
450
 
451
+ loaded_entries = self.loaded_entries
452
+
395
453
  if via.respond_to?(:resource_for)
396
454
  super
397
- loaded_entries.all? { |resource| create_intermediary(safe, via => resource) }
455
+ loaded_entries.all? { |resource| create_intermediary(safe, resource) }
398
456
  else
399
457
  if intermediary = create_intermediary(safe)
400
458
  inverse = via.inverse
@@ -405,32 +463,36 @@ module DataMapper
405
463
  end
406
464
  end
407
465
 
408
- # TODO: document
409
466
  # @api private
410
- def create_intermediary(safe, attributes = {})
411
- collection = intermediaries
467
+ def create_intermediary(safe, resource = nil)
468
+ intermediary_for = self.intermediary_for
412
469
 
413
- return unless collection.send(safe ? :save : :save!)
470
+ intermediary_resource = intermediary_for[resource]
471
+ return intermediary_resource if intermediary_resource
414
472
 
415
- intermediary = collection.first(attributes) ||
416
- collection.send(safe ? :create : :create!, attributes)
473
+ intermediaries = self.intermediaries
474
+ method = safe ? :save : :save!
417
475
 
418
- return intermediary if intermediary.saved?
419
- end
476
+ return unless intermediaries.send(method)
420
477
 
421
- # TODO: document
422
- # @api private
423
- def through
424
- relationship.through
478
+ attributes = {}
479
+ attributes[via] = resource if resource
480
+
481
+ intermediary = intermediaries.first_or_new(attributes)
482
+ return unless intermediary.__send__(method)
483
+
484
+ # map the resource, even if it is nil, to the intermediary
485
+ intermediary_for[resource] = intermediary
425
486
  end
426
487
 
427
- # TODO: document
428
488
  # @api private
429
- def via
430
- relationship.via
489
+ def reset_intermediaries
490
+ through = self.through
491
+ source = self.source
492
+
493
+ through.set!(source, through.collection_for(source))
431
494
  end
432
495
 
433
- # TODO: document
434
496
  # @api private
435
497
  def inverse_set(*)
436
498
  # do nothing