dm-core 0.9.5 → 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/Manifest.txt +3 -0
  2. data/lib/dm-core.rb +14 -20
  3. data/lib/dm-core/adapters.rb +18 -0
  4. data/lib/dm-core/adapters/abstract_adapter.rb +17 -10
  5. data/lib/dm-core/adapters/data_objects_adapter.rb +17 -22
  6. data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
  7. data/lib/dm-core/adapters/mysql_adapter.rb +1 -1
  8. data/lib/dm-core/adapters/postgres_adapter.rb +2 -2
  9. data/lib/dm-core/adapters/sqlite3_adapter.rb +1 -1
  10. data/lib/dm-core/associations.rb +3 -2
  11. data/lib/dm-core/associations/many_to_many.rb +3 -3
  12. data/lib/dm-core/associations/one_to_many.rb +10 -2
  13. data/lib/dm-core/associations/relationship.rb +20 -16
  14. data/lib/dm-core/auto_migrations.rb +5 -4
  15. data/lib/dm-core/collection.rb +10 -6
  16. data/lib/dm-core/dependency_queue.rb +2 -1
  17. data/lib/dm-core/identity_map.rb +3 -6
  18. data/lib/dm-core/model.rb +48 -27
  19. data/lib/dm-core/property.rb +57 -37
  20. data/lib/dm-core/property_set.rb +29 -22
  21. data/lib/dm-core/query.rb +57 -49
  22. data/lib/dm-core/repository.rb +3 -3
  23. data/lib/dm-core/resource.rb +17 -15
  24. data/lib/dm-core/scope.rb +7 -7
  25. data/lib/dm-core/support/kernel.rb +6 -2
  26. data/lib/dm-core/transaction.rb +7 -7
  27. data/lib/dm-core/version.rb +1 -1
  28. data/script/performance.rb +114 -22
  29. data/spec/integration/association_spec.rb +31 -2
  30. data/spec/integration/association_through_spec.rb +2 -0
  31. data/spec/integration/associations/many_to_many_spec.rb +152 -0
  32. data/spec/integration/associations/one_to_many_spec.rb +40 -3
  33. data/spec/integration/dependency_queue_spec.rb +0 -12
  34. data/spec/integration/postgres_adapter_spec.rb +1 -1
  35. data/spec/integration/property_spec.rb +3 -3
  36. data/spec/integration/query_spec.rb +39 -8
  37. data/spec/integration/resource_spec.rb +10 -6
  38. data/spec/integration/sti_spec.rb +22 -0
  39. data/spec/integration/strategic_eager_loading_spec.rb +21 -6
  40. data/spec/integration/type_spec.rb +1 -0
  41. data/spec/lib/model_loader.rb +10 -1
  42. data/spec/models/content.rb +16 -0
  43. data/spec/spec_helper.rb +4 -1
  44. data/spec/unit/adapters/data_objects_adapter_spec.rb +11 -11
  45. data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
  46. data/spec/unit/associations/many_to_many_spec.rb +16 -1
  47. data/spec/unit/model_spec.rb +0 -16
  48. data/spec/unit/property_set_spec.rb +8 -1
  49. data/spec/unit/property_spec.rb +476 -240
  50. data/spec/unit/query_spec.rb +41 -0
  51. data/spec/unit/resource_spec.rb +75 -56
  52. data/tasks/ci.rb +4 -36
  53. data/tasks/dm.rb +3 -3
  54. metadata +5 -2
@@ -13,12 +13,12 @@ module DataMapper
13
13
  # In fact, it just calls #link with the given arguments at the end of the
14
14
  # constructor.
15
15
  #
16
- def initialize(*things, &block)
16
+ def initialize(*things)
17
17
  @transaction_primitives = {}
18
18
  @state = :none
19
19
  @adapters = {}
20
20
  link(*things)
21
- commit(&block) if block_given?
21
+ commit { |*block_args| yield(*block_args) } if block_given?
22
22
  end
23
23
 
24
24
  #
@@ -39,7 +39,7 @@ module DataMapper
39
39
  # within this transaction. The transaction will begin and commit around
40
40
  # the block, and rollback if an exception is raised.
41
41
  #
42
- def link(*things, &block)
42
+ def link(*things)
43
43
  raise "Illegal state for link: #{@state}" unless @state == :none
44
44
  things.each do |thing|
45
45
  if thing.is_a?(Array)
@@ -56,7 +56,7 @@ module DataMapper
56
56
  raise "Unknown argument to #{self}#link: #{thing.inspect}"
57
57
  end
58
58
  end
59
- return commit(&block) if block_given?
59
+ return commit { |*block_args| yield(*block_args) } if block_given?
60
60
  return self
61
61
  end
62
62
 
@@ -83,12 +83,12 @@ module DataMapper
83
83
  # If no block is given, it will simply commit any changes made since the
84
84
  # Transaction did #begin.
85
85
  #
86
- def commit(&block)
86
+ def commit
87
87
  if block_given?
88
88
  raise "Illegal state for commit with block: #{@state}" unless @state == :none
89
89
  begin
90
90
  self.begin
91
- rval = within(&block)
91
+ rval = within { |*block_args| yield(*block_args) }
92
92
  self.commit if @state == :begin
93
93
  return rval
94
94
  rescue Exception => e
@@ -128,7 +128,7 @@ module DataMapper
128
128
  # adapter it is associated with, and it will ensures that it will pop the
129
129
  # Transaction away again after the block is finished.
130
130
  #
131
- def within(&block)
131
+ def within
132
132
  raise "No block provided" unless block_given?
133
133
  raise "Illegal state for within: #{@state}" unless @state == :begin
134
134
  @adapters.each do |adapter, state|
@@ -1,3 +1,3 @@
1
1
  module DataMapper
2
- VERSION = '0.9.5' unless defined?(DataMapper::VERSION)
2
+ VERSION = '0.9.6' unless defined?(DataMapper::VERSION)
3
3
  end
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require File.join(File.dirname(__FILE__), '..', 'lib', 'dm-core')
4
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'dm-core', 'version')
4
5
 
5
6
  require 'rubygems'
6
7
  require 'ftools'
@@ -38,11 +39,11 @@ configuration_options[:socket] = socket_file unless socket_file.nil?
38
39
  log_dir = DataMapper.root / 'log'
39
40
  log_dir.mkdir unless log_dir.directory?
40
41
 
41
- DataMapper::Logger.new(log_dir / 'dm.log', :debug)
42
+ DataMapper::Logger.new(log_dir / 'dm.log', :off)
42
43
  adapter = DataMapper.setup(:default, "mysql://root@localhost/data_mapper_1?socket=#{socket_file}")
43
44
 
44
45
  if configuration_options[:adapter]
45
- sqlfile = File.join(File.dirname(__FILE__),'..','tmp','perf.sql')
46
+ sqlfile = File.join(File.dirname(__FILE__),'..','tmp','performance.sql')
46
47
  mysql_bin = %w[mysql mysql5].select{|bin| `which #{bin}`.length > 0 }
47
48
  mysqldump_bin = %w[mysqldump mysqldump5].select{|bin| `which #{bin}`.length > 0 }
48
49
  end
@@ -54,6 +55,15 @@ ActiveRecord::Base.establish_connection(configuration_options)
54
55
 
55
56
  class ARExhibit < ActiveRecord::Base #:nodoc:
56
57
  set_table_name 'exhibits'
58
+
59
+ belongs_to :user, :class_name => 'ARUser', :foreign_key => 'user_id'
60
+ end
61
+
62
+ class ARUser < ActiveRecord::Base #:nodoc:
63
+ set_table_name 'users'
64
+
65
+ has_many :exhibits, :foreign_key => 'user_id'
66
+
57
67
  end
58
68
 
59
69
  ARExhibit.find_by_sql('SELECT 1')
@@ -64,17 +74,39 @@ class Exhibit
64
74
  property :id, Serial
65
75
  property :name, String
66
76
  property :zoo_id, Integer
77
+ property :user_id, Integer
67
78
  property :notes, Text, :lazy => true
68
79
  property :created_on, Date
80
+
81
+ belongs_to :user
69
82
  # property :updated_at, DateTime
70
83
  end
71
84
 
85
+ class User
86
+ include DataMapper::Resource
87
+
88
+ property :id, Serial
89
+ property :name, String
90
+ property :email, String
91
+ property :about, Text, :lazy => true
92
+ property :created_on, Date
93
+
94
+ end
95
+
72
96
  touch_attributes = lambda do |exhibits|
73
97
  [*exhibits].each do |exhibit|
74
98
  exhibit.id
75
99
  exhibit.name
76
100
  exhibit.created_on
77
- # exhibit.updated_at
101
+ end
102
+ end
103
+
104
+ touch_relationships = lambda do |exhibits|
105
+ [*exhibits].each do |exhibit|
106
+ exhibit.id
107
+ exhibit.name
108
+ exhibit.created_on
109
+ exhibit.user
78
110
  end
79
111
  end
80
112
 
@@ -87,27 +119,49 @@ if sqlfile && File.exists?(sqlfile)
87
119
  `#{mysql_bin} -u #{c[:username]} #{"-p#{c[:password]}" unless c[:password].blank?} #{c[:database]} < #{sqlfile}`
88
120
  else
89
121
 
122
+ puts "Generating data for benchmarking..."
123
+
124
+ User.auto_migrate!
90
125
  Exhibit.auto_migrate!
91
126
 
127
+ users = []
92
128
  exhibits = []
129
+
93
130
  # pre-compute the insert statements and fake data compilation,
94
131
  # so the benchmarks below show the actual runtime for the execute
95
132
  # method, minus the setup steps
96
- 10_000.times do
133
+
134
+ # Using the same paragraph for all exhibits because it is very slow
135
+ # to generate unique paragraphs for all exhibits.
136
+ paragraph = Faker::Lorem.paragraphs.join($/)
137
+
138
+ 10_000.times do |i|
139
+ users << [
140
+ 'INSERT INTO `users` (`name`,`email`,`created_on`) VALUES (?, ?, ?)',
141
+ Faker::Name.name,
142
+ Faker::Internet.email,
143
+ Date.today
144
+ ]
145
+
97
146
  exhibits << [
98
- 'INSERT INTO `exhibits` (`name`, `zoo_id`, `notes`, `created_on`) VALUES (?, ?, ?, ?)',
147
+ 'INSERT INTO `exhibits` (`name`, `zoo_id`, `user_id`, `notes`, `created_on`) VALUES (?, ?, ?, ?, ?)',
99
148
  Faker::Company.name,
100
149
  rand(10).ceil,
101
- Faker::Lorem.paragraphs.join($/),
150
+ i,
151
+ paragraph,#Faker::Lorem.paragraphs.join($/),
102
152
  Date.today
103
153
  ]
104
154
  end
155
+
156
+ puts "Inserting 10,000 users..."
157
+ 10_000.times { |i| adapter.execute(*users.at(i)) }
158
+ puts "Inserting 10,000 exhibits..."
105
159
  10_000.times { |i| adapter.execute(*exhibits.at(i)) }
106
160
 
107
161
  if sqlfile
108
162
  answer = nil
109
163
  until answer && answer[/^$|y|yes|n|no/]
110
- print("Would you like to dump data into tmp/perf.sql (for faster setup)? [Yn]");
164
+ print("Would you like to dump data into tmp/performance.sql (for faster setup)? [Yn]");
111
165
  STDOUT.flush
112
166
  answer = gets
113
167
  end
@@ -115,7 +169,7 @@ else
115
169
  if answer[/^$|y|yes/]
116
170
  File.makedirs(File.dirname(sqlfile))
117
171
  #adapter.execute("SELECT * INTO OUTFILE '#{sqlfile}' FROM exhibits;")
118
- `#{mysqldump_bin} -u #{c[:username]} #{"-p#{c[:password]}" unless c[:password].blank?} #{c[:database]} exhibits > #{sqlfile}`
172
+ `#{mysqldump_bin} -u #{c[:username]} #{"-p#{c[:password]}" unless c[:password].blank?} #{c[:database]} exhibits users > #{sqlfile}`
119
173
  puts "File saved\n"
120
174
  end
121
175
  end
@@ -127,37 +181,63 @@ TIMES = ENV['x'] ? ENV['x'].to_i : 10_000
127
181
  puts "You can specify how many times you want to run the benchmarks with rake:perf x=(number)"
128
182
  puts "Some tasks will be run 10 and 1000 times less than (number)"
129
183
  puts "Benchmarks will now run #{TIMES} times"
184
+ # Inform about slow benchmark
185
+ # answer = nil
186
+ # until answer && answer[/^$|y|yes|n|no/]
187
+ # print("A slow benchmark exposing problems with SEL is newly added. It takes approx. 20s\n");
188
+ # print("you have scheduled it to run #{TIMES / 100} times.\nWould you still include the particular benchmark? [Yn]")
189
+ # STDOUT.flush
190
+ # answer = gets
191
+ # end
192
+ # run_rel_bench = answer[/^$|y|yes/] ? true : false
193
+
130
194
 
131
195
  RBench.run(TIMES) do
132
196
 
133
197
  column :times
134
- column :dm, :title => "DM 0.9.4"
135
198
  column :ar, :title => "AR 2.1"
136
- column :diff, :compare => [:dm,:ar]
199
+ column :dm, :title => "DM #{DataMapper::VERSION}"
200
+ column :diff, :compare => [:ar,:dm]
201
+
202
+ report "Model.new (instantiation)" do
203
+ ar { ARExhibit.new }
204
+ dm { Exhibit.new }
205
+ end
206
+
207
+ report "Model.new (setting attributes)" do
208
+ attrs = {:name => 'sam', :zoo_id => 1}
209
+ ar { ARExhibit.new(attrs) }
210
+ dm { Exhibit.new(attrs) }
211
+ end
137
212
 
138
213
  report "Model.get specific (not cached)" do
139
- dm { touch_attributes[Exhibit.get(1)] }
140
214
  ActiveRecord::Base.uncached { ar { touch_attributes[ARExhibit.find(1)] } }
215
+ dm { touch_attributes[Exhibit.get(1)] }
141
216
  end
142
217
 
143
218
  report "Model.get specific (cached)" do
144
- Exhibit.repository(:default) { dm { touch_attributes[Exhibit.get(1)] } }
145
219
  ActiveRecord::Base.cache { ar { touch_attributes[ARExhibit.find(1)] } }
220
+ Exhibit.repository(:default) { dm { touch_attributes[Exhibit.get(1)] } }
146
221
  end
147
222
 
148
223
  report "Model.first" do
149
- dm { touch_attributes[Exhibit.first] }
150
224
  ar { touch_attributes[ARExhibit.first] }
225
+ dm { touch_attributes[Exhibit.first] }
151
226
  end
152
227
 
153
- report "Model.all limit(100)", TIMES / 10 do
154
- dm { touch_attributes[Exhibit.all(:limit => 100)] }
228
+ report "Model.all limit(100)", (TIMES / 10.0).ceil do
155
229
  ar { touch_attributes[ARExhibit.find(:all, :limit => 100)] }
230
+ dm { touch_attributes[Exhibit.all(:limit => 100)] }
156
231
  end
157
232
 
158
- report "Model.all limit(10,000)", TIMES / 1000 do
159
- dm { touch_attributes[Exhibit.all(:limit => 10_000)] }
233
+ report "Model.all limit(100) with relationship", (TIMES / 10.0).ceil do
234
+ ar { touch_relationships[ARExhibit.all(:limit => 100, :include => [:user])] }
235
+ dm { touch_relationships[Exhibit.all(:limit => 100)] }
236
+ end
237
+
238
+ report "Model.all limit(10,000)", (TIMES / 1000.0).ceil do
160
239
  ar { touch_attributes[ARExhibit.find(:all, :limit => 10_000)] }
240
+ dm { touch_attributes[Exhibit.all(:limit => 10_000)] }
161
241
  end
162
242
 
163
243
  create_exhibit = {
@@ -168,25 +248,37 @@ RBench.run(TIMES) do
168
248
  }
169
249
 
170
250
  report "Model.create" do
171
- dm { Exhibit.create(create_exhibit) }
172
251
  ar { ARExhibit.create(create_exhibit) }
252
+ dm { Exhibit.create(create_exhibit) }
253
+ end
254
+
255
+ report "Resource#attributes" do
256
+ attrs_first = {:name => 'sam', :zoo_id => 1}
257
+ attrs_second = {:name => 'tom', :zoo_id => 1}
258
+ ar { e = ARExhibit.new(attrs_first); e.attributes = attrs_second }
259
+ dm { e = Exhibit.new(attrs_first); e.attributes = attrs_second }
173
260
  end
174
261
 
175
262
  report "Resource#update" do
176
- dm { e = Exhibit.get(1); e.name = 'bob'; e.save }
177
- ar { e = ARExhibit.find(1); e.name = 'bob'; e.save }
263
+ ar { e = ARExhibit.find(1); e.name = 'bob'; e.save }
264
+ dm { e = Exhibit.get(1); e.name = 'bob'; e.save }
178
265
  end
179
266
 
180
267
  report "Resource#destroy" do
181
- dm { Exhibit.first.destroy }
182
268
  ar { ARExhibit.first.destroy }
269
+ dm { Exhibit.first.destroy }
183
270
  end
184
271
 
185
- summary "Total"
272
+ report "Model.transaction" do
273
+ ar { ARExhibit.transaction { ARExhibit.new } }
274
+ dm { Exhibit.transaction { Exhibit.new } }
275
+ end
186
276
 
277
+ summary "Total"
187
278
  end
188
279
 
189
280
  connection = adapter.send(:create_connection)
190
281
  command = connection.create_command("DROP TABLE exhibits")
282
+ command = connection.create_command("DROP TABLE users")
191
283
  command.execute_non_query rescue nil
192
284
  connection.close
@@ -302,6 +302,18 @@ if ADAPTER
302
302
  area.should respond_to(:machine=)
303
303
  end
304
304
 
305
+ it 'should create the foreign key property immediately' do
306
+ class Duck
307
+ include DataMapper::Resource
308
+ property :id, Serial
309
+ belongs_to :sky
310
+ end
311
+ Duck.properties.slice(:sky_id).compact.should_not be_empty
312
+ duck = Duck.new
313
+ duck.should respond_to(:sky_id)
314
+ duck.should respond_to(:sky_id=)
315
+ end
316
+
305
317
  it 'should load without the parent'
306
318
 
307
319
  it 'should allow substituting the parent' do
@@ -331,7 +343,7 @@ if ADAPTER
331
343
  end
332
344
  end
333
345
 
334
- FlightlessBirds::Ostrich.properties.slice(:sky_id).should_not be_empty
346
+ FlightlessBirds::Ostrich.properties(ADAPTER).slice(:sky_id).compact.should_not be_empty
335
347
  end
336
348
  end
337
349
 
@@ -571,6 +583,15 @@ if ADAPTER
571
583
  machine.areas.size.should == 4
572
584
  end
573
585
 
586
+ it "#build should add exactly one instance of the built record" do
587
+ machine = Machine.create(:name => 'my machine')
588
+
589
+ original_size = machine.areas.size
590
+ machine.areas.build(:name => "an area", :machine => machine)
591
+
592
+ machine.areas.size.should == original_size + 1
593
+ end
594
+
574
595
  it '#<< should add default values for relationships that have conditions' do
575
596
  # it should add default values
576
597
  machine = Machine.new(:name => 'my machine')
@@ -1169,8 +1190,16 @@ if ADAPTER
1169
1190
  #
1170
1191
 
1171
1192
  it 'should join tables in the right order during has 1 => has n => has 1 queries' do
1172
- child = Sweets::Shop.first.children(:name => 'Snotling nr 3').booger(:name.like => 'ooger')
1193
+ child = Sweets::Shop.first.children(:name => 'Snotling nr 3').booger(:name.like => 'Nasty booger')
1173
1194
  child.should_not be_nil
1195
+ child.size.should eql(1)
1196
+ child.first.name.should eql("Nasty booger")
1197
+ end
1198
+
1199
+ it 'should join tables in the right order for belongs_to relations' do
1200
+ wife = Sweets::Wife.first(Sweets::Wife.shop_owner.name => "Betsy", Sweets::Wife.shop_owner.shop.name => "Betsy's")
1201
+ wife.should_not be_nil
1202
+ wife.name.should eql("Barry")
1174
1203
  end
1175
1204
 
1176
1205
  it 'should raise exception if you try to change it' do
@@ -50,10 +50,12 @@ if ADAPTER
50
50
  has n, :relationships
51
51
  has n, :related_posts,
52
52
  :through => :relationships,
53
+ :child_key => [:post_id],
53
54
  :class_name => "Post"
54
55
 
55
56
  has n, :void_tags,
56
57
  :through => :taggings,
58
+ :child_key => [:post_id],
57
59
  :class_name => "Tag",
58
60
  :remote_relationship_name => :tag,
59
61
  Post.taggings.tag.voided => true
@@ -294,4 +294,156 @@ describe DataMapper::Associations::ManyToMany::Proxy do
294
294
  end
295
295
  end
296
296
 
297
+ describe "with renamed associations" do
298
+ before :all do
299
+ class Singer
300
+ include DataMapper::Resource
301
+
302
+ def self.default_repository_name; ADAPTER end
303
+
304
+ property :id, Serial
305
+ property :name, String
306
+
307
+ has n, :tunes, :through => Resource, :class_name => 'Song'
308
+ end
309
+
310
+ class Song
311
+ include DataMapper::Resource
312
+
313
+ def self.default_repository_name; ADAPTER end
314
+
315
+ property :id, Serial
316
+ property :title, String
317
+
318
+ has n, :performers, :through => Resource, :class_name => 'Singer'
319
+ end
320
+ end
321
+
322
+ before do
323
+ [ Singer, Song, SingerSong ].each { |k| k.auto_migrate! }
324
+
325
+ song_1 = Song.create(:title => "Dubliners")
326
+ song_2 = Song.create(:title => "Portrait of the Artist as a Young Man")
327
+ song_3 = Song.create(:title => "Ulysses")
328
+
329
+ singer_1 = Singer.create(:name => "Jon Doe")
330
+ singer_2 = Singer.create(:name => "Jane Doe")
331
+
332
+ SingerSong.create(:song => song_1, :singer => singer_1)
333
+ SingerSong.create(:song => song_2, :singer => singer_1)
334
+ SingerSong.create(:song => song_1, :singer => singer_2)
335
+
336
+ @parent = song_3
337
+ @association = @parent.performers
338
+ @other = [ singer_1 ]
339
+ end
340
+
341
+ it "should provide #replace" do
342
+ @association.should respond_to(:replace)
343
+ end
344
+
345
+ it "should correctly link records" do
346
+ Song.get(1).should have(2).performers
347
+ Song.get(2).should have(1).performers
348
+ Song.get(3).should have(0).performers
349
+ Singer.get(1).should have(2).tunes
350
+ Singer.get(2).should have(1).tunes
351
+ end
352
+
353
+ it "should be able to have associated objects manually added" do
354
+ song = Song.get(3)
355
+ song.should have(0).performers
356
+
357
+ be = SingerSong.new(:song_id => song.id, :singer_id => 2)
358
+ song.singer_songs << be
359
+ song.save
360
+
361
+ song.reload.should have(1).performers
362
+ end
363
+
364
+ it "should automatically added necessary through class" do
365
+ song = Song.get(3)
366
+ song.should have(0).performers
367
+
368
+ song.performers << Singer.get(1)
369
+ song.performers << Singer.new(:name => "Jimmy John")
370
+ song.save
371
+
372
+ song.reload.should have(2).performers
373
+ end
374
+
375
+ it "should react correctly to a new record" do
376
+ song = Song.new(:title => "Finnegan's Wake")
377
+ singer = Singer.get(2)
378
+ song.should have(0).performers
379
+ singer.should have(1).tunes
380
+
381
+ song.performers << singer
382
+ song.save
383
+
384
+ song.reload.should have(1).performers
385
+ singer.reload.should have(2).tunes
386
+ end
387
+
388
+ it "should be able to delete intermediate model" do
389
+ song = Song.get(1)
390
+ song.should have(2).singer_songs
391
+ song.should have(2).performers
392
+
393
+ be = SingerSong.get(1,1)
394
+ song.singer_songs.delete(be)
395
+ song.save
396
+
397
+ song.reload
398
+ song.should have(1).singer_songs
399
+ song.should have(1).performers
400
+ end
401
+
402
+ it "should be clearable" do
403
+ repository(ADAPTER) do
404
+ song = Song.get(2)
405
+ song.should have(1).singer_songs
406
+ song.should have(1).performers
407
+
408
+ song.performers.clear
409
+ song.save
410
+
411
+ song.reload
412
+ song.should have(0).singer_songs
413
+ song.should have(0).performers
414
+ end
415
+ repository(ADAPTER) do
416
+ Song.get(2).should have(0).performers
417
+ end
418
+ end
419
+
420
+ it "should be able to delete one object" do
421
+ song = Song.get(1)
422
+ song.should have(2).singer_songs
423
+ song.should have(2).performers
424
+
425
+ editor = song.performers.first
426
+ song.performers.delete(editor)
427
+ song.save
428
+
429
+ song.reload
430
+ song.should have(1).singer_songs
431
+ song.should have(1).performers
432
+ editor.reload.tunes.should_not include(song)
433
+ end
434
+
435
+ it "should be destroyable" do
436
+ pending "cannot destroy a collection yet" do
437
+ song = Song.get(2)
438
+ song.should have(1).performers
439
+
440
+ song.performers.destroy
441
+ song.save
442
+
443
+ song.reload
444
+ song.should have(0).performers
445
+ end
446
+ end
447
+ end
448
+
297
449
  end