sam-dm-core 0.9.6

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