dm-core 0.10.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. data/.autotest +29 -0
  2. data/.document +5 -0
  3. data/.gitignore +27 -0
  4. data/LICENSE +20 -0
  5. data/{README.txt → README.rdoc} +14 -3
  6. data/Rakefile +23 -22
  7. data/VERSION +1 -0
  8. data/dm-core.gemspec +201 -10
  9. data/lib/dm-core.rb +32 -23
  10. data/lib/dm-core/adapters.rb +0 -1
  11. data/lib/dm-core/adapters/data_objects_adapter.rb +230 -151
  12. data/lib/dm-core/adapters/mysql_adapter.rb +7 -8
  13. data/lib/dm-core/adapters/oracle_adapter.rb +39 -59
  14. data/lib/dm-core/adapters/postgres_adapter.rb +0 -1
  15. data/lib/dm-core/adapters/sqlite3_adapter.rb +5 -0
  16. data/lib/dm-core/adapters/sqlserver_adapter.rb +114 -0
  17. data/lib/dm-core/adapters/yaml_adapter.rb +0 -5
  18. data/lib/dm-core/associations/many_to_many.rb +118 -56
  19. data/lib/dm-core/associations/many_to_one.rb +48 -21
  20. data/lib/dm-core/associations/one_to_many.rb +8 -30
  21. data/lib/dm-core/associations/one_to_one.rb +1 -5
  22. data/lib/dm-core/associations/relationship.rb +89 -97
  23. data/lib/dm-core/collection.rb +299 -184
  24. data/lib/dm-core/core_ext/enumerable.rb +28 -0
  25. data/lib/dm-core/core_ext/kernel.rb +0 -2
  26. data/lib/dm-core/migrations.rb +314 -170
  27. data/lib/dm-core/model.rb +97 -66
  28. data/lib/dm-core/model/descendant_set.rb +1 -1
  29. data/lib/dm-core/model/hook.rb +0 -3
  30. data/lib/dm-core/model/property.rb +7 -10
  31. data/lib/dm-core/model/relationship.rb +79 -26
  32. data/lib/dm-core/model/scope.rb +3 -4
  33. data/lib/dm-core/property.rb +152 -90
  34. data/lib/dm-core/property_set.rb +18 -37
  35. data/lib/dm-core/query.rb +452 -153
  36. data/lib/dm-core/query/conditions/comparison.rb +266 -173
  37. data/lib/dm-core/query/conditions/operation.rb +499 -57
  38. data/lib/dm-core/query/direction.rb +0 -3
  39. data/lib/dm-core/query/operator.rb +0 -4
  40. data/lib/dm-core/query/path.rb +10 -12
  41. data/lib/dm-core/query/sort.rb +4 -10
  42. data/lib/dm-core/repository.rb +10 -6
  43. data/lib/dm-core/resource.rb +343 -148
  44. data/lib/dm-core/spec/adapter_shared_spec.rb +17 -1
  45. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +277 -17
  46. data/lib/dm-core/support/chainable.rb +0 -2
  47. data/lib/dm-core/support/equalizer.rb +27 -3
  48. data/lib/dm-core/transaction.rb +75 -75
  49. data/lib/dm-core/type.rb +19 -5
  50. data/lib/dm-core/types/discriminator.rb +4 -4
  51. data/lib/dm-core/types/object.rb +2 -7
  52. data/lib/dm-core/types/paranoid_boolean.rb +8 -2
  53. data/lib/dm-core/types/paranoid_datetime.rb +8 -2
  54. data/lib/dm-core/version.rb +1 -1
  55. data/script/performance.rb +7 -7
  56. data/script/profile.rb +6 -6
  57. data/spec/lib/collection_helpers.rb +2 -2
  58. data/spec/lib/pending_helpers.rb +22 -3
  59. data/spec/lib/rspec_immediate_feedback_formatter.rb +1 -0
  60. data/spec/public/associations/many_to_many_spec.rb +6 -4
  61. data/spec/public/associations/many_to_one_spec.rb +10 -1
  62. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +39 -0
  63. data/spec/public/associations/one_to_many_spec.rb +4 -3
  64. data/spec/public/associations/one_to_one_spec.rb +19 -1
  65. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +45 -0
  66. data/spec/public/collection_spec.rb +4 -3
  67. data/spec/public/migrations_spec.rb +144 -0
  68. data/spec/public/model/relationship_spec.rb +115 -55
  69. data/spec/public/model_spec.rb +13 -13
  70. data/spec/public/property/object_spec.rb +106 -0
  71. data/spec/public/property_spec.rb +18 -14
  72. data/spec/public/resource_spec.rb +10 -1
  73. data/spec/public/sel_spec.rb +16 -49
  74. data/spec/public/setup_spec.rb +1 -1
  75. data/spec/public/shared/association_collection_shared_spec.rb +6 -14
  76. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  77. data/spec/public/shared/collection_shared_spec.rb +214 -217
  78. data/spec/public/shared/finder_shared_spec.rb +259 -365
  79. data/spec/public/shared/resource_shared_spec.rb +524 -248
  80. data/spec/public/transaction_spec.rb +27 -3
  81. data/spec/public/types/discriminator_spec.rb +1 -1
  82. data/spec/rcov.opts +6 -0
  83. data/spec/semipublic/adapters/sqlserver_adapter_spec.rb +17 -0
  84. data/spec/semipublic/associations/many_to_one_spec.rb +3 -20
  85. data/spec/semipublic/associations_spec.rb +2 -2
  86. data/spec/semipublic/collection_spec.rb +0 -32
  87. data/spec/semipublic/model_spec.rb +96 -0
  88. data/spec/semipublic/property_spec.rb +3 -3
  89. data/spec/semipublic/query/conditions/comparison_spec.rb +1719 -0
  90. data/spec/semipublic/query/conditions/operation_spec.rb +1292 -0
  91. data/spec/semipublic/query_spec.rb +1285 -144
  92. data/spec/semipublic/resource_spec.rb +0 -24
  93. data/spec/semipublic/shared/resource_shared_spec.rb +103 -38
  94. data/spec/spec.opts +1 -1
  95. data/spec/spec_helper.rb +15 -6
  96. data/tasks/ci.rake +1 -0
  97. data/tasks/metrics.rake +37 -0
  98. data/tasks/spec.rake +41 -0
  99. data/tasks/yard.rake +9 -0
  100. data/tasks/yardstick.rake +19 -0
  101. metadata +99 -29
  102. data/CONTRIBUTING +0 -51
  103. data/FAQ +0 -93
  104. data/History.txt +0 -27
  105. data/MIT-LICENSE +0 -22
  106. data/Manifest.txt +0 -121
  107. data/QUICKLINKS +0 -11
  108. data/SPECS +0 -35
  109. data/TODO +0 -1
  110. data/spec/semipublic/query/conditions_spec.rb +0 -528
  111. data/tasks/ci.rb +0 -24
  112. data/tasks/dm.rb +0 -58
  113. data/tasks/doc.rb +0 -17
  114. data/tasks/gemspec.rb +0 -23
  115. data/tasks/hoe.rb +0 -45
  116. data/tasks/install.rb +0 -18
@@ -65,8 +65,9 @@ module DataMapper
65
65
  # @return [self]
66
66
  #
67
67
  # @api public
68
- def reload(query = nil)
69
- query = query.nil? ? self.query.dup : self.query.merge(query)
68
+ def reload(other_query = nil)
69
+ query = self.query
70
+ query = other_query.nil? ? query.dup : query.merge(other_query)
70
71
 
71
72
  # make sure the Identity Map contains all the existing resources
72
73
  identity_map = repository.identity_map(model)
@@ -75,20 +76,60 @@ module DataMapper
75
76
  identity_map[resource.key] = resource
76
77
  end
77
78
 
78
- properties = model.properties(repository.name)
79
- fields = properties.key | query.fields
80
-
81
- if discriminator = properties.discriminator
82
- fields |= [ discriminator ]
83
- end
84
-
85
79
  # sort fields based on declared order, for more consistent reload queries
86
- fields = properties & fields
80
+ properties = self.properties
81
+ fields = properties & (query.fields | model_key | [ properties.discriminator ].compact)
87
82
 
88
83
  # replace the list of resources
89
84
  replace(all(query.update(:fields => fields, :reload => true)))
90
85
  end
91
86
 
87
+ # Return the union with another collection
88
+ #
89
+ # @param [Collection] other
90
+ # the other collection
91
+ #
92
+ # @return [Collection]
93
+ # the union of the collection and other
94
+ #
95
+ # @api public
96
+ def union(other)
97
+ set_operation(:|, other)
98
+ end
99
+
100
+ alias | union
101
+ alias + union
102
+
103
+ # Return the intersection with another collection
104
+ #
105
+ # @param [Collection] other
106
+ # the other collection
107
+ #
108
+ # @return [Collection]
109
+ # the intersection of the collection and other
110
+ #
111
+ # @api public
112
+ def intersection(other)
113
+ set_operation(:&, other)
114
+ end
115
+
116
+ alias & intersection
117
+
118
+ # Return the difference with another collection
119
+ #
120
+ # @param [Collection] other
121
+ # the other collection
122
+ #
123
+ # @return [Collection]
124
+ # the difference of the collection and other
125
+ #
126
+ # @api public
127
+ def difference(other)
128
+ set_operation(:-, other)
129
+ end
130
+
131
+ alias - difference
132
+
92
133
  # Lookup a Resource in the Collection by key
93
134
  #
94
135
  # This looksup a Resource by key, typecasting the key to the
@@ -108,9 +149,12 @@ module DataMapper
108
149
  #
109
150
  # @api public
110
151
  def get(*key)
111
- key = model.key(repository.name).typecast(key)
152
+ assert_valid_key_size(key)
153
+
154
+ key = model_key.typecast(key)
155
+ query = self.query
112
156
 
113
- resource = @identity_map[key] || if !loaded? && (query.limit || query.offset > 0)
157
+ @identity_map[key] || if !loaded? && (query.limit || query.offset > 0)
114
158
  # current query is exclusive, find resource within the set
115
159
 
116
160
  # TODO: use a subquery to retrieve the Collection and then match
@@ -128,10 +172,6 @@ module DataMapper
128
172
  # current query is all inclusive, lookup using normal approach
129
173
  first(model.key_conditions(repository, key))
130
174
  end
131
-
132
- return if resource.nil?
133
-
134
- orphan_resource(resource)
135
175
  end
136
176
 
137
177
  # Lookup a Resource in the Collection by key, raising an exception if not found
@@ -175,12 +215,13 @@ module DataMapper
175
215
  #
176
216
  # @api public
177
217
  def all(query = nil)
218
+ # TODO: update this not to accept a nil value, and instead either
219
+ # accept a Hash/Query and nothing else
178
220
  if query.nil? || (query.kind_of?(Hash) && query.empty?)
179
221
  dup
180
222
  else
181
223
  # TODO: if there is no order parameter, and the Collection is not loaded
182
224
  # check to see if the query can be satisfied by the head/tail
183
-
184
225
  new_collection(scoped_query(query))
185
226
  end
186
227
  end
@@ -203,13 +244,16 @@ module DataMapper
203
244
  #
204
245
  # @api public
205
246
  def first(*args)
206
- last_arg = args.last
247
+ first_arg = args.first
248
+ last_arg = args.last
207
249
 
208
- limit = args.first if args.first.kind_of?(Integer)
209
- with_query = last_arg.respond_to?(:merge) && !last_arg.blank?
250
+ limit_specified = first_arg.kind_of?(Integer)
251
+ with_query = (last_arg.kind_of?(Hash) && !last_arg.empty?) || last_arg.kind_of?(Query)
210
252
 
211
- query = with_query ? last_arg : {}
212
- query = self.query.slice(0, limit || 1).update(query)
253
+ limit = limit_specified ? first_arg : 1
254
+ query = with_query ? last_arg : {}
255
+
256
+ query = self.query.slice(0, limit).update(query)
213
257
 
214
258
  # TODO: when a query provided, and there are enough elements in head to
215
259
  # satisfy the query.limit, filter the head with the query, and make
@@ -217,16 +261,23 @@ module DataMapper
217
261
  # of calling all()
218
262
  # - this can probably only be done if there is no :order parameter
219
263
 
220
- collection = if !with_query && (loaded? || lazy_possible?(head, query.limit))
221
- new_collection(query, super(query.limit))
264
+ loaded = loaded?
265
+ head = self.head
266
+
267
+ collection = if !with_query && (loaded || lazy_possible?(head, limit))
268
+ new_collection(query, super(limit))
222
269
  else
223
270
  all(query)
224
271
  end
225
272
 
226
- if limit
227
- collection
228
- else
229
- collection.to_a.first
273
+ return collection if limit_specified
274
+
275
+ resource = collection.to_a.first
276
+
277
+ if with_query || loaded
278
+ resource
279
+ elsif resource
280
+ head[0] = resource
230
281
  end
231
282
  end
232
283
 
@@ -248,13 +299,16 @@ module DataMapper
248
299
  #
249
300
  # @api public
250
301
  def last(*args)
251
- last_arg = args.last
302
+ first_arg = args.first
303
+ last_arg = args.last
304
+
305
+ limit_specified = first_arg.kind_of?(Integer)
306
+ with_query = (last_arg.kind_of?(Hash) && !last_arg.empty?) || last_arg.kind_of?(Query)
252
307
 
253
- limit = args.first if args.first.kind_of?(Integer)
254
- with_query = last_arg.respond_to?(:merge) && !last_arg.blank?
308
+ limit = limit_specified ? first_arg : 1
309
+ query = with_query ? last_arg : {}
255
310
 
256
- query = with_query ? last_arg : {}
257
- query = self.query.slice(0, limit || 1).update(query).reverse!
311
+ query = self.query.slice(0, limit).update(query).reverse!
258
312
 
259
313
  # tell the Query to prepend each result from the adapter
260
314
  query.update(:add_reversed => !query.add_reversed?)
@@ -264,16 +318,23 @@ module DataMapper
264
318
  # sure it matches the limit exactly. if so, use that result instead
265
319
  # of calling all()
266
320
 
267
- collection = if !with_query && (loaded? || lazy_possible?(tail, query.limit))
268
- new_collection(query, super(query.limit))
321
+ loaded = loaded?
322
+ tail = self.tail
323
+
324
+ collection = if !with_query && (loaded || lazy_possible?(tail, limit))
325
+ new_collection(query, super(limit))
269
326
  else
270
327
  all(query)
271
328
  end
272
329
 
273
- if limit
274
- collection
275
- else
276
- collection.to_a.last
330
+ return collection if limit_specified
331
+
332
+ resource = collection.to_a.last
333
+
334
+ if with_query || loaded
335
+ resource
336
+ elsif resource
337
+ tail[tail.empty? ? 0 : -1] = resource
277
338
  end
278
339
  end
279
340
 
@@ -290,8 +351,7 @@ module DataMapper
290
351
  # @api public
291
352
  def at(offset)
292
353
  if loaded? || partially_loaded?(offset)
293
- return unless resource = super
294
- orphan_resource(resource)
354
+ super
295
355
  elsif offset >= 0
296
356
  first(:offset => offset)
297
357
  else
@@ -401,9 +461,6 @@ module DataMapper
401
461
  # mark resources as removed
402
462
  resources_removed(orphans - loaded_entries)
403
463
 
404
- # ensure remaining orphans are still related
405
- (orphans & loaded_entries).each { |resource| relate_resource(resource) }
406
-
407
464
  resources
408
465
  end
409
466
 
@@ -438,6 +495,24 @@ module DataMapper
438
495
  self
439
496
  end
440
497
 
498
+ # Iterate over each Resource
499
+ #
500
+ # @yield [Resource] Each resource in the collection
501
+ #
502
+ # @return [self]
503
+ #
504
+ # @api public
505
+ def each
506
+ super do |resource|
507
+ begin
508
+ original, resource.collection = resource.collection, self
509
+ yield resource
510
+ ensure
511
+ resource.collection = original
512
+ end
513
+ end
514
+ end
515
+
441
516
  # Invoke the block for each resource and replace it the return value
442
517
  #
443
518
  # @yield [Resource] Each resource in the collection
@@ -460,12 +535,7 @@ module DataMapper
460
535
  #
461
536
  # @api public
462
537
  def <<(resource)
463
- if resource.kind_of?(Hash)
464
- resource = new(resource)
465
- end
466
-
467
- resource_added(resource)
468
- super
538
+ super(resource_added(resource))
469
539
  end
470
540
 
471
541
  # Appends the resources to self
@@ -477,8 +547,7 @@ module DataMapper
477
547
  #
478
548
  # @api public
479
549
  def concat(resources)
480
- resources_added(resources)
481
- super
550
+ super(resources_added(resources))
482
551
  end
483
552
 
484
553
  # Append one or more Resources to the Collection
@@ -493,8 +562,7 @@ module DataMapper
493
562
  #
494
563
  # @api public
495
564
  def push(*resources)
496
- resources_added(resources)
497
- super
565
+ super(*resources_added(resources))
498
566
  end
499
567
 
500
568
  # Prepend one or more Resources to the Collection
@@ -509,8 +577,7 @@ module DataMapper
509
577
  #
510
578
  # @api public
511
579
  def unshift(*resources)
512
- resources_added(resources)
513
- super
580
+ super(*resources_added(resources))
514
581
  end
515
582
 
516
583
  # Inserts the Resources before the Resource at the offset (which may be negative).
@@ -524,8 +591,7 @@ module DataMapper
524
591
  #
525
592
  # @api public
526
593
  def insert(offset, *resources)
527
- resources_added(resources)
528
- super
594
+ super(offset, *resources_added(resources))
529
595
  end
530
596
 
531
597
  # Removes and returns the last Resource in the Collection
@@ -619,6 +685,12 @@ module DataMapper
619
685
  super { |resource| yield(resource) && resource_removed(resource) }
620
686
  end
621
687
 
688
+ # Access LazyArray#replace directly
689
+ #
690
+ # @api private
691
+ alias superclass_replace replace
692
+ private :superclass_replace
693
+
622
694
  # Replace the Resources within the Collection
623
695
  #
624
696
  # @param [Enumerable] other
@@ -628,26 +700,23 @@ module DataMapper
628
700
  #
629
701
  # @api public
630
702
  def replace(other)
631
- other = other.map do |resource|
632
- if resource.kind_of?(Hash)
633
- new(resource)
634
- else
635
- resource
636
- end
637
- end
638
-
639
- if loaded?
640
- resources_removed(self - other)
641
- end
642
-
643
- super(resources_added(other))
703
+ other = resources_added(other)
704
+ resources_removed(entries - other)
705
+ super(other)
644
706
  end
645
707
 
646
- # Access Collection#replace directly
708
+ # (Private) Set the Collection
709
+ #
710
+ # @param [Array] resources
711
+ # resources to add to the collection
712
+ #
713
+ # @return [self]
647
714
  #
648
715
  # @api private
649
- alias collection_replace replace
650
- private :collection_replace
716
+ def set(resources)
717
+ superclass_replace(resources_added(resources))
718
+ self
719
+ end
651
720
 
652
721
  # Removes all Resources from the Collection
653
722
  #
@@ -766,11 +835,13 @@ module DataMapper
766
835
  def update!(attributes = {})
767
836
  assert_update_clean_only(:update!)
768
837
 
838
+ model = self.model
839
+
769
840
  dirty_attributes = model.new(attributes).dirty_attributes
770
841
 
771
842
  if dirty_attributes.empty?
772
843
  true
773
- elsif dirty_attributes.any? { |property, value| !property.nullable? && value.nil? }
844
+ elsif dirty_attributes.any? { |property, value| !property.valid?(value) }
774
845
  false
775
846
  else
776
847
  unless _update(dirty_attributes)
@@ -836,21 +907,18 @@ module DataMapper
836
907
  #
837
908
  # @api public
838
909
  def destroy!
839
- if query.limit || query.offset > 0 || query.links.any?
840
- key = model.key(repository.name)
841
- conditions = Query.target_conditions(self, key, key)
910
+ repository = self.repository
911
+ deleted = repository.delete(self)
842
912
 
843
- unless model.all(:repository => repository, :conditions => conditions).destroy!
913
+ if loaded?
914
+ unless deleted == size
844
915
  return false
845
916
  end
846
- else
847
- repository.delete(self)
848
- mark_loaded
849
- end
850
917
 
851
- if loaded?
852
918
  each { |resource| resource.reset }
853
919
  clear
920
+ else
921
+ mark_loaded
854
922
  end
855
923
 
856
924
  true
@@ -884,11 +952,11 @@ module DataMapper
884
952
  # Checks if any resources have unsaved changes
885
953
  #
886
954
  # @return [Boolean]
887
- # true if a resource may be persisted
955
+ # true if the resources have unsaved changed
888
956
  #
889
957
  # @api public
890
958
  def dirty?
891
- loaded_entries.any? { |resource| resource.dirty? }
959
+ loaded_entries.any? { |resource| resource.dirty? } || @removed.any?
892
960
  end
893
961
 
894
962
  # Gets a Human-readable representation of this collection,
@@ -902,14 +970,41 @@ module DataMapper
902
970
  "[#{map { |resource| resource.inspect }.join(', ')}]"
903
971
  end
904
972
 
973
+ # @api semipublic
974
+ def hash
975
+ query.hash
976
+ end
977
+
978
+ protected
979
+
980
+ # Returns the model key
981
+ #
982
+ # @return [PropertySet]
983
+ # the model key
984
+ #
985
+ # @api private
986
+ def model_key
987
+ model.key(repository_name)
988
+ end
989
+
990
+ # Loaded Resources in the collection
991
+ #
992
+ # @return [Array<Resource>]
993
+ # Resources in the collection
994
+ #
995
+ # @api private
996
+ def loaded_entries
997
+ (loaded? ? self : head + tail).reject { |resource| resource.destroyed? }
998
+ end
999
+
905
1000
  # Returns the PropertySet representing the fields in the Collection scope
906
1001
  #
907
1002
  # @return [PropertySet]
908
1003
  # The set of properties this Collection's query will retrieve
909
1004
  #
910
- # @api semipublic
1005
+ # @api private
911
1006
  def properties
912
- PropertySet.new(query.fields)
1007
+ model.properties(repository_name)
913
1008
  end
914
1009
 
915
1010
  # Returns the Relationships for the Collection's Model
@@ -918,9 +1013,9 @@ module DataMapper
918
1013
  # The model's relationships, mapping the name to the
919
1014
  # Associations::Relationship object
920
1015
  #
921
- # @api semipublic
1016
+ # @api private
922
1017
  def relationships
923
- model.relationships(repository.name)
1018
+ model.relationships(repository_name)
924
1019
  end
925
1020
 
926
1021
  private
@@ -947,9 +1042,7 @@ module DataMapper
947
1042
  # TODO: change LazyArray to not use a load proc at all
948
1043
  remove_instance_variable(:@load_with_proc)
949
1044
 
950
- if resources
951
- replace(resources)
952
- end
1045
+ set(resources) if resources
953
1046
  end
954
1047
 
955
1048
  # Copies the original Collection state
@@ -967,6 +1060,19 @@ module DataMapper
967
1060
  @removed = @removed.dup
968
1061
  end
969
1062
 
1063
+ # Initialize a resource from a Hash
1064
+ #
1065
+ # @param [Resource, Hash] resource
1066
+ # resource to process
1067
+ #
1068
+ # @return [Resource]
1069
+ # an initialized resource
1070
+ #
1071
+ # @api private
1072
+ def initialize_resource(resource)
1073
+ resource.kind_of?(Hash) ? new(resource) : resource
1074
+ end
1075
+
970
1076
  # Test if the collection is loaded between the offset and limit
971
1077
  #
972
1078
  # @param [Integer] offset
@@ -998,6 +1104,10 @@ module DataMapper
998
1104
 
999
1105
  mark_loaded
1000
1106
 
1107
+ head = self.head
1108
+ tail = self.tail
1109
+ query = self.query
1110
+
1001
1111
  resources = repository.read(query)
1002
1112
 
1003
1113
  # remove already known results
@@ -1018,14 +1128,14 @@ module DataMapper
1018
1128
  self
1019
1129
  end
1020
1130
 
1021
- # Loaded Resources in the collection
1131
+ # Returns the Query Repository name
1022
1132
  #
1023
- # @return [Array<Resource>]
1024
- # Resources in the collection
1133
+ # @return [Symbol]
1134
+ # the repository name
1025
1135
  #
1026
1136
  # @api private
1027
- def loaded_entries
1028
- loaded? ? self : head + tail
1137
+ def repository_name
1138
+ repository.name
1029
1139
  end
1030
1140
 
1031
1141
  # Initializes a new Collection
@@ -1046,6 +1156,40 @@ module DataMapper
1046
1156
  self.class.new(query, resources, &block)
1047
1157
  end
1048
1158
 
1159
+ # Apply a set operation on self and another collection
1160
+ #
1161
+ # @param [Symbol] operation
1162
+ # the set operation to apply
1163
+ # @param [Collection] other
1164
+ # the other collection to apply the set operation on
1165
+ #
1166
+ # @return [Collection]
1167
+ # the collection that was created for the set operation
1168
+ #
1169
+ # @api private
1170
+ def set_operation(operation, other)
1171
+ resources = set_operation_resources(operation, other)
1172
+ other_query = Query.target_query(repository, model, other)
1173
+ new_collection(query.send(operation, other_query), resources)
1174
+ end
1175
+
1176
+ # Prepopulate the set operation if the collection is loaded
1177
+ #
1178
+ # @param [Symbol] operation
1179
+ # the set operation to apply
1180
+ # @param [Collection] other
1181
+ # the other collection to apply the set operation on
1182
+ #
1183
+ # @return [nil]
1184
+ # nil if the Collection is not loaded
1185
+ # @return [Array]
1186
+ # the resources to prepopulate the set operation results with
1187
+ #
1188
+ # @api private
1189
+ def set_operation_resources(operation, other)
1190
+ entries.send(operation, other.entries) if loaded?
1191
+ end
1192
+
1049
1193
  # Creates a resource in the collection
1050
1194
  #
1051
1195
  # @param [Boolean] safe
@@ -1070,19 +1214,7 @@ module DataMapper
1070
1214
  #
1071
1215
  # @api private
1072
1216
  def _update(dirty_attributes)
1073
- if query.limit || query.offset > 0 || query.links.any?
1074
- attributes = dirty_attributes.map { |property, value| [ property.name, value ] }.to_hash
1075
-
1076
- key = model.key(repository.name)
1077
- conditions = Query.target_conditions(self, key, key)
1078
-
1079
- unless model.all(:repository => repository, :conditions => conditions).update!(attributes)
1080
- return false
1081
- end
1082
- else
1083
- repository.update(dirty_attributes, self)
1084
- end
1085
-
1217
+ repository.update(dirty_attributes, self)
1086
1218
  true
1087
1219
  end
1088
1220
 
@@ -1096,9 +1228,10 @@ module DataMapper
1096
1228
  #
1097
1229
  # @api private
1098
1230
  def _save(safe)
1231
+ loaded_entries = self.loaded_entries
1099
1232
  loaded_entries.each { |resource| set_default_attributes(resource) }
1100
1233
  @removed.clear
1101
- loaded_entries.all? { |resource| resource.send(safe ? :save : :save!) }
1234
+ loaded_entries.all? { |resource| resource.__send__(safe ? :save : :save!) }
1102
1235
  end
1103
1236
 
1104
1237
  # Returns default values to initialize new Resources in the Collection
@@ -1113,23 +1246,21 @@ module DataMapper
1113
1246
 
1114
1247
  conditions = query.conditions
1115
1248
 
1116
- if conditions.kind_of?(Query::Conditions::AndOperation)
1117
- repository_name = repository.name
1118
- relationships = self.relationships.values
1119
- properties = model.properties(repository_name)
1120
- key = model.key(repository_name)
1249
+ if conditions.slug == :and
1250
+ model_properties = properties.dup
1251
+ model_key = self.model_key
1121
1252
 
1122
- # if all the key properties are included in the conditions,
1123
- # then do not allow them to be default attributes
1124
- if query.condition_properties.to_set.superset?(key.to_set)
1125
- properties -= key
1253
+ if model_properties.to_set.superset?(model_key.to_set)
1254
+ model_properties -= model_key
1126
1255
  end
1127
1256
 
1128
1257
  conditions.each do |condition|
1129
- if condition.kind_of?(Query::Conditions::EqualToComparison) &&
1130
- (properties.include?(condition.subject) || (condition.relationship? && condition.subject.source_model == model))
1131
- default_attributes[condition.subject] = condition.value
1132
- end
1258
+ next unless condition.slug == :eql
1259
+
1260
+ subject = condition.subject
1261
+ next unless model_properties.include?(subject) || (condition.relationship? && subject.source_model == model)
1262
+
1263
+ default_attributes[subject] = condition.value
1133
1264
  end
1134
1265
  end
1135
1266
 
@@ -1145,55 +1276,11 @@ module DataMapper
1145
1276
  #
1146
1277
  # @api private
1147
1278
  def set_default_attributes(resource)
1148
- unless resource.frozen?
1279
+ unless resource.readonly?
1149
1280
  resource.attributes = default_attributes
1150
1281
  end
1151
1282
  end
1152
1283
 
1153
- # Relates a Resource to the Collection
1154
- #
1155
- # This is used by SEL related code to reload a Resource and the
1156
- # Collection it belongs to.
1157
- #
1158
- # @param [Resource] resource
1159
- # The Resource to relate
1160
- #
1161
- # @return [Resource]
1162
- # If Resource was successfully related
1163
- # @return [nil]
1164
- # If a nil resource was provided
1165
- #
1166
- # @api private
1167
- def relate_resource(resource)
1168
- unless resource.frozen?
1169
- resource.collection = self
1170
- end
1171
-
1172
- resource
1173
- end
1174
-
1175
- # Orphans a Resource from the Collection
1176
- #
1177
- # Removes the association between the Resource and Collection so that
1178
- # SEL related code will not load the Collection.
1179
- #
1180
- # @param [Resource] resource
1181
- # The Resource to orphan
1182
- #
1183
- # @return [Resource]
1184
- # The Resource that was orphaned
1185
- # @return [nil]
1186
- # If a nil resource was provided
1187
- #
1188
- # @api private
1189
- def orphan_resource(resource)
1190
- if resource.collection.equal?(self) && !resource.frozen?
1191
- resource.collection = nil
1192
- end
1193
-
1194
- resource
1195
- end
1196
-
1197
1284
  # Track the added resource
1198
1285
  #
1199
1286
  # @param [Resource] resource
@@ -1204,6 +1291,8 @@ module DataMapper
1204
1291
  #
1205
1292
  # @api private
1206
1293
  def resource_added(resource)
1294
+ resource = initialize_resource(resource)
1295
+
1207
1296
  if resource.saved?
1208
1297
  @identity_map[resource.key] = resource
1209
1298
  @removed.delete(resource)
@@ -1211,7 +1300,7 @@ module DataMapper
1211
1300
  set_default_attributes(resource)
1212
1301
  end
1213
1302
 
1214
- relate_resource(resource)
1303
+ resource
1215
1304
  end
1216
1305
 
1217
1306
  # Track the added resources
@@ -1225,7 +1314,7 @@ module DataMapper
1225
1314
  # @api private
1226
1315
  def resources_added(resources)
1227
1316
  if resources.kind_of?(Enumerable)
1228
- resources.each { |resource| resource_added(resource) }
1317
+ resources.map { |resource| resource_added(resource) }
1229
1318
  else
1230
1319
  resource_added(resources)
1231
1320
  end
@@ -1246,7 +1335,7 @@ module DataMapper
1246
1335
  @removed << resource
1247
1336
  end
1248
1337
 
1249
- orphan_resource(resource)
1338
+ resource
1250
1339
  end
1251
1340
 
1252
1341
  # Track the removed resources
@@ -1277,17 +1366,20 @@ module DataMapper
1277
1366
  # nil if no resources match the Query
1278
1367
  #
1279
1368
  # @api private
1280
- def filter(query)
1281
- fields = self.query.fields.to_set
1282
-
1283
- if query.links.empty? &&
1284
- (query.unique? || (!query.unique? && !self.query.unique?)) &&
1285
- !query.reload? &&
1286
- !query.raw? &&
1287
- query.fields.to_set.subset?(fields) &&
1288
- query.condition_properties.subset?(fields)
1369
+ def filter(other_query)
1370
+ query = self.query
1371
+ fields = query.fields.to_set
1372
+ unique = other_query.unique?
1373
+
1374
+ # TODO: push this into a Query#subset? method
1375
+ if other_query.links.empty? &&
1376
+ (unique || (!unique && !query.unique?)) &&
1377
+ !other_query.reload? &&
1378
+ !other_query.raw? &&
1379
+ other_query.fields.to_set.subset?(fields) &&
1380
+ other_query.condition_properties.subset?(fields)
1289
1381
  then
1290
- query.filter_records(to_a.dup)
1382
+ other_query.filter_records(to_a.dup)
1291
1383
  end
1292
1384
  end
1293
1385
 
@@ -1340,6 +1432,8 @@ module DataMapper
1340
1432
  #
1341
1433
  # @api public
1342
1434
  def method_missing(method, *args, &block)
1435
+ relationships = self.relationships
1436
+
1343
1437
  if model.model_method_defined?(method)
1344
1438
  delegate_to_model(method, *args, &block)
1345
1439
  elsif relationship = relationships[method] || relationships[method.to_s.singular.to_sym]
@@ -1361,6 +1455,7 @@ module DataMapper
1361
1455
  #
1362
1456
  # @api private
1363
1457
  def delegate_to_model(method, *args, &block)
1458
+ model = self.model
1364
1459
  model.__send__(:with_scope, query) do
1365
1460
  model.send(method, *args, &block)
1366
1461
  end
@@ -1386,7 +1481,27 @@ module DataMapper
1386
1481
  # @api private
1387
1482
  def assert_update_clean_only(method)
1388
1483
  if dirty?
1389
- raise UpdateConflictError, "##{method} cannot be called on a dirty collection"
1484
+ raise UpdateConflictError, "#{self.class}##{method} cannot be called on a dirty collection"
1485
+ end
1486
+ end
1487
+
1488
+ # Raises an exception if #get receives the wrong number of arguments
1489
+ #
1490
+ # @param [Array] key
1491
+ # the key value
1492
+ #
1493
+ # @return [undefined]
1494
+ #
1495
+ # @raise [UpdateConflictError]
1496
+ # raise if the resource is dirty
1497
+ #
1498
+ # @api private
1499
+ def assert_valid_key_size(key)
1500
+ expected_key_size = model_key.size
1501
+ actual_key_size = key.size
1502
+
1503
+ if actual_key_size != expected_key_size
1504
+ raise ArgumentError, "The number of arguments for the key is invalid, expected #{expected_key_size} but was #{actual_key_size}"
1390
1505
  end
1391
1506
  end
1392
1507
  end # class Collection