sequel 3.5.0 → 3.6.0

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 (72) hide show
  1. data/CHANGELOG +108 -0
  2. data/README.rdoc +25 -14
  3. data/Rakefile +20 -1
  4. data/doc/advanced_associations.rdoc +61 -64
  5. data/doc/cheat_sheet.rdoc +16 -7
  6. data/doc/opening_databases.rdoc +3 -3
  7. data/doc/prepared_statements.rdoc +1 -1
  8. data/doc/reflection.rdoc +2 -1
  9. data/doc/release_notes/3.6.0.txt +366 -0
  10. data/doc/schema.rdoc +19 -14
  11. data/lib/sequel/adapters/amalgalite.rb +5 -27
  12. data/lib/sequel/adapters/jdbc.rb +13 -3
  13. data/lib/sequel/adapters/jdbc/h2.rb +17 -0
  14. data/lib/sequel/adapters/jdbc/mysql.rb +20 -7
  15. data/lib/sequel/adapters/mysql.rb +4 -3
  16. data/lib/sequel/adapters/oracle.rb +1 -1
  17. data/lib/sequel/adapters/postgres.rb +87 -28
  18. data/lib/sequel/adapters/shared/mssql.rb +47 -6
  19. data/lib/sequel/adapters/shared/mysql.rb +12 -31
  20. data/lib/sequel/adapters/shared/postgres.rb +15 -12
  21. data/lib/sequel/adapters/shared/sqlite.rb +18 -0
  22. data/lib/sequel/adapters/sqlite.rb +1 -16
  23. data/lib/sequel/connection_pool.rb +1 -1
  24. data/lib/sequel/core.rb +1 -1
  25. data/lib/sequel/database.rb +1 -1
  26. data/lib/sequel/database/schema_generator.rb +2 -0
  27. data/lib/sequel/database/schema_sql.rb +1 -1
  28. data/lib/sequel/dataset.rb +5 -179
  29. data/lib/sequel/dataset/actions.rb +123 -0
  30. data/lib/sequel/dataset/convenience.rb +18 -10
  31. data/lib/sequel/dataset/features.rb +65 -0
  32. data/lib/sequel/dataset/prepared_statements.rb +29 -23
  33. data/lib/sequel/dataset/query.rb +429 -0
  34. data/lib/sequel/dataset/sql.rb +67 -435
  35. data/lib/sequel/model/associations.rb +77 -13
  36. data/lib/sequel/model/base.rb +30 -8
  37. data/lib/sequel/model/errors.rb +4 -4
  38. data/lib/sequel/plugins/caching.rb +38 -15
  39. data/lib/sequel/plugins/force_encoding.rb +18 -7
  40. data/lib/sequel/plugins/hook_class_methods.rb +4 -0
  41. data/lib/sequel/plugins/many_through_many.rb +1 -1
  42. data/lib/sequel/plugins/nested_attributes.rb +40 -11
  43. data/lib/sequel/plugins/serialization.rb +17 -3
  44. data/lib/sequel/plugins/validation_helpers.rb +65 -18
  45. data/lib/sequel/sql.rb +23 -1
  46. data/lib/sequel/version.rb +1 -1
  47. data/spec/adapters/mssql_spec.rb +96 -10
  48. data/spec/adapters/mysql_spec.rb +19 -0
  49. data/spec/adapters/postgres_spec.rb +65 -2
  50. data/spec/adapters/sqlite_spec.rb +10 -0
  51. data/spec/core/core_sql_spec.rb +9 -0
  52. data/spec/core/database_spec.rb +8 -4
  53. data/spec/core/dataset_spec.rb +122 -29
  54. data/spec/core/expression_filters_spec.rb +17 -0
  55. data/spec/extensions/caching_spec.rb +43 -3
  56. data/spec/extensions/force_encoding_spec.rb +43 -1
  57. data/spec/extensions/nested_attributes_spec.rb +55 -2
  58. data/spec/extensions/validation_helpers_spec.rb +71 -0
  59. data/spec/integration/associations_test.rb +281 -0
  60. data/spec/integration/dataset_test.rb +383 -9
  61. data/spec/integration/eager_loader_test.rb +0 -65
  62. data/spec/integration/model_test.rb +110 -0
  63. data/spec/integration/plugin_test.rb +306 -0
  64. data/spec/integration/prepared_statement_test.rb +32 -0
  65. data/spec/integration/schema_test.rb +8 -3
  66. data/spec/integration/spec_helper.rb +1 -25
  67. data/spec/model/association_reflection_spec.rb +38 -0
  68. data/spec/model/associations_spec.rb +184 -8
  69. data/spec/model/eager_loading_spec.rb +23 -0
  70. data/spec/model/model_spec.rb +8 -0
  71. data/spec/model/record_spec.rb +84 -1
  72. metadata +9 -2
@@ -12,6 +12,14 @@ describe Sequel::Model, "caching" do
12
12
  cache = @cache_class.new
13
13
  @cache = cache
14
14
 
15
+ @memcached_class = Class.new(Hash) do
16
+ attr_accessor :ttl
17
+ def set(k, v, ttl); self[k] = v; @ttl = ttl; end
18
+ def get(k); if self[k] then return self[k]; else raise ArgumentError; end end
19
+ end
20
+ cache2 = @memcached_class.new
21
+ @memcached = cache2
22
+
15
23
  @c = Class.new(Sequel::Model(:items))
16
24
  @c.class_eval do
17
25
  plugin :caching, cache
@@ -19,9 +27,26 @@ describe Sequel::Model, "caching" do
19
27
 
20
28
  columns :name, :id
21
29
  end
22
-
30
+
31
+ @c3 = Class.new(Sequel::Model(:items))
32
+ @c3.class_eval do
33
+ plugin :caching, cache2
34
+ def self.name; 'Item' end
35
+
36
+ columns :name, :id
37
+ end
38
+
39
+ @c4 = Class.new(Sequel::Model(:items))
40
+ @c4.class_eval do
41
+ plugin :caching, cache2, :ignore_exceptions => true
42
+ def self.name; 'Item' end
43
+
44
+ columns :name, :id
45
+ end
46
+
47
+
23
48
  $cache_dataset_row = {:name => 'sharon', :id => 1}
24
- @dataset = @c.dataset
49
+ @dataset = @c.dataset = @c3.dataset = @c4.dataset
25
50
  $sqls = []
26
51
  @dataset.extend(Module.new {
27
52
  def fetch_rows(sql)
@@ -38,9 +63,10 @@ describe Sequel::Model, "caching" do
38
63
  $sqls << delete_sql
39
64
  end
40
65
  })
66
+
41
67
  @c2 = Class.new(@c) do
42
68
  def self.name; 'SubItem' end
43
- end
69
+ end
44
70
  end
45
71
 
46
72
  it "should set the model's cache store" do
@@ -205,4 +231,18 @@ describe Sequel::Model, "caching" do
205
231
  "SELECT * FROM items WHERE (id = 1) LIMIT 1", \
206
232
  "SELECT * FROM items WHERE (id = 4) LIMIT 1"]
207
233
  end
234
+
235
+ it "should support ignore_exception option" do
236
+ c = Class.new(Sequel::Model(:items))
237
+ c.plugin :caching, @cache, :ignore_exceptions => true
238
+ Class.new(c).cache_ignore_exceptions.should == true
239
+ end
240
+
241
+ it "should raise an exception if cache_store is memcached and ignore_exception is not enabled" do
242
+ proc{@c3[1]}.should raise_error
243
+ end
244
+
245
+ it "should rescue an exception if cache_store is memcached and ignore_exception is enabled" do
246
+ @c4[1].values.should == $cache_dataset_row
247
+ end
208
248
  end
@@ -71,5 +71,47 @@ describe "force_encoding plugin" do
71
71
  o.x.should == ''
72
72
  o.x.encoding.should == Encoding.find('UTF-16LE')
73
73
  end
74
+
75
+ specify "should work when saving new model instances" do
76
+ o = @c.new
77
+ @c.dataset = ds = MODEL_DB[:a]
78
+ def ds.first
79
+ s = 'blah'
80
+ s.force_encoding('US-ASCII')
81
+ {:id=>1, :x=>s}
82
+ end
83
+ o.save
84
+ o.x.should == 'blah'
85
+ o.x.encoding.should == @e1
86
+ end
87
+
88
+ specify "should work when refreshing model instances" do
89
+ o = @c.load(:id=>1, :x=>'as')
90
+ @c.dataset = ds = MODEL_DB[:a]
91
+ def ds.first
92
+ s = 'blah'
93
+ s.force_encoding('US-ASCII')
94
+ {:id=>1, :x=>s}
95
+ end
96
+ o.refresh
97
+ o.x.should == 'blah'
98
+ o.x.encoding.should == @e1
99
+ end
100
+
101
+ specify "should work when used with the identity_map plugin if the identity_map plugin is setup first" do
102
+ @c = Class.new(Sequel::Model) do
103
+ end
104
+ @c.columns :id, :x
105
+ @c.plugin :identity_map
106
+ @c.plugin :force_encoding, 'UTF-8'
107
+ @c.with_identity_map do
108
+ o = @c.load(:id=>1)
109
+ s = 'blah'
110
+ s.force_encoding('US-ASCII')
111
+ @c.load(:id=>1, :x=>s)
112
+ o.x.should == 'blah'
113
+ o.x.encoding.should == @e1
114
+ end
115
+ end
74
116
  end
75
- end
117
+ end
@@ -7,6 +7,7 @@ describe "NestedAttributes plugin" do
7
7
  gi = lambda{i}
8
8
  ii = lambda{i+=1}
9
9
  ds_mod = Module.new do
10
+ def empty?; false; end
10
11
  define_method(:insert) do |h|
11
12
  x = ii.call
12
13
  mods << [:i, first_source, h, x]
@@ -71,6 +72,12 @@ describe "NestedAttributes plugin" do
71
72
  @mods.should == [[:is, :albums, {:name=>"Al"}, 1], [:is, :tags, {:name=>"T"}, 2], [:i, :at, {:album_id=>1, :tag_id=>2}, 3]]
72
73
  end
73
74
 
75
+ it "should add new objects to the cached association array as soon as the *_attributes= method is called" do
76
+ a = @Artist.new({:name=>'Ar', :albums_attributes=>[{:name=>'Al', :tags_attributes=>[{:name=>'T'}]}]})
77
+ a.albums.should == [@Album.new(:name=>'Al')]
78
+ a.albums.first.tags.should == [@Tag.new(:name=>'T')]
79
+ end
80
+
74
81
  it "should support updating many_to_one objects" do
75
82
  al = @Album.load(:id=>10, :name=>'Al')
76
83
  ar = @Artist.load(:id=>20, :name=>'Ar')
@@ -294,7 +301,53 @@ describe "NestedAttributes plugin" do
294
301
  @mods.should == [[:is, :albums, {:name=>"Al"}, 1], [:is, :tags, {:name=>"T"}, 2], [:i, :at, {:album_id=>1, :tag_id=>2}, 3], [:is, :tags, {:name=>"T2"}, 4], [:i, :at, {:album_id=>1, :tag_id=>4}, 5]]
295
302
  end
296
303
 
297
- it "should deal properly with accessing the same nested attribute associated object with conflicting actions" do
298
-
304
+ it "should return objects created/modified in the internal methods" do
305
+ @Album.nested_attributes :tags, :remove=>true, :strict=>false
306
+ objs = []
307
+ @Album.class_eval do
308
+ define_method(:nested_attributes_create){|*a| objs << [super(*a), :create]}
309
+ define_method(:nested_attributes_remove){|*a| objs << [super(*a), :remove]}
310
+ define_method(:nested_attributes_update){|*a| objs << [super(*a), :update]}
311
+ end
312
+ a = @Album.new(:name=>'Al')
313
+ a.associations[:tags] = [@Tag.load(:id=>6, :name=>'A'), @Tag.load(:id=>7, :name=>'A2')]
314
+ a.tags_attributes = [{:id=>6, :name=>'T'}, {:id=>7, :name=>'T2', :_remove=>true}, {:name=>'T3'}, {:id=>8, :name=>'T4'}, {:id=>9, :name=>'T5', :_remove=>true}]
315
+ objs.should == [[@Tag.load(:id=>6, :name=>'T'), :update], [@Tag.load(:id=>7, :name=>'A2'), :remove], [@Tag.new(:name=>'T3'), :create], [nil, :update], [nil, :remove]]
316
+ end
317
+
318
+ it "should raise an error if updating modifies the associated objects keys" do
319
+ @Artist.columns :id, :name, :artist_id
320
+ @Album.columns :id, :name, :artist_id
321
+ @Tag.columns :id, :name, :tag_id
322
+ @Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id, :primary_key=>:artist_id
323
+ @Album.many_to_one :artist, :class=>@Artist, :primary_key=>:artist_id
324
+ @Album.many_to_many :tags, :class=>@Tag, :left_key=>:album_id, :right_key=>:tag_id, :join_table=>:at, :right_primary_key=>:tag_id
325
+ @Artist.nested_attributes :albums, :destroy=>true, :remove=>true
326
+ @Album.nested_attributes :artist, :tags, :destroy=>true, :remove=>true
327
+
328
+ al = @Album.load(:id=>10, :name=>'Al', :artist_id=>25)
329
+ ar = @Artist.load(:id=>20, :name=>'Ar', :artist_id=>25)
330
+ t = @Tag.load(:id=>30, :name=>'T', :tag_id=>15)
331
+ al.associations[:artist] = ar
332
+ al.associations[:tags] = [t]
333
+ ar.associations[:albums] = [al]
334
+ proc{ar.set(:albums_attributes=>[{:id=>10, :name=>'Al2', :artist_id=>'3'}])}.should raise_error(Sequel::Error)
335
+ proc{al.set(:artist_attributes=>{:id=>20, :name=>'Ar2', :artist_id=>'3'})}.should raise_error(Sequel::Error)
336
+ proc{al.set(:tags_attributes=>[{:id=>30, :name=>'T2', :tag_id=>'3'}])}.should raise_error(Sequel::Error)
337
+ end
338
+
339
+ it "should accept a :fields option and only allow modification of those fields" do
340
+ @Tag.columns :id, :name, :number
341
+ @Album.nested_attributes :tags, :destroy=>true, :remove=>true, :fields=>[:name]
342
+
343
+ al = @Album.load(:id=>10, :name=>'Al')
344
+ t = @Tag.load(:id=>30, :name=>'T', :number=>10)
345
+ al.associations[:tags] = [t]
346
+ al.set(:tags_attributes=>[{:id=>30, :name=>'T2'}, {:name=>'T3'}])
347
+ @mods.should == []
348
+ al.save
349
+ @mods.should == [[:u, :albums, {:name=>'Al'}, '(id = 10)'], [:u, :tags, {:name=>'T2'}, '(id = 30)'], [:is, :tags, {:name=>"T3"}, 1], [:i, :at, {:album_id=>10, :tag_id=>1}, 2]]
350
+ proc{al.set(:tags_attributes=>[{:id=>30, :name=>'T2', :number=>3}])}.should raise_error(Sequel::Error)
351
+ proc{al.set(:tags_attributes=>[{:name=>'T2', :number=>3}])}.should raise_error(Sequel::Error)
299
352
  end
300
353
  end
@@ -55,6 +55,13 @@ describe "Sequel::Plugins::ValidationHelpers" do
55
55
  @m.value = '1_1'
56
56
  @m.should be_valid
57
57
  end
58
+
59
+ specify "should allow a proc for the :message option" do
60
+ @c.set_validations{validates_format(/.+_.+/, :value, :message=>proc{|f| "doesn't match #{f.inspect}"})}
61
+ @m.value = 'abc_'
62
+ @m.should_not be_valid
63
+ @m.errors.should == {:value=>["doesn't match /.+_.+/"]}
64
+ end
58
65
 
59
66
  specify "should take multiple attributes in the same call" do
60
67
  @c.columns :value, :value2
@@ -65,6 +72,70 @@ describe "Sequel::Plugins::ValidationHelpers" do
65
72
  @m.value2 = 1
66
73
  @m.should be_valid
67
74
  end
75
+
76
+ specify "should support modifying default options for all models" do
77
+ @c.set_validations{validates_presence(:value)}
78
+ @m.should_not be_valid
79
+ @m.errors.should == {:value=>['is not present']}
80
+ o = Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS[:presence].dup
81
+ Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS[:presence][:message] = lambda{"was not entered"}
82
+ @m.should_not be_valid
83
+ @m.errors.should == {:value=>["was not entered"]}
84
+ @m.value = 1
85
+ @m.should be_valid
86
+
87
+ @m.values.clear
88
+ Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS[:presence][:allow_missing] = true
89
+ @m.should be_valid
90
+ @m.value = nil
91
+ @m.should_not be_valid
92
+ @m.errors.should == {:value=>["was not entered"]}
93
+
94
+
95
+ c = Class.new(Sequel::Model)
96
+ c.class_eval do
97
+ plugin :validation_helpers
98
+ set_columns([:value])
99
+ def validate
100
+ validates_presence(:value)
101
+ end
102
+ end
103
+ m = c.new(:value=>nil)
104
+ m.should_not be_valid
105
+ m.errors.should == {:value=>["was not entered"]}
106
+ Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS[:presence] = o
107
+ end
108
+
109
+ specify "should support modifying default validation options for a particular model" do
110
+ @c.set_validations{validates_presence(:value)}
111
+ @m.should_not be_valid
112
+ @m.errors.should == {:value=>['is not present']}
113
+ @c.class_eval do
114
+ def default_validation_helpers_options(type)
115
+ {:allow_missing=>true, :message=>proc{'was not entered'}}
116
+ end
117
+ end
118
+ @m.value = nil
119
+ @m.should_not be_valid
120
+ @m.errors.should == {:value=>["was not entered"]}
121
+ @m.value = 1
122
+ @m.should be_valid
123
+
124
+ @m.values.clear
125
+ @m.should be_valid
126
+
127
+ c = Class.new(Sequel::Model)
128
+ c.class_eval do
129
+ plugin :validation_helpers
130
+ attr_accessor :value
131
+ def validate
132
+ validates_presence(:value)
133
+ end
134
+ end
135
+ m = c.new
136
+ m.should_not be_valid
137
+ m.errors.should == {:value=>['is not present']}
138
+ end
68
139
 
69
140
  specify "should support validates_exact_length" do
70
141
  @c.set_validations{validates_exact_length(3, :value)}
@@ -0,0 +1,281 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ shared_examples_for "regular and composite key associations" do
4
+ specify "should return no objects if none are associated" do
5
+ @album.artist.should == nil
6
+ @artist.albums.should == []
7
+ @album.tags.should == []
8
+ @tag.albums.should == []
9
+ end
10
+
11
+ specify "should have add and set methods work any associated objects" do
12
+ @album.update(:artist => @artist)
13
+ @album.add_tag(@tag)
14
+
15
+ @album.reload
16
+ @artist.reload
17
+ @tag.reload
18
+
19
+ @album.artist.should == @artist
20
+ @artist.albums.should == [@album]
21
+ @album.tags.should == [@tag]
22
+ @tag.albums.should == [@album]
23
+ end
24
+
25
+ specify "should have remove methods work" do
26
+ @album.update(:artist => @artist)
27
+ @album.add_tag(@tag)
28
+
29
+ @album.update(:artist => nil)
30
+ @album.remove_tag(@tag)
31
+
32
+ @album.reload
33
+ @artist.reload
34
+ @tag.reload
35
+
36
+ @album.artist.should == nil
37
+ @artist.albums.should == []
38
+ @album.tags.should == []
39
+ @tag.albums.should == []
40
+ end
41
+
42
+ specify "should have remove_all methods work" do
43
+ @artist.add_album(@album)
44
+ @album.add_tag(@tag)
45
+
46
+ @artist.remove_all_albums
47
+ @album.remove_all_tags
48
+
49
+ @album.reload
50
+ @artist.reload
51
+ @tag.reload
52
+
53
+ @album.artist.should == nil
54
+ @artist.albums.should == []
55
+ @album.tags.should == []
56
+ @tag.albums.should == []
57
+ end
58
+
59
+ specify "should eager load via eager correctly" do
60
+ @album.update(:artist => @artist)
61
+ @album.add_tag(@tag)
62
+
63
+ a = Artist.eager(:albums=>:tags).all
64
+ a.should == [@artist]
65
+ a.first.albums.should == [@album]
66
+ a.first.albums.first.tags.should == [@tag]
67
+
68
+ a = Tag.eager(:albums=>:artist).all
69
+ a.should == [@tag]
70
+ a.first.albums.should == [@album]
71
+ a.first.albums.first.artist.should == @artist
72
+ end
73
+
74
+ specify "should eager load via eager_graph correctly" do
75
+ @album.update(:artist => @artist)
76
+ @album.add_tag(@tag)
77
+
78
+ a = Artist.eager_graph(:albums=>:tags).all
79
+ a.should == [@artist]
80
+ a.first.albums.should == [@album]
81
+ a.first.albums.first.tags.should == [@tag]
82
+
83
+ a = Tag.eager_graph(:albums=>:artist).all
84
+ a.should == [@tag]
85
+ a.first.albums.should == [@album]
86
+ a.first.albums.first.artist.should == @artist
87
+ end
88
+
89
+ specify "should work with a many_through_many association" do
90
+ @album.update(:artist => @artist)
91
+ @album.add_tag(@tag)
92
+
93
+ @album.reload
94
+ @artist.reload
95
+ @tag.reload
96
+
97
+ @album.tags.should == [@tag]
98
+
99
+ a = Artist.eager(:tags).all
100
+ a.should == [@artist]
101
+ a.first.tags.should == [@tag]
102
+
103
+ a = Artist.eager_graph(:tags).all
104
+ a.should == [@artist]
105
+ a.first.tags.should == [@tag]
106
+
107
+ a = Album.eager(:artist=>:tags).all
108
+ a.should == [@album]
109
+ a.first.artist.should == @artist
110
+ a.first.artist.tags.should == [@tag]
111
+
112
+ a = Album.eager_graph(:artist=>:tags).all
113
+ a.should == [@album]
114
+ a.first.artist.should == @artist
115
+ a.first.artist.tags.should == [@tag]
116
+ end
117
+ end
118
+
119
+ describe "Sequel::Model Simple Associations" do
120
+ before do
121
+ @db = INTEGRATION_DB
122
+ @db.create_table!(:artists) do
123
+ primary_key :id
124
+ String :name
125
+ end
126
+ @db.create_table!(:albums) do
127
+ primary_key :id
128
+ String :name
129
+ foreign_key :artist_id, :artists
130
+ end
131
+ @db.create_table!(:tags) do
132
+ primary_key :id
133
+ String :name
134
+ end
135
+ @db.create_table!(:albums_tags) do
136
+ foreign_key :album_id, :albums
137
+ foreign_key :tag_id, :tags
138
+ end
139
+ class ::Artist < Sequel::Model(@db)
140
+ one_to_many :albums
141
+ plugin :many_through_many
142
+ Artist.many_through_many :tags, [[:albums, :artist_id, :id], [:albums_tags, :album_id, :tag_id]]
143
+ end
144
+ class ::Album < Sequel::Model(@db)
145
+ many_to_one :artist
146
+ many_to_many :tags
147
+ end
148
+ class ::Tag < Sequel::Model(@db)
149
+ many_to_many :albums
150
+ end
151
+ @album = Album.create(:name=>'Al')
152
+ @artist = Artist.create(:name=>'Ar')
153
+ @tag = Tag.create(:name=>'T')
154
+ end
155
+ after do
156
+ @db.drop_table(:albums_tags, :tags, :albums, :artists)
157
+ [:Tag, :Album, :Artist].each{|x| Object.send(:remove_const, x)}
158
+ end
159
+
160
+ it_should_behave_like "regular and composite key associations"
161
+
162
+ specify "should have add method accept hashes and create new records" do
163
+ @artist.remove_all_albums
164
+ Album.delete
165
+ @album = @artist.add_album(:name=>'Al2')
166
+ Album.first[:name].should == 'Al2'
167
+ @artist.albums_dataset.first[:name].should == 'Al2'
168
+
169
+ @album.remove_all_tags
170
+ Tag.delete
171
+ @album.add_tag(:name=>'T2')
172
+ Tag.first[:name].should == 'T2'
173
+ @album.tags_dataset.first[:name].should == 'T2'
174
+ end
175
+
176
+ specify "should have remove method accept primary key and remove related album" do
177
+ @artist.add_album(@album)
178
+ @artist.reload.remove_album(@album.id)
179
+ @artist.reload.albums.should == []
180
+
181
+ @album.add_tag(@tag)
182
+ @album.reload.remove_tag(@tag.id)
183
+ @tag.reload.albums.should == []
184
+ end
185
+
186
+ specify "should have remove method raise an error for one_to_many records if the object isn't already associated" do
187
+ proc{@artist.remove_album(@album.id)}.should raise_error(Sequel::Error)
188
+ proc{@artist.remove_album(@album)}.should raise_error(Sequel::Error)
189
+ end
190
+ end
191
+
192
+ describe "Sequel::Model Composite Key Associations" do
193
+ before do
194
+ @db = INTEGRATION_DB
195
+ @db.create_table!(:artists) do
196
+ Integer :id1
197
+ Integer :id2
198
+ String :name
199
+ primary_key [:id1, :id2]
200
+ end
201
+ @db.create_table!(:albums) do
202
+ Integer :id1
203
+ Integer :id2
204
+ String :name
205
+ Integer :artist_id1
206
+ Integer :artist_id2
207
+ foreign_key [:artist_id1, :artist_id2], :artists
208
+ primary_key [:id1, :id2]
209
+ end
210
+ @db.create_table!(:tags) do
211
+ Integer :id1
212
+ Integer :id2
213
+ String :name
214
+ primary_key [:id1, :id2]
215
+ end
216
+ @db.create_table!(:albums_tags) do
217
+ Integer :album_id1
218
+ Integer :album_id2
219
+ Integer :tag_id1
220
+ Integer :tag_id2
221
+ foreign_key [:album_id1, :album_id2], :albums
222
+ foreign_key [:tag_id1, :tag_id2], :tags
223
+ end
224
+ class ::Artist < Sequel::Model(@db)
225
+ set_primary_key :id1, :id2
226
+ unrestrict_primary_key
227
+ one_to_many :albums, :key=>[:artist_id1, :artist_id2]
228
+ plugin :many_through_many
229
+ Artist.many_through_many :tags, [[:albums, [:artist_id1, :artist_id2], [:id1, :id2]], [:albums_tags, [:album_id1, :album_id2], [:tag_id1, :tag_id2]]]
230
+ end
231
+ class ::Album < Sequel::Model(@db)
232
+ set_primary_key :id1, :id2
233
+ unrestrict_primary_key
234
+ many_to_one :artist, :key=>[:artist_id1, :artist_id2]
235
+ many_to_many :tags, :left_key=>[:album_id1, :album_id2], :right_key=>[:tag_id1, :tag_id2]
236
+ end
237
+ class ::Tag < Sequel::Model(@db)
238
+ set_primary_key :id1, :id2
239
+ unrestrict_primary_key
240
+ many_to_many :albums, :right_key=>[:album_id1, :album_id2], :left_key=>[:tag_id1, :tag_id2]
241
+ end
242
+ @album = Album.create(:name=>'Al', :id1=>1, :id2=>2)
243
+ @artist = Artist.create(:name=>'Ar', :id1=>3, :id2=>4)
244
+ @tag = Tag.create(:name=>'T', :id1=>5, :id2=>6)
245
+ end
246
+ after do
247
+ @db.drop_table(:albums_tags, :tags, :albums, :artists)
248
+ [:Tag, :Album, :Artist].each{|x| Object.send(:remove_const, x)}
249
+ end
250
+
251
+ it_should_behave_like "regular and composite key associations"
252
+
253
+ specify "should have add method accept hashes and create new records" do
254
+ @artist.remove_all_albums
255
+ Album.delete
256
+ @artist.add_album(:id1=>1, :id2=>2, :name=>'Al2')
257
+ Album.first[:name].should == 'Al2'
258
+ @artist.albums_dataset.first[:name].should == 'Al2'
259
+
260
+ @album.remove_all_tags
261
+ Tag.delete
262
+ @album.add_tag(:id1=>1, :id2=>2, :name=>'T2')
263
+ Tag.first[:name].should == 'T2'
264
+ @album.tags_dataset.first[:name].should == 'T2'
265
+ end
266
+
267
+ specify "should have remove method accept primary key and remove related album" do
268
+ @artist.add_album(@album)
269
+ @artist.reload.remove_album([@album.id1, @album.id2])
270
+ @artist.reload.albums.should == []
271
+
272
+ @album.add_tag(@tag)
273
+ @album.reload.remove_tag([@tag.id1, @tag.id2])
274
+ @tag.reload.albums.should == []
275
+ end
276
+
277
+ specify "should have remove method raise an error for one_to_many records if the object isn't already associated" do
278
+ proc{@artist.remove_album([@album.id1, @album.id2])}.should raise_error(Sequel::Error)
279
+ proc{@artist.remove_album(@album)}.should raise_error(Sequel::Error)
280
+ end
281
+ end