sam-dm-core 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
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