rpbertp13-dm-core 0.9.11.1

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.
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 +52 -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 +32 -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 +138 -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 +221 -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 +252 -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 +60 -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 +469 -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