dm-core 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/CHANGELOG +144 -0
  2. data/FAQ +74 -0
  3. data/MIT-LICENSE +22 -0
  4. data/QUICKLINKS +12 -0
  5. data/README +143 -0
  6. data/lib/dm-core.rb +213 -0
  7. data/lib/dm-core/adapters.rb +4 -0
  8. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +701 -0
  10. data/lib/dm-core/adapters/mysql_adapter.rb +132 -0
  11. data/lib/dm-core/adapters/postgres_adapter.rb +179 -0
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  13. data/lib/dm-core/associations.rb +172 -0
  14. data/lib/dm-core/associations/many_to_many.rb +138 -0
  15. data/lib/dm-core/associations/many_to_one.rb +101 -0
  16. data/lib/dm-core/associations/one_to_many.rb +275 -0
  17. data/lib/dm-core/associations/one_to_one.rb +61 -0
  18. data/lib/dm-core/associations/relationship.rb +116 -0
  19. data/lib/dm-core/associations/relationship_chain.rb +74 -0
  20. data/lib/dm-core/auto_migrations.rb +64 -0
  21. data/lib/dm-core/collection.rb +604 -0
  22. data/lib/dm-core/hook.rb +11 -0
  23. data/lib/dm-core/identity_map.rb +45 -0
  24. data/lib/dm-core/is.rb +16 -0
  25. data/lib/dm-core/logger.rb +233 -0
  26. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  27. data/lib/dm-core/migrator.rb +29 -0
  28. data/lib/dm-core/model.rb +399 -0
  29. data/lib/dm-core/naming_conventions.rb +52 -0
  30. data/lib/dm-core/property.rb +611 -0
  31. data/lib/dm-core/property_set.rb +158 -0
  32. data/lib/dm-core/query.rb +590 -0
  33. data/lib/dm-core/repository.rb +159 -0
  34. data/lib/dm-core/resource.rb +618 -0
  35. data/lib/dm-core/scope.rb +35 -0
  36. data/lib/dm-core/support.rb +7 -0
  37. data/lib/dm-core/support/array.rb +13 -0
  38. data/lib/dm-core/support/assertions.rb +8 -0
  39. data/lib/dm-core/support/errors.rb +23 -0
  40. data/lib/dm-core/support/kernel.rb +7 -0
  41. data/lib/dm-core/support/symbol.rb +41 -0
  42. data/lib/dm-core/transaction.rb +267 -0
  43. data/lib/dm-core/type.rb +160 -0
  44. data/lib/dm-core/type_map.rb +80 -0
  45. data/lib/dm-core/types.rb +19 -0
  46. data/lib/dm-core/types/boolean.rb +7 -0
  47. data/lib/dm-core/types/discriminator.rb +32 -0
  48. data/lib/dm-core/types/object.rb +20 -0
  49. data/lib/dm-core/types/paranoid_boolean.rb +23 -0
  50. data/lib/dm-core/types/paranoid_datetime.rb +22 -0
  51. data/lib/dm-core/types/serial.rb +9 -0
  52. data/lib/dm-core/types/text.rb +10 -0
  53. data/spec/integration/association_spec.rb +1215 -0
  54. data/spec/integration/association_through_spec.rb +150 -0
  55. data/spec/integration/associations/many_to_many_spec.rb +171 -0
  56. data/spec/integration/associations/many_to_one_spec.rb +123 -0
  57. data/spec/integration/associations/one_to_many_spec.rb +66 -0
  58. data/spec/integration/auto_migrations_spec.rb +398 -0
  59. data/spec/integration/collection_spec.rb +1015 -0
  60. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  61. data/spec/integration/model_spec.rb +68 -0
  62. data/spec/integration/mysql_adapter_spec.rb +85 -0
  63. data/spec/integration/postgres_adapter_spec.rb +732 -0
  64. data/spec/integration/property_spec.rb +224 -0
  65. data/spec/integration/query_spec.rb +376 -0
  66. data/spec/integration/repository_spec.rb +57 -0
  67. data/spec/integration/resource_spec.rb +324 -0
  68. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  69. data/spec/integration/sti_spec.rb +185 -0
  70. data/spec/integration/transaction_spec.rb +75 -0
  71. data/spec/integration/type_spec.rb +149 -0
  72. data/spec/lib/mock_adapter.rb +27 -0
  73. data/spec/spec_helper.rb +112 -0
  74. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  75. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  76. data/spec/unit/adapters/data_objects_adapter_spec.rb +627 -0
  77. data/spec/unit/adapters/postgres_adapter_spec.rb +125 -0
  78. data/spec/unit/associations/many_to_many_spec.rb +14 -0
  79. data/spec/unit/associations/many_to_one_spec.rb +138 -0
  80. data/spec/unit/associations/one_to_many_spec.rb +385 -0
  81. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  82. data/spec/unit/associations/relationship_spec.rb +67 -0
  83. data/spec/unit/associations_spec.rb +205 -0
  84. data/spec/unit/auto_migrations_spec.rb +110 -0
  85. data/spec/unit/collection_spec.rb +174 -0
  86. data/spec/unit/data_mapper_spec.rb +21 -0
  87. data/spec/unit/identity_map_spec.rb +126 -0
  88. data/spec/unit/is_spec.rb +80 -0
  89. data/spec/unit/migrator_spec.rb +33 -0
  90. data/spec/unit/model_spec.rb +339 -0
  91. data/spec/unit/naming_conventions_spec.rb +28 -0
  92. data/spec/unit/property_set_spec.rb +96 -0
  93. data/spec/unit/property_spec.rb +447 -0
  94. data/spec/unit/query_spec.rb +485 -0
  95. data/spec/unit/repository_spec.rb +93 -0
  96. data/spec/unit/resource_spec.rb +557 -0
  97. data/spec/unit/scope_spec.rb +131 -0
  98. data/spec/unit/transaction_spec.rb +493 -0
  99. data/spec/unit/type_map_spec.rb +114 -0
  100. data/spec/unit/type_spec.rb +119 -0
  101. metadata +187 -0
@@ -0,0 +1,80 @@
1
+ # TODO: move to dm-more/dm-migrations
2
+
3
+ module DataMapper
4
+ class TypeMap
5
+
6
+ attr_accessor :parent, :chains
7
+
8
+ def initialize(parent = nil, &blk)
9
+ @parent, @chains = parent, {}
10
+
11
+ blk.call(self) unless blk.nil?
12
+ end
13
+
14
+ def map(type)
15
+ @chains[type] ||= TypeChain.new
16
+ end
17
+
18
+ def lookup(type)
19
+ if type_mapped?(type)
20
+ lookup_from_map(type)
21
+ else
22
+ lookup_by_type(type)
23
+ end
24
+ end
25
+
26
+ def lookup_from_map(type)
27
+ lookup_from_parent(type).merge(map(type).translate)
28
+ end
29
+
30
+ def lookup_from_parent(type)
31
+ if !@parent.nil? && @parent.type_mapped?(type)
32
+ @parent[type]
33
+ else
34
+ {}
35
+ end
36
+ end
37
+
38
+ # @raise <DataMapper::TypeMap::Error> if the type is not a default primitive or has a type map entry.
39
+ def lookup_by_type(type)
40
+ raise DataMapper::TypeMap::Error.new(type) unless type.respond_to?(:primitive) && !type.primitive.nil?
41
+
42
+ lookup(type.primitive).merge(Type::PROPERTY_OPTIONS.inject({}) {|h, k| h[k] = type.send(k); h})
43
+ end
44
+
45
+ alias [] lookup
46
+
47
+ def type_mapped?(type)
48
+ @chains.has_key?(type) || (@parent.nil? ? false : @parent.type_mapped?(type))
49
+ end
50
+
51
+ class TypeChain
52
+ attr_accessor :primitive, :attributes
53
+
54
+ def initialize
55
+ @attributes = {}
56
+ end
57
+
58
+ def to(primitive)
59
+ @primitive = primitive
60
+ self
61
+ end
62
+
63
+ def with(attributes)
64
+ raise "method 'with' expects a hash" unless attributes.kind_of?(Hash)
65
+ @attributes.merge!(attributes)
66
+ self
67
+ end
68
+
69
+ def translate
70
+ @attributes.merge((@primitive.nil? ? {} : {:primitive => @primitive}))
71
+ end
72
+ end # class TypeChain
73
+
74
+ class Error < StandardError
75
+ def initialize(type)
76
+ super("Type #{type} must have a default primitive or type map entry")
77
+ end
78
+ end
79
+ end # class TypeMap
80
+ end # module DataMapper
@@ -0,0 +1,19 @@
1
+ dir = Pathname(__FILE__).dirname.expand_path / 'types'
2
+
3
+ require dir / 'boolean'
4
+ require dir / 'discriminator'
5
+ require dir / 'text'
6
+ require dir / 'paranoid_datetime'
7
+ require dir / 'paranoid_boolean'
8
+ require dir / 'object'
9
+ require dir / 'serial'
10
+
11
+ unless defined?(DM)
12
+ DM = DataMapper::Types
13
+ end
14
+
15
+ module DataMapper
16
+ module Resource
17
+ include Types
18
+ end # module Resource
19
+ end # module DataMapper
@@ -0,0 +1,7 @@
1
+ module DataMapper
2
+ module Types
3
+ class Boolean < DataMapper::Type
4
+ primitive TrueClass
5
+ end # class Boolean
6
+ end # module Types
7
+ end # module DataMapper
@@ -0,0 +1,32 @@
1
+ module DataMapper
2
+ module Types
3
+ class Discriminator < DataMapper::Type
4
+ primitive Class
5
+ track :set
6
+ default lambda { |r,p| p.model }
7
+ nullable false
8
+
9
+ def self.bind(property)
10
+ model = property.model
11
+
12
+ model.class_eval <<-EOS, __FILE__, __LINE__
13
+ def self.child_classes
14
+ @child_classes ||= []
15
+ end
16
+
17
+ after_class_method :inherited, :add_scope_for_discriminator
18
+
19
+ def self.add_scope_for_discriminator(target)
20
+ target.send(:scope_stack) << DataMapper::Query.new(target.repository, target, :#{property.name} => target.child_classes << target)
21
+ propagate_child_classes(target)
22
+ end
23
+
24
+ def self.propagate_child_classes(target)
25
+ child_classes << target
26
+ superclass.send(:propagate_child_classes,target) if superclass.respond_to?(:propagate_child_classes)
27
+ end
28
+ EOS
29
+ end
30
+ end # class Discriminator
31
+ end # module Types
32
+ end # module DataMapper
@@ -0,0 +1,20 @@
1
+ require "base64"
2
+
3
+ module DataMapper
4
+ module Types
5
+ class Object < DataMapper::Type
6
+ primitive String
7
+ size 65535
8
+ lazy true
9
+ track :hash
10
+
11
+ def self.dump(value, property)
12
+ Base64.encode64(Marshal.dump(value))
13
+ end
14
+
15
+ def self.load(value, property)
16
+ value.nil? ? nil : Marshal.load(Base64.decode64(value))
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module DataMapper
2
+ module Types
3
+ class ParanoidBoolean < DataMapper::Type(Boolean)
4
+ primitive TrueClass
5
+ default false
6
+
7
+ def self.bind(property)
8
+ model = property.model
9
+ repository = property.repository
10
+
11
+ model.class_eval <<-EOS
12
+ def destroy
13
+ attribute_set(#{property.name.inspect}, true)
14
+ save
15
+ end
16
+ EOS
17
+
18
+ model.send(:scope_stack) << DataMapper::Query.new(repository, model, property.name => nil)
19
+
20
+ end
21
+ end # class ParanoidBoolean
22
+ end # module Types
23
+ end # module DataMapper
@@ -0,0 +1,22 @@
1
+ module DataMapper
2
+ module Types
3
+ class ParanoidDateTime < DataMapper::Type(DateTime)
4
+ primitive DateTime
5
+
6
+ def self.bind(property)
7
+ model = property.model
8
+ repository = property.repository
9
+
10
+ model.class_eval <<-EOS
11
+ def destroy
12
+ attribute_set(#{property.name.inspect}, DateTime.now)
13
+ save
14
+ end
15
+ EOS
16
+
17
+ model.send(:scope_stack) << DataMapper::Query.new(repository, model, property.name => nil)
18
+
19
+ end
20
+ end # class ParanoidDateTime
21
+ end # module Types
22
+ end # module DataMapper
@@ -0,0 +1,9 @@
1
+ # FIXME: can we alias this to the class Text if it isn't already defined?
2
+ module DataMapper
3
+ module Types
4
+ class Serial < DataMapper::Type
5
+ primitive Integer
6
+ serial true
7
+ end # class Text
8
+ end # module Types
9
+ end # module DataMapper
@@ -0,0 +1,10 @@
1
+ # FIXME: can we alias this to the class Text if it isn't already defined?
2
+ module DataMapper
3
+ module Types
4
+ class Text < DataMapper::Type
5
+ primitive String
6
+ size 65535
7
+ lazy true
8
+ end # class Text
9
+ end # module Types
10
+ end # module DataMapper
@@ -0,0 +1,1215 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ if ADAPTER
4
+ repository(ADAPTER) do
5
+ class Engine
6
+ include DataMapper::Resource
7
+
8
+ def self.default_repository_name
9
+ ADAPTER
10
+ end
11
+
12
+ property :id, Serial
13
+ property :name, String
14
+
15
+ has n, :yards
16
+ has n, :fussy_yards, :class_name => 'Yard', :rating.gte => 3, :type => 'particular'
17
+ end
18
+
19
+ class Yard
20
+ include DataMapper::Resource
21
+
22
+ def self.default_repository_name
23
+ ADAPTER
24
+ end
25
+
26
+ property :id, Serial
27
+ property :name, String
28
+ property :rating, Integer
29
+ property :type, String
30
+
31
+ belongs_to :engine
32
+ end
33
+
34
+ class Pie
35
+ include DataMapper::Resource
36
+
37
+ def self.default_repository_name
38
+ ADAPTER
39
+ end
40
+
41
+ property :id, Serial
42
+ property :name, String
43
+
44
+ belongs_to :sky
45
+ end
46
+
47
+ class Sky
48
+ include DataMapper::Resource
49
+
50
+ def self.default_repository_name
51
+ ADAPTER
52
+ end
53
+
54
+ property :id, Serial
55
+ property :name, String
56
+
57
+ has 1, :pie
58
+ end
59
+
60
+ class Host
61
+ include DataMapper::Resource
62
+
63
+ def self.default_repository_name
64
+ ADAPTER
65
+ end
66
+
67
+ property :id, Serial
68
+ property :name, String
69
+
70
+ has n, :slices, :order => [:id.desc]
71
+ end
72
+
73
+ class Slice
74
+ include DataMapper::Resource
75
+
76
+ def self.default_repository_name
77
+ ADAPTER
78
+ end
79
+
80
+ property :id, Serial
81
+ property :name, String
82
+
83
+ belongs_to :host
84
+ end
85
+
86
+ class Node
87
+ include DataMapper::Resource
88
+
89
+ def self.default_repository_name
90
+ ADAPTER
91
+ end
92
+
93
+ property :id, Serial
94
+ property :name, String
95
+
96
+ has n, :children, :class_name => 'Node', :child_key => [ :parent_id ]
97
+ belongs_to :parent, :class_name => 'Node', :child_key => [ :parent_id ]
98
+ end
99
+
100
+ module Models
101
+ class Project
102
+ include DataMapper::Resource
103
+
104
+ def self.default_repository_name
105
+ ADAPTER
106
+ end
107
+
108
+ property :title, String, :length => 255, :key => true
109
+ property :summary, DataMapper::Types::Text
110
+
111
+ has n, :tasks, :class_name => 'Models::Task'
112
+ has 1, :goal, :class_name => 'Models::Goal'
113
+ end
114
+
115
+ class Goal
116
+ include DataMapper::Resource
117
+
118
+ def self.default_repository_name
119
+ ADAPTER
120
+ end
121
+
122
+ property :title, String, :length => 255, :key => true
123
+ property :summary, DataMapper::Types::Text
124
+
125
+ belongs_to :project, :class_name => "Models::Project"
126
+ end
127
+
128
+ class Task
129
+ include DataMapper::Resource
130
+
131
+ def self.default_repository_name
132
+ ADAPTER
133
+ end
134
+
135
+ property :title, String, :length => 255, :key => true
136
+ property :description, DataMapper::Types::Text
137
+
138
+ belongs_to :project, :class_name => 'Models::Project'
139
+ end
140
+ end
141
+
142
+ class Galaxy
143
+ include DataMapper::Resource
144
+
145
+ def self.default_repository_name
146
+ ADAPTER
147
+ end
148
+
149
+ property :name, String, :key => true, :length => 255
150
+ property :size, Float, :key => true, :precision => 15, :scale => 6
151
+ end
152
+
153
+ class Star
154
+ include DataMapper::Resource
155
+
156
+ def self.default_repository_name
157
+ ADAPTER
158
+ end
159
+
160
+ belongs_to :galaxy
161
+ end
162
+ end
163
+
164
+ describe DataMapper::Associations do
165
+ describe 'namespaced associations' do
166
+ before do
167
+ Models::Project.auto_migrate!(ADAPTER)
168
+ Models::Task.auto_migrate!(ADAPTER)
169
+ Models::Goal.auto_migrate!(ADAPTER)
170
+ end
171
+
172
+ it 'should allow namespaced classes in parent and child for many <=> one' do
173
+ m = Models::Project.new(:title => 'p1', :summary => 'sum1')
174
+ m.tasks << Models::Task.new(:title => 't1', :description => 'desc 1')
175
+ m.save
176
+
177
+ t = Models::Task.first(:title => 't1')
178
+
179
+ t.project.should_not be_nil
180
+ t.project.title.should == 'p1'
181
+ t.project.tasks.size.should == 1
182
+
183
+ p = Models::Project.first(:title => 'p1')
184
+
185
+ p.tasks.size.should == 1
186
+ p.tasks[0].title.should == 't1'
187
+ end
188
+
189
+ it 'should allow namespaced classes in parent and child for one <=> one' do
190
+ g = Models::Goal.new(:title => "g2", :description => "desc 2")
191
+ p = Models::Project.create!(:title => "p2", :summary => "sum 2", :goal => g)
192
+
193
+ pp = Models::Project.first(:title => 'p2')
194
+ pp.goal.title.should == "g2"
195
+
196
+ g = Models::Goal.first(:title => "g2")
197
+
198
+ g.project.should_not be_nil
199
+ g.project.title.should == 'p2'
200
+
201
+ g.project.goal.should_not be_nil
202
+ end
203
+ end
204
+
205
+ describe 'many to one associations' do
206
+ before do
207
+ Engine.auto_migrate!(ADAPTER)
208
+ Yard.auto_migrate!(ADAPTER)
209
+
210
+ engine1 = Engine.create!(:name => 'engine1')
211
+ engine2 = Engine.create!(:name => 'engine2')
212
+ yard1 = Yard.create!(:name => 'yard1', :engine => engine1)
213
+ yard2 = Yard.create!(:name => 'yard2')
214
+ end
215
+
216
+ it '#belongs_to' do
217
+ yard = Yard.new
218
+ yard.should respond_to(:engine)
219
+ yard.should respond_to(:engine=)
220
+ end
221
+
222
+ it 'should load without the parent'
223
+
224
+ it 'should allow substituting the parent' do
225
+ yard1 = Yard.first(:name => 'yard1')
226
+ engine2 = Engine.first(:name => 'engine2')
227
+
228
+ yard1.engine = engine2
229
+ yard1.save
230
+ Yard.first(:name => 'yard1').engine.should == engine2
231
+ end
232
+
233
+ it '#belongs_to with namespaced models' do
234
+ repository(ADAPTER) do
235
+ module FlightlessBirds
236
+ class Ostrich
237
+ include DataMapper::Resource
238
+ property :id, Serial
239
+ property :name, String
240
+ belongs_to :sky # there's something sad about this :'(
241
+ end
242
+ end
243
+
244
+ FlightlessBirds::Ostrich.properties.slice(:sky_id).should_not be_empty
245
+ end
246
+ end
247
+
248
+ it 'should load the associated instance' do
249
+ engine1 = Engine.first(:name => 'engine1')
250
+ Yard.first(:name => 'yard1').engine.should == engine1
251
+ end
252
+
253
+ it 'should save the association key in the child' do
254
+ engine2 = Engine.first(:name => 'engine2')
255
+
256
+ Yard.create!(:name => 'yard3', :engine => engine2)
257
+ Yard.first(:name => 'yard3').engine.should == engine2
258
+ end
259
+
260
+ it 'should set the association key immediately' do
261
+ engine = Engine.first(:name => 'engine1')
262
+ Yard.new(:engine => engine).engine_id.should == engine.id
263
+ end
264
+
265
+ it 'should save the parent upon saving of child' do
266
+ e = Engine.new(:name => 'engine10')
267
+ y = Yard.create!(:name => 'yard10', :engine => e)
268
+
269
+ y.engine.name.should == 'engine10'
270
+ Engine.first(:name => 'engine10').should_not be_nil
271
+ end
272
+
273
+ it 'should convert NULL parent ids into nils' do
274
+ Yard.first(:name => 'yard2').engine.should be_nil
275
+ end
276
+
277
+ it 'should save nil parents as NULL ids' do
278
+ y1 = Yard.create!(:id => 20, :name => 'yard20')
279
+ y2 = Yard.create!(:id => 30, :name => 'yard30', :engine => nil)
280
+
281
+ y1.id.should == 20
282
+ y1.engine.should be_nil
283
+ y2.id.should == 30
284
+ y2.engine.should be_nil
285
+ end
286
+
287
+ it 'should respect length on foreign keys' do
288
+ property = Star.relationships[:galaxy].child_key[:galaxy_name]
289
+ property.length.should == 255
290
+ end
291
+
292
+ it 'should respect precision and scale on foreign keys' do
293
+ property = Star.relationships[:galaxy].child_key[:galaxy_size]
294
+ property.precision.should == 15
295
+ property.scale.should == 6
296
+ end
297
+
298
+ it 'should be reloaded when calling Resource#reload' do
299
+ e = Engine.new(:name => 'engine40')
300
+ y = Yard.create!(:name => 'yard40', :engine => e)
301
+
302
+ y.send(:engine_association).should_receive(:reload).once
303
+
304
+ lambda { y.reload }.should_not raise_error
305
+ end
306
+ end
307
+
308
+ describe 'one to one associations' do
309
+ before do
310
+ Sky.auto_migrate!(ADAPTER)
311
+ Pie.auto_migrate!(ADAPTER)
312
+
313
+ pie1 = Pie.create!(:name => 'pie1')
314
+ pie2 = Pie.create!(:name => 'pie2')
315
+ sky1 = Sky.create!(:name => 'sky1', :pie => pie1)
316
+ end
317
+
318
+ it '#has 1' do
319
+ s = Sky.new
320
+ s.should respond_to(:pie)
321
+ s.should respond_to(:pie=)
322
+ end
323
+
324
+ it 'should allow substituting the child' do
325
+ sky1 = Sky.first(:name => 'sky1')
326
+ pie1 = Pie.first(:name => 'pie1')
327
+ pie2 = Pie.first(:name => 'pie2')
328
+
329
+ sky1.pie.should == pie1
330
+ pie2.sky.should be_nil
331
+
332
+ sky1.pie = pie2
333
+ sky1.save
334
+
335
+ pie2.sky.should == sky1
336
+ pie1.reload.sky.should be_nil
337
+ end
338
+
339
+ it 'should load the associated instance' do
340
+ sky1 = Sky.first(:name => 'sky1')
341
+ pie1 = Pie.first(:name => 'pie1')
342
+
343
+ sky1.pie.should == pie1
344
+ end
345
+
346
+ it 'should save the association key in the child' do
347
+ pie2 = Pie.first(:name => 'pie2')
348
+
349
+ sky2 = Sky.create!(:id => 2, :name => 'sky2', :pie => pie2)
350
+ pie2.sky.should == sky2
351
+ end
352
+
353
+ it 'should save the children upon saving of parent' do
354
+ p = Pie.new(:id => 10, :name => 'pie10')
355
+ s = Sky.create!(:id => 10, :name => 'sky10', :pie => p)
356
+
357
+ p.sky.should == s
358
+
359
+ Pie.first(:name => 'pie10').should_not be_nil
360
+ end
361
+
362
+ it 'should save nil parents as NULL ids' do
363
+ p1 = Pie.create!(:id => 20, :name => 'pie20')
364
+ p2 = Pie.create!(:id => 30, :name => 'pie30', :sky => nil)
365
+
366
+ p1.id.should == 20
367
+ p1.sky.should be_nil
368
+ p2.id.should == 30
369
+ p2.sky.should be_nil
370
+ end
371
+
372
+ it 'should be reloaded when calling Resource#reload' do
373
+ pie = Pie.first(:name => 'pie1')
374
+ pie.send(:sky_association).should_receive(:reload).once
375
+ lambda { pie.reload }.should_not raise_error
376
+ end
377
+ end
378
+
379
+ describe 'one to many associations' do
380
+ before do
381
+ Host.auto_migrate!(ADAPTER)
382
+ Slice.auto_migrate!(ADAPTER)
383
+ Engine.auto_migrate!(ADAPTER)
384
+ Yard.auto_migrate!(ADAPTER)
385
+
386
+ host1 = Host.create!(:name => 'host1')
387
+ host2 = Host.create!(:name => 'host2')
388
+ slice1 = Slice.create!(:name => 'slice1', :host => host1)
389
+ slice2 = Slice.create!(:name => 'slice2', :host => host1)
390
+ slice3 = Slice.create!(:name => 'slice3')
391
+ end
392
+
393
+ it '#has n' do
394
+ h = Host.new
395
+ h.should respond_to(:slices)
396
+ end
397
+
398
+ it 'should allow removal of a child through a loaded association' do
399
+ host1 = Host.first(:name => 'host1')
400
+ slice2 = host1.slices.first
401
+
402
+ host1.slices.size.should == 2
403
+ host1.slices.delete(slice2)
404
+ host1.slices.size.should == 1
405
+
406
+ slice2 = Slice.first(:name => 'slice2')
407
+ slice2.host.should_not be_nil
408
+
409
+ host1.save
410
+
411
+ slice2.reload.host.should be_nil
412
+ end
413
+
414
+ it 'should use the IdentityMap correctly' do
415
+ repository(ADAPTER) do
416
+ host1 = Host.first(:name => 'host1')
417
+
418
+ slice = host1.slices.first
419
+ slice2 = host1.slices(:order => [:id]).last # should be the same as 1
420
+ slice3 = Slice.get(2) # should be the same as 1
421
+
422
+ slice.object_id.should == slice2.object_id
423
+ slice.object_id.should == slice3.object_id
424
+ end
425
+ end
426
+
427
+ it '#<< should add exactly the parameters' do
428
+ engine = Engine.new(:name => 'my engine')
429
+ 4.times do |i|
430
+ engine.yards << Yard.new(:name => "yard nr #{i}")
431
+ end
432
+ engine.save
433
+ engine.yards.size.should == 4
434
+ 4.times do |i|
435
+ engine.yards.any? do |yard|
436
+ yard.name == "yard nr #{i}"
437
+ end.should == true
438
+ end
439
+ engine = Engine.get!(engine.id)
440
+ engine.yards.size.should == 4
441
+ 4.times do |i|
442
+ engine.yards.any? do |yard|
443
+ yard.name == "yard nr #{i}"
444
+ end.should == true
445
+ end
446
+ end
447
+
448
+ it '#<< should add default values for relationships that have conditions' do
449
+ # it should add default values
450
+ engine = Engine.new(:name => 'my engine')
451
+ engine.fussy_yards << Yard.new(:name => 'yard 1', :rating => 4 )
452
+ engine.save
453
+ Yard.first(:name => 'yard 1').type.should == 'particular'
454
+ # it should not add default values if the condition's property already has a value
455
+ engine.fussy_yards << Yard.new(:name => 'yard 2', :rating => 4, :type => 'not particular')
456
+ engine.save
457
+ Yard.first(:name => 'yard 2').type.should == 'not particular'
458
+ # it should ignore non :eql conditions
459
+ engine.fussy_yards << Yard.new(:name => 'yard 3')
460
+ engine.save
461
+ Yard.first(:name => 'yard 3').rating.should == nil
462
+ end
463
+
464
+ it 'should load the associated instances, in the correct order' do
465
+ host1 = Host.first(:name => 'host1')
466
+
467
+ host1.slices.should_not be_nil
468
+ host1.slices.size.should == 2
469
+ host1.slices.first.name.should == 'slice2' # ordered by [:id.desc]
470
+ host1.slices.last.name.should == 'slice1'
471
+
472
+ slice3 = Slice.first(:name => 'slice3')
473
+
474
+ slice3.host.should be_nil
475
+ end
476
+
477
+ it 'should add and save the associated instance' do
478
+ host1 = Host.first(:name => 'host1')
479
+ host1.slices << Slice.new(:id => 4, :name => 'slice4')
480
+ host1.save
481
+
482
+ Slice.first(:name => 'slice4').host.should == host1
483
+ end
484
+
485
+ it 'should not save the associated instance if the parent is not saved' do
486
+ h = Host.new(:id => 10, :name => 'host10')
487
+ h.slices << Slice.new(:id => 10, :name => 'slice10')
488
+
489
+ Slice.first(:name => 'slice10').should be_nil
490
+ end
491
+
492
+ it 'should save the associated instance upon saving of parent' do
493
+ h = Host.new(:id => 10, :name => 'host10')
494
+ h.slices << Slice.new(:id => 10, :name => 'slice10')
495
+ h.save
496
+
497
+ s = Slice.first(:name => 'slice10')
498
+
499
+ s.should_not be_nil
500
+ s.host.should == h
501
+ end
502
+
503
+ it 'should save the associated instances upon saving of parent when mass-assigned' do
504
+ h = Host.create!(:id => 10, :name => 'host10', :slices => [ Slice.new(:id => 10, :name => 'slice10') ])
505
+
506
+ s = Slice.first(:name => 'slice10')
507
+
508
+ s.should_not be_nil
509
+ s.host.should == h
510
+ end
511
+
512
+ it 'should have finder-functionality' do
513
+ h = Host.first(:name => 'host1')
514
+
515
+ h.slices.should have(2).entries
516
+
517
+ s = h.slices.all(:name => 'slice2')
518
+
519
+ s.should have(1).entries
520
+ s.first.id.should == 2
521
+
522
+ h.slices.first(:name => 'slice2').should == s.first
523
+ end
524
+
525
+ it 'should be reloaded when calling Resource#reload' do
526
+ host = Host.first(:name => 'host1')
527
+ host.send(:slices_association).should_receive(:reload).once
528
+ lambda { host.reload }.should_not raise_error
529
+ end
530
+ end
531
+
532
+ describe 'many-to-one and one-to-many associations combined' do
533
+ before do
534
+ Node.auto_migrate!(ADAPTER)
535
+
536
+ Node.create!(:name => 'r1')
537
+ Node.create!(:name => 'r2')
538
+ Node.create!(:name => 'r1c1', :parent_id => 1)
539
+ Node.create!(:name => 'r1c2', :parent_id => 1)
540
+ Node.create!(:name => 'r1c3', :parent_id => 1)
541
+ Node.create!(:name => 'r1c1c1', :parent_id => 3)
542
+ end
543
+
544
+ it 'should properly set #parent' do
545
+ r1 = Node.get 1
546
+ r1.parent.should be_nil
547
+
548
+ n3 = Node.get 3
549
+ n3.parent.should == r1
550
+
551
+ n6 = Node.get 6
552
+ n6.parent.should == n3
553
+ end
554
+
555
+ it 'should properly set #children' do
556
+ r1 = Node.get(1)
557
+ off = r1.children
558
+ off.size.should == 3
559
+ off.include?(Node.get(3)).should be_true
560
+ off.include?(Node.get(4)).should be_true
561
+ off.include?(Node.get(5)).should be_true
562
+ end
563
+
564
+ it 'should allow to create root nodes' do
565
+ r = Node.create!(:name => 'newroot')
566
+ r.parent.should be_nil
567
+ r.children.size.should == 0
568
+ end
569
+
570
+ it 'should properly delete nodes' do
571
+ r1 = Node.get 1
572
+
573
+ r1.children.size.should == 3
574
+ r1.children.delete(Node.get(4))
575
+ r1.save
576
+ Node.get(4).parent.should be_nil
577
+ r1.children.size.should == 2
578
+ end
579
+ end
580
+
581
+ describe 'through-associations' do
582
+ before :all do
583
+ repository(ADAPTER) do
584
+ module Sweets
585
+ class Shop
586
+ include DataMapper::Resource
587
+ def self.default_repository_name
588
+ ADAPTER
589
+ end
590
+ property :id, Serial
591
+ property :name, String
592
+ has n, :cakes, :class_name => 'Sweets::Cake' # has n
593
+ has n, :recipes, :through => :cakes, :class_name => 'Sweets::Recipe' # has n => has 1
594
+ has n, :ingredients, :through => :cakes, :class_name => 'Sweets::Ingredient' # has n => has 1 => has n
595
+ has n, :creators, :through => :cakes, :class_name => 'Sweets::Creator' # has n => has 1 => has 1
596
+ has n, :slices, :through => :cakes, :class_name => 'Sweets::Slice' # has n => has n
597
+ has n, :bites, :through => :cakes, :class_name => 'Sweets::Bite' # has n => has n => has n
598
+ has n, :shapes, :through => :cakes, :class_name => 'Sweets::Shape' # has n => has n => has 1
599
+ has n, :customers, :through => :cakes, :class_name => 'Sweets::Customer' # has n => belongs_to (pending)
600
+ has 1, :shop_owner, :class_name => 'Sweets::ShopOwner' # has 1
601
+ has 1, :wife, :through => :shop_owner, :class_name => 'Sweets::Wife' # has 1 => has 1
602
+ has 1, :ring, :through => :shop_owner, :class_name => 'Sweets::Ring' # has 1 => has 1 => has 1
603
+ has n, :coats, :through => :shop_owner, :class_name => 'Sweets::Coat' # has 1 => has 1 => has n
604
+ has n, :children, :through => :shop_owner, :class_name => 'Sweets::Child' # has 1 => has n
605
+ has n, :toys, :through => :shop_owner, :class_name => 'Sweets::Toy' # has 1 => has n => has n
606
+ has n, :boogers, :through => :shop_owner, :class_name => 'Sweets::Booger' # has 1 => has n => has 1
607
+ end
608
+
609
+ class ShopOwner
610
+ include DataMapper::Resource
611
+ def self.default_repository_name
612
+ ADAPTER
613
+ end
614
+ property :id, Serial
615
+ property :name, String
616
+ belongs_to :shop, :class_name => 'Sweets::Shop'
617
+ has 1, :wife, :class_name => 'Sweets::Wife'
618
+ has n, :children, :class_name => 'Sweets::Child'
619
+ has n, :toys, :through => :children, :class_name => 'Sweets::Toy'
620
+ has n, :boogers, :through => :children, :class_name => 'Sweets::Booger'
621
+ has n, :coats, :through => :wife, :class_name => 'Sweets::Coat'
622
+ has 1, :ring, :through => :wife, :class_name => 'Sweets::Ring'
623
+ has n, :schools, :through => :children, :class_name => 'Sweets::School'
624
+ end
625
+
626
+ class Wife
627
+ include DataMapper::Resource
628
+ def self.default_repository_name
629
+ ADAPTER
630
+ end
631
+ property :id, Serial
632
+ property :name, String
633
+ belongs_to :shop_owner, :class_name => 'Sweets::ShopOwner'
634
+ has 1, :ring, :class_name => 'Sweets::Ring'
635
+ has n, :coats, :class_name => 'Sweets::Coat'
636
+ end
637
+
638
+ class Coat
639
+ include DataMapper::Resource
640
+ def self.default_repository_name
641
+ ADAPTER
642
+ end
643
+ property :id, Serial
644
+ property :name, String
645
+ belongs_to :wife, :class_name => 'Sweets::Wife'
646
+ end
647
+
648
+ class Ring
649
+ include DataMapper::Resource
650
+ def self.default_repository_name
651
+ ADAPTER
652
+ end
653
+ property :id, Serial
654
+ property :name, String
655
+ belongs_to :wife, :class_name => 'Sweets::Wife'
656
+ end
657
+
658
+ class Child
659
+ include DataMapper::Resource
660
+ def self.default_repository_name
661
+ ADAPTER
662
+ end
663
+ property :id, Serial
664
+ property :name, String
665
+ belongs_to :shop_owner, :class_name => 'Sweets::ShopOwner'
666
+ has n, :toys, :class_name => 'Sweets::Toy'
667
+ has 1, :booger, :class_name => 'Sweets::Booger'
668
+ end
669
+
670
+ class Booger
671
+ include DataMapper::Resource
672
+ def self.default_repository_name
673
+ ADAPTER
674
+ end
675
+ property :id, Serial
676
+ property :name, String
677
+ belongs_to :child, :class_name => 'Sweets::Child'
678
+ end
679
+
680
+ class Toy
681
+ include DataMapper::Resource
682
+ def self.default_repository_name
683
+ ADAPTER
684
+ end
685
+ property :id, Serial
686
+ property :name, String
687
+ belongs_to :child, :class_name => 'Sweets::Child'
688
+ end
689
+
690
+ class Cake
691
+ include DataMapper::Resource
692
+ def self.default_repository_name
693
+ ADAPTER
694
+ end
695
+ property :id, Serial
696
+ property :name, String
697
+ belongs_to :shop, :class_name => 'Sweets::Shop'
698
+ belongs_to :customer, :class_name => 'Sweets::Customer'
699
+ has n, :slices, :class_name => 'Sweets::Slice'
700
+ has n, :bites, :through => :slices, :class_name => 'Sweets::Bite'
701
+ has 1, :recipe, :class_name => 'Sweets::Recipe'
702
+ has n, :ingredients, :through => :recipe, :class_name => 'Sweets::Ingredient'
703
+ has 1, :creator, :through => :recipe, :class_name => 'Sweets::Creator'
704
+ has n, :shapes, :through => :slices, :class_name => 'Sweets::Shape'
705
+ end
706
+
707
+ class Recipe
708
+ include DataMapper::Resource
709
+ def self.default_repository_name
710
+ ADAPTER
711
+ end
712
+ property :id, Serial
713
+ property :name, String
714
+ belongs_to :cake, :class_name => 'Sweets::Cake'
715
+ has n, :ingredients, :class_name => 'Sweets::Ingredient'
716
+ has 1, :creator, :class_name => 'Sweets::Creator'
717
+ end
718
+
719
+ class Customer
720
+ include DataMapper::Resource
721
+ def self.default_repository_name
722
+ ADAPTER
723
+ end
724
+ property :id, Serial
725
+ property :name, String
726
+ has n, :cakes, :class_name => 'Sweets::Cake'
727
+ end
728
+
729
+ class Creator
730
+ include DataMapper::Resource
731
+ def self.default_repository_name
732
+ ADAPTER
733
+ end
734
+ property :id, Serial
735
+ property :name, String
736
+ belongs_to :recipe, :class_name => 'Sweets::Recipe'
737
+ end
738
+
739
+ class Ingredient
740
+ include DataMapper::Resource
741
+ def self.default_repository_name
742
+ ADAPTER
743
+ end
744
+ property :id, Serial
745
+ property :name, String
746
+ belongs_to :recipe, :class_name => 'Sweets::Recipe'
747
+ end
748
+
749
+ class Slice
750
+ include DataMapper::Resource
751
+ def self.default_repository_name
752
+ ADAPTER
753
+ end
754
+ property :id, Serial
755
+ property :size, Integer
756
+ belongs_to :cake, :class_name => 'Sweets::Cake'
757
+ has n, :bites, :class_name => 'Sweets::Bite'
758
+ has 1, :shape, :class_name => 'Sweets::Shape'
759
+ end
760
+
761
+ class Shape
762
+ include DataMapper::Resource
763
+ def self.default_repository_name
764
+ ADAPTER
765
+ end
766
+ property :id, Serial
767
+ property :name, String
768
+ belongs_to :slice, :class_name => 'Sweets::Slice'
769
+ end
770
+
771
+ class Bite
772
+ include DataMapper::Resource
773
+ def self.default_repository_name
774
+ ADAPTER
775
+ end
776
+ property :id, Serial
777
+ property :name, String
778
+ belongs_to :slice, :class_name => 'Sweets::Slice'
779
+ end
780
+
781
+ DataMapper::Resource.descendants.each do |descendant|
782
+ descendant.auto_migrate!(ADAPTER) if descendant.name =~ /^Sweets::/
783
+ end
784
+
785
+ betsys = Shop.new(:name => "Betsy's")
786
+ betsys.save
787
+
788
+ #
789
+ # has n
790
+ #
791
+
792
+ german_chocolate = Cake.new(:name => 'German Chocolate')
793
+ betsys.cakes << german_chocolate
794
+ german_chocolate.save
795
+ short_cake = Cake.new(:name => 'Short Cake')
796
+ betsys.cakes << short_cake
797
+ short_cake.save
798
+
799
+ # has n => belongs_to
800
+
801
+ old_customer = Customer.new(:name => 'John Johnsen')
802
+ old_customer.cakes << german_chocolate
803
+ old_customer.cakes << short_cake
804
+ german_chocolate.save
805
+ short_cake.save
806
+ old_customer.save
807
+
808
+ # has n => has 1
809
+
810
+ schwarzwald = Recipe.new(:name => 'Schwarzwald Cake')
811
+ schwarzwald.save
812
+ german_chocolate.recipe = schwarzwald
813
+ german_chocolate.save
814
+ shortys_special = Recipe.new(:name => "Shorty's Special")
815
+ shortys_special.save
816
+ short_cake.recipe = shortys_special
817
+ short_cake.save
818
+
819
+ # has n => has 1 => has 1
820
+
821
+ runar = Creator.new(:name => 'Runar')
822
+ schwarzwald.creator = runar
823
+ runar.save
824
+ berit = Creator.new(:name => 'Berit')
825
+ shortys_special.creator = berit
826
+ berit.save
827
+
828
+ # has n => has 1 => has n
829
+
830
+ 4.times do |i| schwarzwald.ingredients << Ingredient.new(:name => "Secret ingredient nr #{i}") end
831
+ 6.times do |i| shortys_special.ingredients << Ingredient.new(:name => "Well known ingredient nr #{i}") end
832
+
833
+ # has n => has n
834
+
835
+ 10.times do |i| german_chocolate.slices << Slice.new(:size => i) end
836
+ 5.times do |i| short_cake.slices << Slice.new(:size => i) end
837
+ german_chocolate.slices.size.should == 10
838
+ # has n => has n => has 1
839
+
840
+ german_chocolate.slices.each do |slice|
841
+ shape = Shape.new(:name => 'square')
842
+ slice.shape = shape
843
+ shape.save
844
+ end
845
+ short_cake.slices.each do |slice|
846
+ shape = Shape.new(:name => 'round')
847
+ slice.shape = shape
848
+ shape.save
849
+ end
850
+
851
+ # has n => has n => has n
852
+ german_chocolate.slices.each do |slice|
853
+ 6.times do |i|
854
+ slice.bites << Bite.new(:name => "Big bite nr #{i}")
855
+ end
856
+ end
857
+ short_cake.slices.each do |slice|
858
+ 3.times do |i|
859
+ slice.bites << Bite.new(:name => "Small bite nr #{i}")
860
+ end
861
+ end
862
+
863
+ #
864
+ # has 1
865
+ #
866
+
867
+ betsy = ShopOwner.new(:name => 'Betsy')
868
+ betsys.shop_owner = betsy
869
+ betsys.save
870
+
871
+ # has 1 => has 1
872
+
873
+ barry = Wife.new(:name => 'Barry')
874
+ betsy.wife = barry
875
+ barry.save
876
+
877
+ # has 1 => has 1 => has 1
878
+
879
+ golden = Ring.new(:name => 'golden')
880
+ barry.ring = golden
881
+ golden.save
882
+
883
+ # has 1 => has 1 => has n
884
+
885
+ 3.times do |i|
886
+ barry.coats << Coat.new(:name => "Fancy coat nr #{i}")
887
+ end
888
+ barry.save
889
+
890
+ # has 1 => has n
891
+
892
+ 5.times do |i|
893
+ betsy.children << Child.new(:name => "Snotling nr #{i}")
894
+ end
895
+ betsy.save
896
+
897
+ # has 1 => has n => has n
898
+
899
+ betsy.children.each do |child|
900
+ 4.times do |i|
901
+ child.toys << Toy.new(:name => "Cheap toy nr #{i}")
902
+ end
903
+ child.save
904
+ end
905
+
906
+ # has 1 => has n => has 1
907
+
908
+ betsy.children.each do |child|
909
+ booger = Booger.new(:name => 'Nasty booger')
910
+ child.booger = booger
911
+ child.save
912
+ end
913
+ end
914
+ end
915
+ end
916
+
917
+ #
918
+ # has n
919
+ #
920
+
921
+ it 'should return the right children for has n => has n relationships' do
922
+ Sweets::Shop.first.slices.size.should == 15
923
+ 10.times do |i|
924
+ Sweets::Shop.first.slices.select do |slice|
925
+ slice.cake == Sweets::Cake.first(:name => 'German Chocolate') && slice.size == i
926
+ end
927
+ end
928
+ end
929
+
930
+ it 'should return the right children for has n => has n => has 1' do
931
+ Sweets::Shop.first.shapes.size.should == 15
932
+ Sweets::Shop.first.shapes.select do |shape|
933
+ shape.name == 'square'
934
+ end.size.should == 10
935
+ Sweets::Shop.first.shapes.select do |shape|
936
+ shape.name == 'round'
937
+ end.size.should == 5
938
+ end
939
+
940
+ it 'should return the right children for has n => has n => has n' do
941
+ Sweets::Shop.first.bites.size.should == 75
942
+ Sweets::Shop.first.bites.select do |bite|
943
+ bite.slice.cake == Sweets::Cake.first(:name => 'German Chocolate')
944
+ end.size.should == 60
945
+ Sweets::Shop.first.bites.select do |bite|
946
+ bite.slice.cake == Sweets::Cake.first(:name => 'Short Cake')
947
+ end.size.should == 15
948
+ end
949
+
950
+ it 'should return the right children for has n => belongs_to relationships' do
951
+ Sweets::Customer.first.cakes.size.should == 2
952
+ customers = Sweets::Shop.first.customers.select do |customer|
953
+ customer.name == 'John Johnsen'
954
+ end
955
+ customers.size.should == 1
956
+ # another example can be found here: http://pastie.textmate.org/private/tt1hf1syfsytyxdgo4qxawfl
957
+ end
958
+
959
+ it 'should return the right children for has n => has 1 relationships' do
960
+ Sweets::Shop.first.recipes.size.should == 2
961
+ Sweets::Shop.first.recipes.select do |recipe|
962
+ recipe.name == 'Schwarzwald Cake'
963
+ end.size.should == 1
964
+ Sweets::Shop.first.recipes.select do |recipe|
965
+ recipe.name == "Shorty's Special"
966
+ end.size.should == 1
967
+ end
968
+
969
+ it 'should return the right children for has n => has 1 => has 1 relationships' do
970
+ Sweets::Shop.first.creators.size.should == 2
971
+ Sweets::Shop.first.creators.any? do |creator|
972
+ creator.name == 'Runar'
973
+ end.should == true
974
+ Sweets::Shop.first.creators.any? do |creator|
975
+ creator.name == 'Berit'
976
+ end.should == true
977
+ end
978
+
979
+ it 'should return the right children for has n => has 1 => has n relationships' do
980
+ Sweets::Shop.first.ingredients.size.should == 10
981
+ 4.times do |i|
982
+ Sweets::Shop.first.ingredients.any? do |ingredient|
983
+ ingredient.name == "Secret ingredient nr #{i}" && ingredient.recipe.cake == Sweets::Cake.first(:name => 'German Chocolate')
984
+ end.should == true
985
+ end
986
+ 6.times do |i|
987
+ Sweets::Shop.first.ingredients.any? do |ingredient|
988
+ ingredient.name == "Well known ingredient nr #{i}" && ingredient.recipe.cake == Sweets::Cake.first(:name => 'Short Cake')
989
+ end.should == true
990
+ end
991
+ end
992
+
993
+ #
994
+ # has 1
995
+ #
996
+
997
+ it 'should return the right children for has 1 => has 1 relationships' do
998
+ Sweets::Shop.first.wife.should == Sweets::Wife.first
999
+ end
1000
+
1001
+ it 'should return the right children for has 1 => has 1 => has 1 relationships' do
1002
+ Sweets::Shop.first.ring.should == Sweets::Ring.first
1003
+ end
1004
+
1005
+ it 'should return the right children for has 1 => has 1 => has n relationships' do
1006
+ Sweets::Shop.first.coats.size.should == 3
1007
+ 3.times do |i|
1008
+ Sweets::Shop.first.coats.any? do |coat|
1009
+ coat.name == "Fancy coat nr #{i}"
1010
+ end.should == true
1011
+ end
1012
+ end
1013
+
1014
+ it 'should return the right children for has 1 => has n relationships' do
1015
+ Sweets::Shop.first.children.size.should == 5
1016
+ 5.times do |i|
1017
+ Sweets::Shop.first.children.any? do |child|
1018
+ child.name == "Snotling nr #{i}"
1019
+ end.should == true
1020
+ end
1021
+ end
1022
+
1023
+ it 'should return the right children for has 1 => has n => has 1 relationships' do
1024
+ Sweets::Shop.first.boogers.size.should == 5
1025
+ Sweets::Shop.first.boogers.inject(Set.new) do |sum, booger|
1026
+ sum << booger.child_id
1027
+ end.size.should == 5
1028
+ end
1029
+
1030
+ it 'should return the right children for has 1 => has n => has n relationships' do
1031
+ Sweets::Shop.first.toys.size.should == 20
1032
+ 5.times do |child_nr|
1033
+ 4.times do |toy_nr|
1034
+ Sweets::Shop.first.toys.any? do |toy|
1035
+ toy.name == "Cheap toy nr #{toy_nr}" && toy.child = Sweets::Child.first(:name => "Snotling nr #{child_nr}")
1036
+ end.should == true
1037
+ end
1038
+ end
1039
+ end
1040
+
1041
+ #
1042
+ # misc
1043
+ #
1044
+
1045
+ it 'should raise exception if you try to change it' do
1046
+ lambda do
1047
+ Sweets::Shop.first.wife = Sweets::Wife.new(:name => 'Larry')
1048
+ end.should raise_error(DataMapper::Associations::ImmutableAssociationError)
1049
+ end
1050
+
1051
+ it 'should be reloaded when calling Resource#reload' do
1052
+ betsys = Sweets::Shop.first(:name => "Betsy's")
1053
+ betsys.send(:customers_association).should_receive(:reload).once
1054
+ lambda { betsys.reload }.should_not raise_error
1055
+ end
1056
+
1057
+ end
1058
+
1059
+ if false # Many to many not yet implemented
1060
+ describe "many to many associations" do
1061
+ before(:all) do
1062
+ class RightItem
1063
+ include DataMapper::Resource
1064
+
1065
+ def self.default_repository_name
1066
+ ADAPTER
1067
+ end
1068
+
1069
+ property :id, Serial
1070
+ property :name, String
1071
+
1072
+ has n..n, :left_items
1073
+ end
1074
+
1075
+ class LeftItem
1076
+ include DataMapper::Resource
1077
+
1078
+ def self.default_repository_name
1079
+ ADAPTER
1080
+ end
1081
+
1082
+ property :id, Serial
1083
+ property :name, String
1084
+
1085
+ has n..n, :right_items
1086
+ end
1087
+
1088
+ RightItem.auto_migrate!
1089
+ LeftItem.auto_migrate!
1090
+ end
1091
+
1092
+ def create_item_pair(number)
1093
+ @ri = RightItem.new(:name => "ri#{number}")
1094
+ @li = LeftItem.new(:name => "li#{number}")
1095
+ end
1096
+
1097
+ it "should add to the assocaiton from the left" do
1098
+ pending "Waiting on Many To Many to be implemented"
1099
+ create_item_pair "0000"
1100
+ @ri.save; @li.save
1101
+ @ri.should_not be_new_record
1102
+ @li.should_not be_new_record
1103
+
1104
+ @li.right_items << @ri
1105
+ @li.right_items.should include(@ri)
1106
+ @li.reload
1107
+ @ri.reload
1108
+ @li.right_items.should include(@ri)
1109
+ end
1110
+
1111
+ it "should add to the association from the right" do
1112
+ create_item_pair "0010"
1113
+ @ri.save; @li.save
1114
+ @ri.should_not be_new_record
1115
+ @li.should_not be_new_record
1116
+
1117
+ @ri.left_items << @li
1118
+ @ri.left_items.should include(@li)
1119
+ @li.reload
1120
+ @ri.reload
1121
+ @ri.left_items.should include(@li)
1122
+ end
1123
+
1124
+ it "should load the assocaited collection from the either side" do
1125
+ pending "Waiting on Many To Many to be implemented"
1126
+ create_item_pair "0020"
1127
+ @ri.save; @li.save
1128
+ @ri.left_items << @li
1129
+ @ri.reload; @li.reload
1130
+
1131
+ @ri.left_items.should include(@li)
1132
+ @li.right_items.should include(@ri)
1133
+ end
1134
+
1135
+ it "should load the assocatied collection from the right" do
1136
+ pending "Waiting on Many To Many to be implemented"
1137
+ create_item_pair "0030"
1138
+ @ri.save; @li.save
1139
+ @li.right_items << @li
1140
+ @ri.reload; @li.reload
1141
+
1142
+ @ri.left_items.should include(@li)
1143
+ @li.right_items.should include(@ri)
1144
+
1145
+ end
1146
+
1147
+ it "should save the left side of the association if new record" do
1148
+ pending "Waiting on Many To Many to be implemented"
1149
+ create_item_pair "0040"
1150
+ @ri.save
1151
+ @li.should be_new_record
1152
+ @ri.left_items << @li
1153
+ @li.should_not be_new_record
1154
+ end
1155
+
1156
+ it "should save the right side of the assocaition if new record" do
1157
+ pending "Waiting on Many To Many to be implemented"
1158
+ create_item_pair "0050"
1159
+ @li.save
1160
+ @ri.should be_new_record
1161
+ @li.right_items << @ri
1162
+ @ri.should_not be_new_record
1163
+ end
1164
+
1165
+ it "should save both side of the assocaition if new record" do
1166
+ pending "Waiting on Many To Many to be implemented"
1167
+ create_item_pair "0060"
1168
+ @li.should be_new_record
1169
+ @ri.should be_new_record
1170
+ @ri.left_items << @li
1171
+ @ri.should_not be_new_record
1172
+ @li.should_not be_new_record
1173
+ end
1174
+
1175
+ it "should remove an item from the left collection without destroying the item" do
1176
+ pending "Waiting on Many To Many to be implemented"
1177
+ create_item_pair "0070"
1178
+ @li.save; @ri.save
1179
+ @ri.left_items << @li
1180
+ @ri.reload; @li.reload
1181
+ @ri.left_items.should include(@li)
1182
+ @ri.left_items.delete(@li)
1183
+ @ri.left_items.should_not include(@li)
1184
+ @li.reload
1185
+ LeftItem.get(@li.id).should_not be_nil
1186
+ end
1187
+
1188
+ it "should remove an item from the right collection without destroying the item" do
1189
+ pending "Waiting on Many To Many to be implemented"
1190
+ create_item_pair "0080"
1191
+ @li.save; @ri.save
1192
+ @li.right_items << @ri
1193
+ @li.reload; @ri.reload
1194
+ @li.right_items.should include(@ri)
1195
+ @li.right_items.delete(@ri)
1196
+ @li.right_items.should_not include(@ri)
1197
+ @ri.reload
1198
+ RightItem.get(@ri.id).should_not be_nil
1199
+ end
1200
+
1201
+ it "should remove the item from the collection when an item is deleted" do
1202
+ pending "Waiting on Many To Many to be implemented"
1203
+ create_item_pair "0090"
1204
+ @li.save; @ri.save
1205
+ @ri.left_items << @li
1206
+ @ri.reload; @li.reload
1207
+ @ri.left_items.should include(@li)
1208
+ @li.destroy
1209
+ @ri.reload
1210
+ @ri.left_items.should_not include(@li)
1211
+ end
1212
+ end
1213
+ end
1214
+ end
1215
+ end