dm-core 0.9.5 → 0.9.6

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 (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