datamapper-dm-core 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. data/.autotest +26 -0
  2. data/.gitignore +18 -0
  3. data/CONTRIBUTING +51 -0
  4. data/FAQ +92 -0
  5. data/History.txt +41 -0
  6. data/MIT-LICENSE +22 -0
  7. data/Manifest.txt +130 -0
  8. data/QUICKLINKS +11 -0
  9. data/README.txt +143 -0
  10. data/Rakefile +30 -0
  11. data/SPECS +62 -0
  12. data/TODO +1 -0
  13. data/dm-core.gemspec +40 -0
  14. data/lib/dm-core.rb +217 -0
  15. data/lib/dm-core/adapters.rb +16 -0
  16. data/lib/dm-core/adapters/abstract_adapter.rb +209 -0
  17. data/lib/dm-core/adapters/data_objects_adapter.rb +716 -0
  18. data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
  19. data/lib/dm-core/adapters/mysql_adapter.rb +136 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +189 -0
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  22. data/lib/dm-core/associations.rb +207 -0
  23. data/lib/dm-core/associations/many_to_many.rb +147 -0
  24. data/lib/dm-core/associations/many_to_one.rb +107 -0
  25. data/lib/dm-core/associations/one_to_many.rb +315 -0
  26. data/lib/dm-core/associations/one_to_one.rb +61 -0
  27. data/lib/dm-core/associations/relationship.rb +229 -0
  28. data/lib/dm-core/associations/relationship_chain.rb +81 -0
  29. data/lib/dm-core/auto_migrations.rb +105 -0
  30. data/lib/dm-core/collection.rb +670 -0
  31. data/lib/dm-core/dependency_queue.rb +32 -0
  32. data/lib/dm-core/hook.rb +11 -0
  33. data/lib/dm-core/identity_map.rb +42 -0
  34. data/lib/dm-core/is.rb +16 -0
  35. data/lib/dm-core/logger.rb +232 -0
  36. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  37. data/lib/dm-core/migrator.rb +29 -0
  38. data/lib/dm-core/model.rb +526 -0
  39. data/lib/dm-core/naming_conventions.rb +84 -0
  40. data/lib/dm-core/property.rb +676 -0
  41. data/lib/dm-core/property_set.rb +169 -0
  42. data/lib/dm-core/query.rb +676 -0
  43. data/lib/dm-core/repository.rb +167 -0
  44. data/lib/dm-core/resource.rb +671 -0
  45. data/lib/dm-core/scope.rb +58 -0
  46. data/lib/dm-core/support.rb +7 -0
  47. data/lib/dm-core/support/array.rb +13 -0
  48. data/lib/dm-core/support/assertions.rb +8 -0
  49. data/lib/dm-core/support/errors.rb +23 -0
  50. data/lib/dm-core/support/kernel.rb +11 -0
  51. data/lib/dm-core/support/symbol.rb +41 -0
  52. data/lib/dm-core/transaction.rb +267 -0
  53. data/lib/dm-core/type.rb +160 -0
  54. data/lib/dm-core/type_map.rb +80 -0
  55. data/lib/dm-core/types.rb +19 -0
  56. data/lib/dm-core/types/boolean.rb +7 -0
  57. data/lib/dm-core/types/discriminator.rb +34 -0
  58. data/lib/dm-core/types/object.rb +24 -0
  59. data/lib/dm-core/types/paranoid_boolean.rb +34 -0
  60. data/lib/dm-core/types/paranoid_datetime.rb +33 -0
  61. data/lib/dm-core/types/serial.rb +9 -0
  62. data/lib/dm-core/types/text.rb +10 -0
  63. data/lib/dm-core/version.rb +3 -0
  64. data/script/all +4 -0
  65. data/script/performance.rb +282 -0
  66. data/script/profile.rb +87 -0
  67. data/spec/integration/association_spec.rb +1382 -0
  68. data/spec/integration/association_through_spec.rb +203 -0
  69. data/spec/integration/associations/many_to_many_spec.rb +449 -0
  70. data/spec/integration/associations/many_to_one_spec.rb +163 -0
  71. data/spec/integration/associations/one_to_many_spec.rb +188 -0
  72. data/spec/integration/auto_migrations_spec.rb +413 -0
  73. data/spec/integration/collection_spec.rb +1073 -0
  74. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  75. data/spec/integration/dependency_queue_spec.rb +46 -0
  76. data/spec/integration/model_spec.rb +197 -0
  77. data/spec/integration/mysql_adapter_spec.rb +85 -0
  78. data/spec/integration/postgres_adapter_spec.rb +731 -0
  79. data/spec/integration/property_spec.rb +253 -0
  80. data/spec/integration/query_spec.rb +514 -0
  81. data/spec/integration/repository_spec.rb +61 -0
  82. data/spec/integration/resource_spec.rb +513 -0
  83. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  84. data/spec/integration/sti_spec.rb +273 -0
  85. data/spec/integration/strategic_eager_loading_spec.rb +156 -0
  86. data/spec/integration/transaction_spec.rb +75 -0
  87. data/spec/integration/type_spec.rb +275 -0
  88. data/spec/lib/logging_helper.rb +18 -0
  89. data/spec/lib/mock_adapter.rb +27 -0
  90. data/spec/lib/model_loader.rb +100 -0
  91. data/spec/lib/publicize_methods.rb +28 -0
  92. data/spec/models/content.rb +16 -0
  93. data/spec/models/vehicles.rb +34 -0
  94. data/spec/models/zoo.rb +48 -0
  95. data/spec/spec.opts +3 -0
  96. data/spec/spec_helper.rb +91 -0
  97. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  98. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  99. data/spec/unit/adapters/data_objects_adapter_spec.rb +632 -0
  100. data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
  101. data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
  102. data/spec/unit/associations/many_to_many_spec.rb +32 -0
  103. data/spec/unit/associations/many_to_one_spec.rb +159 -0
  104. data/spec/unit/associations/one_to_many_spec.rb +393 -0
  105. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  106. data/spec/unit/associations/relationship_spec.rb +71 -0
  107. data/spec/unit/associations_spec.rb +242 -0
  108. data/spec/unit/auto_migrations_spec.rb +111 -0
  109. data/spec/unit/collection_spec.rb +182 -0
  110. data/spec/unit/data_mapper_spec.rb +35 -0
  111. data/spec/unit/identity_map_spec.rb +126 -0
  112. data/spec/unit/is_spec.rb +80 -0
  113. data/spec/unit/migrator_spec.rb +33 -0
  114. data/spec/unit/model_spec.rb +321 -0
  115. data/spec/unit/naming_conventions_spec.rb +36 -0
  116. data/spec/unit/property_set_spec.rb +90 -0
  117. data/spec/unit/property_spec.rb +753 -0
  118. data/spec/unit/query_spec.rb +571 -0
  119. data/spec/unit/repository_spec.rb +93 -0
  120. data/spec/unit/resource_spec.rb +649 -0
  121. data/spec/unit/scope_spec.rb +142 -0
  122. data/spec/unit/transaction_spec.rb +493 -0
  123. data/spec/unit/type_map_spec.rb +114 -0
  124. data/spec/unit/type_spec.rb +119 -0
  125. data/tasks/ci.rb +36 -0
  126. data/tasks/dm.rb +63 -0
  127. data/tasks/doc.rb +20 -0
  128. data/tasks/gemspec.rb +23 -0
  129. data/tasks/hoe.rb +46 -0
  130. data/tasks/install.rb +20 -0
  131. metadata +215 -0
@@ -0,0 +1,81 @@
1
+ module DataMapper
2
+ module Associations
3
+ class RelationshipChain < Relationship
4
+ OPTIONS = [
5
+ :repository_name, :near_relationship_name, :remote_relationship_name,
6
+ :child_model, :parent_model, :parent_key, :child_key,
7
+ :min, :max
8
+ ]
9
+
10
+ undef_method :get_parent
11
+ undef_method :attach_parent
12
+
13
+ # @api private
14
+ def child_model
15
+ near_relationship.child_model
16
+ end
17
+
18
+ # @api private
19
+ def get_children(parent, options = {}, finder = :all, *args)
20
+ query = @query.merge(options).merge(child_key.to_query(parent_key.get(parent)))
21
+
22
+ query[:links] = links
23
+ query[:unique] = true
24
+
25
+ with_repository(parent) do
26
+ results = grandchild_model.send(finder, *(args << query))
27
+ # FIXME: remove the need for the uniq.freeze
28
+ finder == :all ? (@mutable ? results : results.freeze) : results
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # @api private
35
+ def initialize(options)
36
+ if (missing_options = OPTIONS - [ :min, :max ] - options.keys ).any?
37
+ raise ArgumentError, "The options #{missing_options * ', '} are required", caller
38
+ end
39
+
40
+ @repository_name = options.fetch(:repository_name)
41
+ @near_relationship_name = options.fetch(:near_relationship_name)
42
+ @remote_relationship_name = options.fetch(:remote_relationship_name)
43
+ @child_model = options.fetch(:child_model)
44
+ @parent_model = options.fetch(:parent_model)
45
+ @parent_properties = options.fetch(:parent_key)
46
+ @child_properties = options.fetch(:child_key)
47
+ @mutable = options.delete(:mutable) || false
48
+
49
+ @name = near_relationship.name
50
+ @query = options.reject{ |key,val| OPTIONS.include?(key) }
51
+ @extra_links = []
52
+ @options = options
53
+ end
54
+
55
+ # @api private
56
+ def near_relationship
57
+ parent_model.relationships[@near_relationship_name]
58
+ end
59
+
60
+ # @api private
61
+ def links
62
+ if remote_relationship.kind_of?(RelationshipChain)
63
+ remote_relationship.send(:links) + [ remote_relationship.send(:near_relationship) ]
64
+ else
65
+ [ remote_relationship ]
66
+ end
67
+ end
68
+
69
+ # @api private
70
+ def remote_relationship
71
+ near_relationship.child_model.relationships[@remote_relationship_name] ||
72
+ near_relationship.child_model.relationships[@remote_relationship_name.to_s.singularize.to_sym]
73
+ end
74
+
75
+ # @api private
76
+ def grandchild_model
77
+ Class === @child_model ? @child_model : (Class === @parent_model ? @parent_model.find_const(@child_model) : Object.find_const(@child_model))
78
+ end
79
+ end # class Relationship
80
+ end # module Associations
81
+ end # module DataMapper
@@ -0,0 +1,105 @@
1
+ # TODO: move to dm-more/dm-migrations
2
+
3
+ module DataMapper
4
+ class AutoMigrator
5
+ ##
6
+ # Destructively automigrates the data-store to match the model.
7
+ # First migrates all models down and then up.
8
+ # REPEAT: THIS IS DESTRUCTIVE
9
+ #
10
+ # @param Symbol repository_name the repository to be migrated
11
+ def self.auto_migrate(repository_name = nil, *descendants)
12
+ auto_migrate_down(repository_name, *descendants)
13
+ auto_migrate_up(repository_name, *descendants)
14
+ end
15
+
16
+ ##
17
+ # Destructively automigrates the data-store down
18
+ # REPEAT: THIS IS DESTRUCTIVE
19
+ #
20
+ # @param Symbol repository_name the repository to be migrated
21
+ # @calls DataMapper::Resource#auto_migrate_down!
22
+ # @api private
23
+ def self.auto_migrate_down(repository_name = nil, *descendants)
24
+ descendants = DataMapper::Resource.descendants.to_a if descendants.empty?
25
+ descendants.reverse.each do |model|
26
+ model.auto_migrate_down!(repository_name)
27
+ end
28
+ end
29
+
30
+ ##
31
+ # Automigrates the data-store up
32
+ #
33
+ # @param Symbol repository_name the repository to be migrated
34
+ # @calls DataMapper::Resource#auto_migrate_up!
35
+ # @api private
36
+ def self.auto_migrate_up(repository_name = nil, *descendants)
37
+ descendants = DataMapper::Resource.descendants.to_a if descendants.empty?
38
+ descendants.each do |model|
39
+ model.auto_migrate_up!(repository_name)
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Safely migrates the data-store to match the model
45
+ # preserving data already in the data-store
46
+ #
47
+ # @param Symbol repository_name the repository to be migrated
48
+ # @calls DataMapper::Resource#auto_upgrade!
49
+ def self.auto_upgrade(repository_name = nil)
50
+ DataMapper::Resource.descendants.each do |model|
51
+ model.auto_upgrade!(repository_name)
52
+ end
53
+ end
54
+ end # class AutoMigrator
55
+
56
+ module AutoMigrations
57
+ ##
58
+ # Destructively automigrates the data-store to match the model
59
+ # REPEAT: THIS IS DESTRUCTIVE
60
+ #
61
+ # @param Symbol repository_name the repository to be migrated
62
+ def auto_migrate!(repository_name = self.repository_name)
63
+ auto_migrate_down!(repository_name)
64
+ auto_migrate_up!(repository_name)
65
+ end
66
+
67
+ ##
68
+ # Destructively migrates the data-store down, which basically
69
+ # deletes all the models.
70
+ # REPEAT: THIS IS DESTRUCTIVE
71
+ #
72
+ # @param Symbol repository_name the repository to be migrated
73
+ # @api private
74
+ def auto_migrate_down!(repository_name = self.repository_name)
75
+ # repository_name ||= default_repository_name
76
+ repository(repository_name) do |r|
77
+ r.adapter.destroy_model_storage(r, self.base_model)
78
+ end
79
+ end
80
+
81
+ ##
82
+ # Auto migrates the data-store to match the model
83
+ #
84
+ # @param Symbol repository_name the repository to be migrated
85
+ # @api private
86
+ def auto_migrate_up!(repository_name = self.repository_name)
87
+ repository(repository_name) do |r|
88
+ r.adapter.create_model_storage(r, self.base_model)
89
+ end
90
+ end
91
+
92
+ ##
93
+ # Safely migrates the data-store to match the model
94
+ # preserving data already in the data-store
95
+ #
96
+ # @param Symbol repository_name the repository to be migrated
97
+ def auto_upgrade!(repository_name = self.repository_name)
98
+ repository(repository_name) do |r|
99
+ r.adapter.upgrade_model_storage(r, self)
100
+ end
101
+ end
102
+
103
+ Model.send(:include, self)
104
+ end # module AutoMigrations
105
+ end # module DataMapper
@@ -0,0 +1,670 @@
1
+ module DataMapper
2
+ class Collection < LazyArray
3
+ include Assertions
4
+
5
+ attr_reader :query
6
+
7
+ ##
8
+ # @return [Repository] the repository the collection is
9
+ # associated with
10
+ #
11
+ # @api public
12
+ def repository
13
+ query.repository
14
+ end
15
+
16
+ ##
17
+ # loads the entries for the collection. Used by the
18
+ # adapters to load the instances of the declared
19
+ # model for this collection's query.
20
+ #
21
+ # @api private
22
+ def load(values)
23
+ add(model.load(values, query))
24
+ end
25
+
26
+ ##
27
+ # reloads the entries associated with this collection
28
+ #
29
+ # @param [DataMapper::Query] query (optional) additional query
30
+ # to scope by. Use this if you want to query a collections result
31
+ # set
32
+ #
33
+ # @see DataMapper::Collection#all
34
+ #
35
+ # @api public
36
+ def reload(query = {})
37
+ @query = scoped_query(query)
38
+ inheritance_property = model.base_model.inheritance_property
39
+ fields = (@key_properties | [inheritance_property]).compact
40
+ @query.update(:fields => @query.fields | fields)
41
+ replace(all(:reload => true))
42
+ end
43
+
44
+ ##
45
+ # retrieves an entry out of the collection's entry by key
46
+ #
47
+ # @param [DataMapper::Types::*, ...] key keys which uniquely
48
+ # identify a resource in the collection
49
+ #
50
+ # @return [DataMapper::Resource, NilClass] the resource which
51
+ # has the supplied keys
52
+ #
53
+ # @api public
54
+ def get(*key)
55
+ key = model.typecast_key(key)
56
+ if loaded?
57
+ # find indexed resource (create index first if it does not exist)
58
+ each {|r| @cache[r.key] = r } if @cache.empty?
59
+ @cache[key]
60
+ elsif query.limit || query.offset > 0
61
+ # current query is exclusive, find resource within the set
62
+
63
+ # TODO: use a subquery to retrieve the collection and then match
64
+ # it up against the key. This will require some changes to
65
+ # how subqueries are generated, since the key may be a
66
+ # composite key. In the case of DO adapters, it means subselects
67
+ # like the form "(a, b) IN(SELECT a,b FROM ...)", which will
68
+ # require making it so the Query condition key can be a
69
+ # Property or an Array of Property objects
70
+
71
+ # use the brute force approach until subquery lookups work
72
+ lazy_load
73
+ get(*key)
74
+ else
75
+ # current query is all inclusive, lookup using normal approach
76
+ first(model.to_query(repository, key))
77
+ end
78
+ end
79
+
80
+ ##
81
+ # retrieves an entry out of the collection's entry by key,
82
+ # raising an exception if the object cannot be found
83
+ #
84
+ # @param [DataMapper::Types::*, ...] key keys which uniquely
85
+ # identify a resource in the collection
86
+ #
87
+ # @calls DataMapper::Collection#get
88
+ #
89
+ # @raise [ObjectNotFoundError] "Could not find #{model.name} with key #{key.inspect} in collection"
90
+ #
91
+ # @api public
92
+ def get!(*key)
93
+ get(*key) || raise(ObjectNotFoundError, "Could not find #{model.name} with key #{key.inspect} in collection")
94
+ end
95
+
96
+ ##
97
+ # Further refines a collection's conditions. #all provides an
98
+ # interface which simulates a database view.
99
+ #
100
+ # @param [Hash[Symbol, Object], DataMapper::Query] query parameters for
101
+ # an query within the results of the original query.
102
+ #
103
+ # @return [DataMapper::Collection] a collection whose query is the result
104
+ # of a merge
105
+ #
106
+ # @api public
107
+ def all(query = {})
108
+ # TODO: this shouldn't be a kicker if scoped_query() is called
109
+ return self if query.kind_of?(Hash) ? query.empty? : query == self.query
110
+ query = scoped_query(query)
111
+ query.repository.read_many(query)
112
+ end
113
+
114
+ ##
115
+ # Simulates Array#first by returning the first entry (when
116
+ # there are no arguments), or transforms the collection's query
117
+ # by applying :limit => n when you supply an Integer. If you
118
+ # provide a conditions hash, or a Query object, the internal
119
+ # query is scoped and a new collection is returned
120
+ #
121
+ # @param [Integer, Hash[Symbol, Object], Query] args
122
+ #
123
+ # @return [DataMapper::Resource, DataMapper::Collection] The
124
+ # first resource in the entries of this collection, or
125
+ # a new collection whose query has been merged
126
+ #
127
+ # @api public
128
+ def first(*args)
129
+ # TODO: this shouldn't be a kicker if scoped_query() is called
130
+ if loaded?
131
+ if args.empty?
132
+ return super
133
+ elsif args.size == 1 && args.first.kind_of?(Integer)
134
+ limit = args.shift
135
+ return self.class.new(scoped_query(:limit => limit)) { |c| c.replace(super(limit)) }
136
+ end
137
+ end
138
+
139
+ query = args.last.respond_to?(:merge) ? args.pop : {}
140
+ query = scoped_query(query.merge(:limit => args.first || 1))
141
+
142
+ if args.any?
143
+ query.repository.read_many(query)
144
+ else
145
+ query.repository.read_one(query)
146
+ end
147
+ end
148
+
149
+ ##
150
+ # Simulates Array#last by returning the last entry (when
151
+ # there are no arguments), or transforming the collection's
152
+ # query by reversing the declared order, and applying
153
+ # :limit => n when you supply an Integer. If you
154
+ # supply a conditions hash, or a Query object, the
155
+ # internal query is scoped and a new collection is returned
156
+ #
157
+ # @calls Collection#first
158
+ #
159
+ # @api public
160
+ def last(*args)
161
+ return super if loaded? && args.empty?
162
+
163
+ reversed = reverse
164
+
165
+ # tell the collection to reverse the order of the
166
+ # results coming out of the adapter
167
+ reversed.query.add_reversed = !query.add_reversed?
168
+
169
+ reversed.first(*args)
170
+ end
171
+
172
+ ##
173
+ # Simulates Array#at and returns the entry at that index.
174
+ # Also accepts negative indexes and appropriate reverses
175
+ # the order of the query
176
+ #
177
+ # @calls Collection#first
178
+ # @calls Collection#last
179
+ #
180
+ # @api public
181
+ def at(offset)
182
+ return super if loaded?
183
+ offset >= 0 ? first(:offset => offset) : last(:offset => offset.abs - 1)
184
+ end
185
+
186
+ ##
187
+ # Simulates Array#slice and returns a new Collection
188
+ # whose query has a new offset or limit according to the
189
+ # arguments provided.
190
+ #
191
+ # If you provide a range, the min is used as the offset
192
+ # and the max minues the offset is used as the limit.
193
+ #
194
+ # @param [Integer, Array(Integer), Range] args the offset,
195
+ # offset and limit, or range indicating offsets and limits
196
+ #
197
+ # @return [DataMapper::Resource, DataMapper::Collection]
198
+ # The entry which resides at that offset and limit,
199
+ # or a new Collection object with the set limits and offset
200
+ #
201
+ # @raise [ArgumentError] "arguments may be 1 or 2 Integers,
202
+ # or 1 Range object, was: #{args.inspect}"
203
+ #
204
+ # @alias []
205
+ #
206
+ # @api public
207
+ def slice(*args)
208
+ return at(args.first) if args.size == 1 && args.first.kind_of?(Integer)
209
+
210
+ if args.size == 2 && args.first.kind_of?(Integer) && args.last.kind_of?(Integer)
211
+ offset, limit = args
212
+ elsif args.size == 1 && args.first.kind_of?(Range)
213
+ range = args.first
214
+ offset = range.first
215
+ limit = range.last - offset
216
+ limit += 1 unless range.exclude_end?
217
+ else
218
+ raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}", caller
219
+ end
220
+
221
+ all(:offset => offset, :limit => limit)
222
+ end
223
+
224
+ alias [] slice
225
+
226
+ ##
227
+ #
228
+ # @return [DataMapper::Collection] a new collection whose
229
+ # query is sorted in the reverse
230
+ #
231
+ # @see Array#reverse, DataMapper#all, DataMapper::Query#reverse
232
+ #
233
+ # @api public
234
+ def reverse
235
+ all(self.query.reverse)
236
+ end
237
+
238
+ ##
239
+ # @see Array#<<
240
+ #
241
+ # @api public
242
+ def <<(resource)
243
+ super
244
+ relate_resource(resource)
245
+ self
246
+ end
247
+
248
+ ##
249
+ # @see Array#push
250
+ #
251
+ # @api public
252
+ def push(*resources)
253
+ super
254
+ resources.each { |resource| relate_resource(resource) }
255
+ self
256
+ end
257
+
258
+ ##
259
+ # @see Array#unshift
260
+ #
261
+ # @api public
262
+ def unshift(*resources)
263
+ super
264
+ resources.each { |resource| relate_resource(resource) }
265
+ self
266
+ end
267
+
268
+ ##
269
+ # @see Array#replace
270
+ #
271
+ # @api public
272
+ def replace(other)
273
+ if loaded?
274
+ each { |resource| orphan_resource(resource) }
275
+ end
276
+ super
277
+ other.each { |resource| relate_resource(resource) }
278
+ self
279
+ end
280
+
281
+ ##
282
+ # @see Array#pop
283
+ #
284
+ # @api public
285
+ def pop
286
+ orphan_resource(super)
287
+ end
288
+
289
+ ##
290
+ # @see Array#shift
291
+ #
292
+ # @api public
293
+ def shift
294
+ orphan_resource(super)
295
+ end
296
+
297
+ ##
298
+ # @see Array#delete
299
+ #
300
+ # @api public
301
+ def delete(resource)
302
+ orphan_resource(super)
303
+ end
304
+
305
+ ##
306
+ # @see Array#delete_at
307
+ #
308
+ # @api public
309
+ def delete_at(index)
310
+ orphan_resource(super)
311
+ end
312
+
313
+ ##
314
+ # @see Array#clear
315
+ #
316
+ # @api public
317
+ def clear
318
+ if loaded?
319
+ each { |resource| orphan_resource(resource) }
320
+ end
321
+ super
322
+ self
323
+ end
324
+
325
+ # builds a new resource and appends it to the collection
326
+ #
327
+ # @param Hash[Symbol => Object] attributes attributes which
328
+ # the new resource should have.
329
+ #
330
+ # @api public
331
+ def build(attributes = {})
332
+ repository.scope do
333
+ resource = model.new(default_attributes.merge(attributes))
334
+ self << resource
335
+ resource
336
+ end
337
+ end
338
+
339
+ ##
340
+ # creates a new resource, saves it, and appends it to the collection
341
+ #
342
+ # @param Hash[Symbol => Object] attributes attributes which
343
+ # the new resource should have.
344
+ #
345
+ # @api public
346
+ def create(attributes = {})
347
+ repository.scope do
348
+ resource = model.create(default_attributes.merge(attributes))
349
+ self << resource unless resource.new_record?
350
+ resource
351
+ end
352
+ end
353
+
354
+ def update(attributes = {}, preload = false)
355
+ raise NotImplementedError, 'update *with* validations has not be written yet, try update!'
356
+ end
357
+
358
+ ##
359
+ # batch updates the entries belongs to this collection, and skip
360
+ # validations for all resources.
361
+ #
362
+ # @example Reached the Age of Alchohol Consumption
363
+ # Person.all(:age.gte => 21).update!(:allow_beer => true)
364
+ #
365
+ # @param attributes Hash[Symbol => Object] attributes to update
366
+ # @param reload [FalseClass, TrueClass] if set to true, collection
367
+ # will have loaded resources reflect updates.
368
+ #
369
+ # @return [TrueClass, FalseClass]
370
+ # TrueClass indicates that all entries were affected
371
+ # FalseClass indicates that some entries were affected
372
+ #
373
+ # @api public
374
+ def update!(attributes = {}, reload = false)
375
+ # TODO: delegate to Model.update
376
+ return true if attributes.empty?
377
+
378
+ dirty_attributes = {}
379
+
380
+ model.properties(repository.name).slice(*attributes.keys).each do |property|
381
+ dirty_attributes[property] = attributes[property.name] if property
382
+ end
383
+
384
+ # this should never be done on update! even if collection is loaded. or?
385
+ # each { |resource| resource.attributes = attributes } if loaded?
386
+
387
+ changes = repository.update(dirty_attributes, scoped_query)
388
+
389
+ # need to decide if this should be done in update!
390
+ query.update(attributes)
391
+
392
+ if identity_map.any? && reload
393
+ reload_query = @key_properties.zip(identity_map.keys.transpose).to_hash
394
+ model.all(reload_query.merge(attributes)).reload(:fields => attributes.keys)
395
+ end
396
+
397
+ # this should return true if there are any changes at all. as it skips validations
398
+ # the only way it could be fewer changes is if some resources already was updated.
399
+ # that should not return false? true = 'now all objects have these new values'
400
+ return loaded? ? changes == size : changes > 0
401
+ end
402
+
403
+ def destroy
404
+ raise NotImplementedError, 'destroy *with* validations has not be written yet, try destroy!'
405
+ end
406
+
407
+ ##
408
+ # batch destroy the entries belongs to this collection, and skip
409
+ # validations for all resources.
410
+ #
411
+ # @example The War On Terror (if only it were this easy)
412
+ # Person.all(:terrorist => true).destroy() #
413
+ #
414
+ # @return [TrueClass, FalseClass]
415
+ # TrueClass indicates that all entries were affected
416
+ # FalseClass indicates that some entries were affected
417
+ #
418
+ # @api public
419
+ def destroy!
420
+ # TODO: delegate to Model.destroy
421
+ if loaded?
422
+ return false unless repository.delete(scoped_query) == size
423
+
424
+ each do |resource|
425
+ resource.instance_variable_set(:@new_record, true)
426
+ identity_map.delete(resource.key)
427
+ resource.dirty_attributes.clear
428
+
429
+ model.properties(repository.name).each do |property|
430
+ next unless resource.attribute_loaded?(property.name)
431
+ resource.dirty_attributes[property] = property.get(resource)
432
+ end
433
+ end
434
+ else
435
+ return false unless repository.delete(scoped_query) > 0
436
+ end
437
+
438
+ clear
439
+
440
+ true
441
+ end
442
+
443
+ ##
444
+ # @return [DataMapper::PropertySet] The set of properties this
445
+ # query will be retrieving
446
+ #
447
+ # @api public
448
+ def properties
449
+ PropertySet.new(query.fields)
450
+ end
451
+
452
+ ##
453
+ # @return [DataMapper::Relationship] The model's relationships
454
+ #
455
+ # @api public
456
+ def relationships
457
+ model.relationships(repository.name)
458
+ end
459
+
460
+ ##
461
+ # default values to use when creating a Resource within the Collection
462
+ #
463
+ # @return [Hash] The default attributes for DataMapper::Collection#create
464
+ #
465
+ # @see DataMapper::Collection#create
466
+ #
467
+ # @api public
468
+ def default_attributes
469
+ default_attributes = {}
470
+ query.conditions.each do |tuple|
471
+ operator, property, bind_value = *tuple
472
+
473
+ next unless operator == :eql &&
474
+ property.kind_of?(DataMapper::Property) &&
475
+ ![ Array, Range ].any? { |k| bind_value.kind_of?(k) }
476
+ !@key_properties.include?(property)
477
+
478
+ default_attributes[property.name] = bind_value
479
+ end
480
+ default_attributes
481
+ end
482
+
483
+ ##
484
+ # check to see if collection can respond to the method
485
+ #
486
+ # @param method [Symbol] method to check in the object
487
+ # @param include_private [FalseClass, TrueClass] if set to true,
488
+ # collection will check private methods
489
+ #
490
+ # @return [TrueClass, FalseClass]
491
+ # TrueClass indicates the method can be responded to by the collection
492
+ # FalseClass indicates the method can not be responded to by the collection
493
+ #
494
+ # @api public
495
+ def respond_to?(method, include_private = false)
496
+ super || model.public_methods(false).map { |m| m.to_s }.include?(method.to_s) || relationships.has_key?(method)
497
+ end
498
+
499
+ # TODO: add docs
500
+ # @api private
501
+ def _dump(*)
502
+ Marshal.dump([ query, to_a ])
503
+ end
504
+
505
+ # TODO: add docs
506
+ # @api private
507
+ def self._load(marshalled)
508
+ query, array = Marshal.load(marshalled)
509
+
510
+ # XXX: IMHO it is a code smell to be forced to use allocate
511
+ # and instance_variable_set to load an object. You should
512
+ # be able to use a constructor to provide all the info needed
513
+ # to initialize an object. This should be fixed in the edge
514
+ # branch dkubb/dm-core
515
+
516
+ collection = allocate
517
+ collection.instance_variable_set(:@query, query)
518
+ collection.instance_variable_set(:@array, array)
519
+ collection.instance_variable_set(:@loaded, true)
520
+ collection.instance_variable_set(:@key_properties, collection.send(:model).key(collection.repository.name))
521
+ collection.instance_variable_set(:@cache, {})
522
+ collection
523
+ end
524
+
525
+ protected
526
+
527
+ ##
528
+ # @api private
529
+ def model
530
+ query.model
531
+ end
532
+
533
+ private
534
+
535
+ ##
536
+ # @api public
537
+ def initialize(query, &block)
538
+ assert_kind_of 'query', query, Query
539
+
540
+ unless block_given?
541
+ # It can be helpful (relationship.rb: 112-13, used for SEL) to have a non-lazy Collection.
542
+ block = lambda { |c| }
543
+ end
544
+
545
+ @query = query
546
+ @key_properties = model.key(repository.name)
547
+ @cache = {}
548
+
549
+ super()
550
+
551
+ load_with(&block)
552
+ end
553
+
554
+ ##
555
+ # @api private
556
+ def add(resource)
557
+ query.add_reversed? ? unshift(resource) : push(resource)
558
+ resource
559
+ end
560
+
561
+ ##
562
+ # @api private
563
+ def relate_resource(resource)
564
+ return unless resource
565
+ resource.collection = self
566
+ @cache[resource.key] = resource
567
+ resource
568
+ end
569
+
570
+ ##
571
+ # @api private
572
+ def orphan_resource(resource)
573
+ return unless resource
574
+ resource.collection = nil if resource.collection.object_id == self.object_id
575
+ @cache.delete(resource.key)
576
+ resource
577
+ end
578
+
579
+ ##
580
+ # @api private
581
+ def scoped_query(query = self.query)
582
+ assert_kind_of 'query', query, Query, Hash
583
+
584
+ query.update(keys) if loaded?
585
+
586
+ return self.query if query == self.query
587
+
588
+ query = if query.kind_of?(Hash)
589
+ Query.new(query.has_key?(:repository) ? query.delete(:repository) : self.repository, model, query)
590
+ else
591
+ query
592
+ end
593
+
594
+ if query.limit || query.offset > 0
595
+ set_relative_position(query)
596
+ end
597
+
598
+ self.query.merge(query)
599
+ end
600
+
601
+ ##
602
+ # @api private
603
+ def keys
604
+ keys = map {|r| r.key }
605
+ keys.any? ? @key_properties.zip(keys.transpose).to_hash : {}
606
+ end
607
+
608
+ ##
609
+ # @api private
610
+ def identity_map
611
+ repository.identity_map(model)
612
+ end
613
+
614
+ ##
615
+ # @api private
616
+ def set_relative_position(query)
617
+ return if query == self.query
618
+
619
+ if query.offset == 0
620
+ return if !query.limit.nil? && !self.query.limit.nil? && query.limit <= self.query.limit
621
+ return if query.limit.nil? && self.query.limit.nil?
622
+ end
623
+
624
+ first_pos = self.query.offset + query.offset
625
+ last_pos = self.query.offset + self.query.limit if self.query.limit
626
+
627
+ if limit = query.limit
628
+ if last_pos.nil? || first_pos + limit < last_pos
629
+ last_pos = first_pos + limit
630
+ end
631
+ end
632
+
633
+ if last_pos && first_pos >= last_pos
634
+ raise 'outside range' # TODO: raise a proper exception object
635
+ end
636
+
637
+ query.update(:offset => first_pos)
638
+ query.update(:limit => last_pos - first_pos) if last_pos
639
+ end
640
+
641
+ ##
642
+ # @api private
643
+ def method_missing(method, *args, &block)
644
+ if model.public_methods(false).map { |m| m.to_s }.include?(method.to_s)
645
+ model.send(:with_scope, query) do
646
+ model.send(method, *args, &block)
647
+ end
648
+ elsif relationship = relationships[method]
649
+ klass = model == relationship.child_model ? relationship.parent_model : relationship.child_model
650
+
651
+ # TODO: when self.query includes an offset/limit use it as a
652
+ # subquery to scope the results rather than a join
653
+
654
+ query = Query.new(repository, klass)
655
+ query.conditions.push(*self.query.conditions)
656
+ query.update(relationship.query)
657
+ query.update(args.pop) if args.last.kind_of?(Hash)
658
+
659
+ query.update(
660
+ :fields => klass.properties(repository.name).defaults,
661
+ :links => [ relationship ] + self.query.links
662
+ )
663
+
664
+ klass.all(query, &block)
665
+ else
666
+ super
667
+ end
668
+ end
669
+ end # class Collection
670
+ end # module DataMapper