dm-core 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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