sequel 3.3.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/CHANGELOG +62 -0
  2. data/README.rdoc +4 -4
  3. data/doc/release_notes/3.3.0.txt +1 -1
  4. data/doc/release_notes/3.4.0.txt +325 -0
  5. data/doc/sharding.rdoc +3 -3
  6. data/lib/sequel/adapters/amalgalite.rb +1 -1
  7. data/lib/sequel/adapters/firebird.rb +4 -9
  8. data/lib/sequel/adapters/jdbc.rb +21 -7
  9. data/lib/sequel/adapters/mysql.rb +2 -1
  10. data/lib/sequel/adapters/odbc.rb +7 -21
  11. data/lib/sequel/adapters/oracle.rb +1 -1
  12. data/lib/sequel/adapters/postgres.rb +6 -1
  13. data/lib/sequel/adapters/shared/mssql.rb +11 -0
  14. data/lib/sequel/adapters/shared/mysql.rb +8 -12
  15. data/lib/sequel/adapters/shared/oracle.rb +13 -0
  16. data/lib/sequel/adapters/shared/postgres.rb +5 -10
  17. data/lib/sequel/adapters/shared/sqlite.rb +21 -1
  18. data/lib/sequel/adapters/sqlite.rb +2 -2
  19. data/lib/sequel/core.rb +147 -11
  20. data/lib/sequel/database.rb +21 -9
  21. data/lib/sequel/dataset.rb +31 -6
  22. data/lib/sequel/dataset/convenience.rb +1 -1
  23. data/lib/sequel/dataset/sql.rb +76 -18
  24. data/lib/sequel/extensions/inflector.rb +2 -51
  25. data/lib/sequel/model.rb +16 -10
  26. data/lib/sequel/model/associations.rb +4 -1
  27. data/lib/sequel/model/base.rb +13 -6
  28. data/lib/sequel/model/default_inflections.rb +46 -0
  29. data/lib/sequel/model/inflections.rb +1 -51
  30. data/lib/sequel/plugins/boolean_readers.rb +52 -0
  31. data/lib/sequel/plugins/instance_hooks.rb +57 -0
  32. data/lib/sequel/plugins/lazy_attributes.rb +13 -1
  33. data/lib/sequel/plugins/nested_attributes.rb +171 -0
  34. data/lib/sequel/plugins/serialization.rb +35 -16
  35. data/lib/sequel/plugins/timestamps.rb +87 -0
  36. data/lib/sequel/plugins/validation_helpers.rb +8 -1
  37. data/lib/sequel/sql.rb +33 -0
  38. data/lib/sequel/version.rb +1 -1
  39. data/spec/adapters/sqlite_spec.rb +11 -6
  40. data/spec/core/core_sql_spec.rb +29 -0
  41. data/spec/core/database_spec.rb +16 -7
  42. data/spec/core/dataset_spec.rb +264 -20
  43. data/spec/extensions/boolean_readers_spec.rb +86 -0
  44. data/spec/extensions/inflector_spec.rb +67 -4
  45. data/spec/extensions/instance_hooks_spec.rb +133 -0
  46. data/spec/extensions/lazy_attributes_spec.rb +45 -5
  47. data/spec/extensions/nested_attributes_spec.rb +272 -0
  48. data/spec/extensions/serialization_spec.rb +64 -1
  49. data/spec/extensions/timestamps_spec.rb +150 -0
  50. data/spec/extensions/validation_helpers_spec.rb +18 -0
  51. data/spec/integration/dataset_test.rb +79 -2
  52. data/spec/integration/schema_test.rb +17 -0
  53. data/spec/integration/timezone_test.rb +55 -0
  54. data/spec/model/associations_spec.rb +19 -7
  55. data/spec/model/model_spec.rb +29 -0
  56. data/spec/model/record_spec.rb +36 -0
  57. data/spec/spec_config.rb +1 -1
  58. metadata +14 -2
@@ -0,0 +1,86 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe Sequel::Model, "BooleanReaders plugin" do
4
+ before do
5
+ @db = Sequel::Database.new({})
6
+ def @db.schema(*args)
7
+ [[:id, {}], [:y, {:type=>:integer, :db_type=>'tinyint(1)'}], [:b, {:type=>:boolean, :db_type=>'boolean'}]]
8
+ end
9
+
10
+ @c = Class.new(Sequel::Model(@db[:items]))
11
+ @p =lambda do
12
+ @columns = [:id, :b, :y]
13
+ def columns; @columns; end
14
+ end
15
+ @c.instance_eval(&@p)
16
+ end
17
+
18
+ specify "should create attribute? readers for all boolean attributes" do
19
+ @c.plugin(:boolean_readers)
20
+ o = @c.new
21
+ o.b?.should == nil
22
+ o.b = '1'
23
+ o.b?.should == true
24
+ o.b = '0'
25
+ o.b?.should == false
26
+ o.b = ''
27
+ o.b?.should == nil
28
+ end
29
+
30
+ specify "should not create attribute? readers for non-boolean attributes" do
31
+ @c.plugin(:boolean_readers)
32
+ proc{@c.new.y?}.should raise_error(NoMethodError)
33
+ proc{@c.new.id?}.should raise_error(NoMethodError)
34
+ end
35
+
36
+ specify "should accept a block to determine if an attribute is boolean" do
37
+ @c.plugin(:boolean_readers){|c| db_schema[c][:db_type] == 'tinyint(1)'}
38
+ proc{@c.new.b?}.should raise_error(NoMethodError)
39
+ o = @c.new
40
+ o.y.should == nil
41
+ o.y?.should == nil
42
+ o.y = '1'
43
+ o.y.should == 1
44
+ o.y?.should == true
45
+ o.y = '0'
46
+ o.y.should == 0
47
+ o.y?.should == false
48
+ o.y = ''
49
+ o.y.should == nil
50
+ o.y?.should == nil
51
+ end
52
+
53
+ specify "should create boolean readers when set_dataset is defined" do
54
+ c = Class.new(Sequel::Model(@db))
55
+ c.instance_eval(&@p)
56
+ c.plugin(:boolean_readers)
57
+ c.set_dataset(@db[:a])
58
+ o = c.new
59
+ o.b?.should == nil
60
+ o.b = '1'
61
+ o.b?.should == true
62
+ o.b = '0'
63
+ o.b?.should == false
64
+ o.b = ''
65
+ o.b?.should == nil
66
+ proc{o.i?}.should raise_error(NoMethodError)
67
+
68
+ c = Class.new(Sequel::Model(@db))
69
+ c.instance_eval(&@p)
70
+ c.plugin(:boolean_readers){|x| db_schema[x][:db_type] == 'tinyint(1)'}
71
+ c.set_dataset(@db[:a])
72
+ o = c.new
73
+ o.y.should == nil
74
+ o.y?.should == nil
75
+ o.y = '1'
76
+ o.y.should == 1
77
+ o.y?.should == true
78
+ o.y = '0'
79
+ o.y.should == 0
80
+ o.y?.should == false
81
+ o.y = ''
82
+ o.y.should == nil
83
+ o.y?.should == nil
84
+ proc{o.b?}.should raise_error(NoMethodError)
85
+ end
86
+ end
@@ -48,16 +48,16 @@ describe String do
48
48
 
49
49
  it "#pluralize should transform words from singular to plural" do
50
50
  "post".pluralize.should == "posts"
51
- "octopus".pluralize.should =="octopi"
51
+ "octopus".pluralize.should =="octopuses"
52
52
  "the blue mailman".pluralize.should == "the blue mailmen"
53
- "CamelOctopus".pluralize.should == "CamelOctopi"
53
+ "CamelOctopus".pluralize.should == "CamelOctopuses"
54
54
  end
55
55
 
56
56
  it "#singularize should transform words from plural to singular" do
57
57
  "posts".singularize.should == "post"
58
- "octopi".singularize.should == "octopus"
58
+ "octopuses".singularize.should == "octopus"
59
59
  "the blue mailmen".singularize.should == "the blue mailman"
60
- "CamelOctopi".singularize.should == "CamelOctopus"
60
+ "CamelOctopuses".singularize.should == "CamelOctopus"
61
61
  end
62
62
 
63
63
  it "#tableize should transform class names to table names" do
@@ -117,3 +117,66 @@ describe String::Inflections do
117
117
  String.inflections{|i| i.should == String::Inflections}.should == String::Inflections
118
118
  end
119
119
  end
120
+
121
+ describe 'Default inflections' do
122
+ it "should support the default inflection rules" do
123
+ {
124
+ :test=>:tests,
125
+ :ax=>:axes,
126
+ :testis=>:testes,
127
+ :octopus=>:octopuses,
128
+ :virus=>:viruses,
129
+ :alias=>:aliases,
130
+ :status=>:statuses,
131
+ :bus=>:buses,
132
+ :buffalo=>:buffaloes,
133
+ :tomato=>:tomatoes,
134
+ :datum=>:data,
135
+ :bacterium=>:bacteria,
136
+ :analysis=>:analyses,
137
+ :basis=>:bases,
138
+ :diagnosis=>:diagnoses,
139
+ :parenthesis=>:parentheses,
140
+ :prognosis=>:prognoses,
141
+ :synopsis=>:synopses,
142
+ :thesis=>:theses,
143
+ :wife=>:wives,
144
+ :giraffe=>:giraffes,
145
+ :self=>:selves,
146
+ :dwarf=>:dwarves,
147
+ :hive=>:hives,
148
+ :fly=>:flies,
149
+ :buy=>:buys,
150
+ :soliloquy=>:soliloquies,
151
+ :day=>:days,
152
+ :attorney=>:attorneys,
153
+ :boy=>:boys,
154
+ :hoax=>:hoaxes,
155
+ :lunch=>:lunches,
156
+ :princess=>:princesses,
157
+ :matrix=>:matrices,
158
+ :vertex=>:vertices,
159
+ :index=>:indices,
160
+ :mouse=>:mice,
161
+ :louse=>:lice,
162
+ :ox=>:oxen,
163
+ :quiz=>:quizzes,
164
+ :motive=>:motives,
165
+ :movie=>:movies,
166
+ :series=>:series,
167
+ :crisis=>:crises,
168
+ :person=>:people,
169
+ :man=>:men,
170
+ :woman=>:women,
171
+ :child=>:children,
172
+ :sex=>:sexes,
173
+ :move=>:moves
174
+ }.each do |k, v|
175
+ k.to_s.pluralize.should == v.to_s
176
+ v.to_s.singularize.should == k.to_s
177
+ end
178
+ [:equipment, :information, :rice, :money, :species, :series, :fish, :sheep, :news].each do |a|
179
+ a.to_s.pluralize.should == a.to_s.singularize
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,133 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe "InstanceHooks plugin" do
4
+ def r(x)
5
+ @r << x
6
+ x
7
+ end
8
+
9
+ before do
10
+ @c = Class.new(Sequel::Model(:items))
11
+ @c.plugin :instance_hooks
12
+ @c.raise_on_save_failure = false
13
+ @o = @c.new
14
+ @x = @c.load({:id=>1})
15
+ @r = []
16
+ end
17
+
18
+ it "should support before_create_hook and after_create_hook" do
19
+ @o.after_create_hook{r 1}
20
+ @o.before_create_hook{r 2}
21
+ @o.after_create_hook{r 3}
22
+ @o.before_create_hook{r 4}
23
+ @o.save.should_not == nil
24
+ @r.should == [4, 2, 1, 3]
25
+ end
26
+
27
+ it "should cancel the save if before_create_hook block returns false" do
28
+ @o.after_create_hook{r 1}
29
+ @o.before_create_hook{r false}
30
+ @o.before_create_hook{r 4}
31
+ @o.save.should == nil
32
+ @r.should == [4, false]
33
+ @r.clear
34
+ @o.save.should == nil
35
+ @r.should == [4, false]
36
+ end
37
+
38
+ it "should support before_update_hook and after_update_hook" do
39
+ @x.after_update_hook{r 1}
40
+ @x.before_update_hook{r 2}
41
+ @x.after_update_hook{r 3}
42
+ @x.before_update_hook{r 4}
43
+ @x.save.should_not == nil
44
+ @r.should == [4, 2, 1, 3]
45
+ @x.save.should_not == nil
46
+ @r.should == [4, 2, 1, 3]
47
+ end
48
+
49
+ it "should cancel the save if before_update_hook block returns false" do
50
+ @x.after_update_hook{r 1}
51
+ @x.before_update_hook{r false}
52
+ @x.before_update_hook{r 4}
53
+ @x.save.should == nil
54
+ @r.should == [4, false]
55
+ @r.clear
56
+ @x.save.should == nil
57
+ @r.should == [4, false]
58
+ end
59
+
60
+ it "should support before_save_hook and after_save_hook" do
61
+ @o.after_save_hook{r 1}
62
+ @o.before_save_hook{r 2}
63
+ @o.after_save_hook{r 3}
64
+ @o.before_save_hook{r 4}
65
+ @o.save.should_not == nil
66
+ @r.should == [4, 2, 1, 3]
67
+ @r.clear
68
+
69
+ @x.after_save_hook{r 1}
70
+ @x.before_save_hook{r 2}
71
+ @x.after_save_hook{r 3}
72
+ @x.before_save_hook{r 4}
73
+ @x.save.should_not == nil
74
+ @r.should == [4, 2, 1, 3]
75
+ @x.save.should_not == nil
76
+ @r.should == [4, 2, 1, 3]
77
+ end
78
+
79
+ it "should cancel the save if before_save_hook block returns false" do
80
+ @x.after_save_hook{r 1}
81
+ @x.before_save_hook{r false}
82
+ @x.before_save_hook{r 4}
83
+ @x.save.should == nil
84
+ @r.should == [4, false]
85
+ @r.clear
86
+
87
+ @x.after_save_hook{r 1}
88
+ @x.before_save_hook{r false}
89
+ @x.before_save_hook{r 4}
90
+ @x.save.should == nil
91
+ @r.should == [4, false]
92
+ @r.clear
93
+ @x.save.should == nil
94
+ @r.should == [4, false]
95
+ end
96
+
97
+ it "should support before_destroy_hook and after_destroy_hook" do
98
+ @x.after_destroy_hook{r 1}
99
+ @x.before_destroy_hook{r 2}
100
+ @x.after_destroy_hook{r 3}
101
+ @x.before_destroy_hook{r 4}
102
+ @x.destroy.should_not == nil
103
+ @r.should == [4, 2, 1, 3]
104
+ end
105
+
106
+ it "should cancel the destroy if before_destroy_hook block returns false" do
107
+ @x.after_destroy_hook{r 1}
108
+ @x.before_destroy_hook{r false}
109
+ @x.before_destroy_hook{r 4}
110
+ @x.destroy.should == nil
111
+ @r.should == [4, false]
112
+ end
113
+
114
+ it "should support before_validation_hook and after_validation_hook" do
115
+ @o.after_validation_hook{r 1}
116
+ @o.before_validation_hook{r 2}
117
+ @o.after_validation_hook{r 3}
118
+ @o.before_validation_hook{r 4}
119
+ @o.valid?.should == true
120
+ @r.should == [4, 2, 1, 3]
121
+ end
122
+
123
+ it "should cancel the save if before_validation_hook block returns false" do
124
+ @o.after_validation_hook{r 1}
125
+ @o.before_validation_hook{r false}
126
+ @o.before_validation_hook{r 4}
127
+ @o.valid?.should == false
128
+ @r.should == [4, false]
129
+ @r.clear
130
+ @o.valid?.should == false
131
+ @r.should == [4, false]
132
+ end
133
+ end
@@ -4,7 +4,7 @@ describe "Sequel::Plugins::LazyAttributes" do
4
4
  before do
5
5
  class ::LazyAttributesModel < Sequel::Model(:la)
6
6
  plugin :lazy_attributes
7
- columns :id, :name
7
+ set_columns([:id, :name])
8
8
  meta_def(:columns){[:id, :name]}
9
9
  lazy_attributes :name
10
10
  meta_def(:columns){[:id]}
@@ -13,10 +13,11 @@ describe "Sequel::Plugins::LazyAttributes" do
13
13
  execute(sql)
14
14
  select = @opts[:select]
15
15
  where = @opts[:where]
16
+ block = @mod_block || proc{|s| s}
16
17
  if !where
17
18
  if select.include?(:name)
18
- yield(:id=>1, :name=>'1')
19
- yield(:id=>2, :name=>'2')
19
+ yield(block[:id=>1, :name=>'1'])
20
+ yield(block[:id=>2, :name=>'2'])
20
21
  else
21
22
  yield(:id=>1)
22
23
  yield(:id=>2)
@@ -26,9 +27,9 @@ describe "Sequel::Plugins::LazyAttributes" do
26
27
  i = i.instance_variable_get(:@array) if i.is_a?(Sequel::SQL::SQLArray)
27
28
  Array(i).each do |x|
28
29
  if sql =~ /SELECT name FROM/
29
- yield(:name=>x.to_s)
30
+ yield(block[:name=>x.to_s])
30
31
  else
31
- yield(:id=>x, :name=>x.to_s)
32
+ yield(block[:id=>x, :name=>x.to_s])
32
33
  end
33
34
  end
34
35
  end
@@ -110,4 +111,43 @@ describe "Sequel::Plugins::LazyAttributes" do
110
111
  MODEL_DB.sqls.should == ['SELECT id FROM la', 'SELECT id, name FROM la WHERE (id IN (1, 2))']
111
112
  end
112
113
  end
114
+
115
+ it "should add the accessors to a module included in the class, so they can be easily overridden" do
116
+ @c.class_eval do
117
+ def name
118
+ "#{super}-blah"
119
+ end
120
+ end
121
+ @c.with_identity_map do
122
+ ms = @c.all
123
+ ms.map{|m| m.values}.should == [{:id=>1}, {:id=>2}]
124
+ ms.map{|m| m.name}.should == %w'1-blah 2-blah'
125
+ ms.map{|m| m.values}.should == [{:id=>1, :name=>'1'}, {:id=>2, :name=>'2'}]
126
+ MODEL_DB.sqls.should == ['SELECT id FROM la', 'SELECT id, name FROM la WHERE (id IN (1, 2))']
127
+ end
128
+ end
129
+
130
+ it "should work with the serialization plugin" do
131
+ @c.plugin :serialization, :yaml, :name
132
+ @ds.instance_variable_set(:@mod_block, proc{|s| s.merge(:name=>"--- #{s[:name].to_i*3}\n")})
133
+ @c.with_identity_map do
134
+ ms = @ds.all
135
+ ms.map{|m| m.values}.should == [{:id=>1}, {:id=>2}]
136
+ ms.map{|m| m.name}.should == [3,6]
137
+ ms.map{|m| m.values}.should == [{:id=>1, :name=>"--- 3\n"}, {:id=>2, :name=>"--- 6\n"}]
138
+ ms.map{|m| m.deserialized_values}.should == [{:name=>3}, {:name=>6}]
139
+ ms.map{|m| m.name}.should == [3,6]
140
+ MODEL_DB.sqls.should == ['SELECT id FROM la', 'SELECT id, name FROM la WHERE (id IN (1, 2))']
141
+ end
142
+ MODEL_DB.reset
143
+ @c.with_identity_map do
144
+ m = @ds.first
145
+ m.values.should == {:id=>1}
146
+ m.name.should == 3
147
+ m.values.should == {:id=>1, :name=>"--- 3\n"}
148
+ m.deserialized_values.should == {:name=>3}
149
+ m.name.should == 3
150
+ MODEL_DB.sqls.should == ["SELECT id FROM la LIMIT 1", "SELECT name FROM la WHERE (id = 1) LIMIT 1"]
151
+ end
152
+ end
113
153
  end
@@ -0,0 +1,272 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe "NestedAttributes plugin" do
4
+ before do
5
+ mods = @mods = []
6
+ i = 0
7
+ gi = lambda{i}
8
+ ii = lambda{i+=1}
9
+ ds_mod = Module.new do
10
+ define_method(:insert) do |h|
11
+ x = ii.call
12
+ mods << [:i, first_source, h, x]
13
+ x
14
+ end
15
+ define_method(:insert_select) do |h|
16
+ x = ii.call
17
+ mods << [:is, first_source, h, x]
18
+ h.merge(:id=>x)
19
+ end
20
+ define_method(:update) do |h|
21
+ mods << [:u, first_source, h, literal(opts[:where])]
22
+ 1
23
+ end
24
+ define_method(:delete) do
25
+ mods << [:d, first_source, literal(opts[:where])]
26
+ 1
27
+ end
28
+ end
29
+ db = Sequel::Database.new({})
30
+ db.meta_def(:dataset) do |*a|
31
+ x = super(*a)
32
+ x.extend(ds_mod)
33
+ x
34
+ end
35
+ @c = Class.new(Sequel::Model(db))
36
+ @c.plugin :nested_attributes
37
+ @Artist = Class.new(@c).set_dataset(:artists)
38
+ @Album = Class.new(@c).set_dataset(:albums)
39
+ @Tag = Class.new(@c).set_dataset(:tags)
40
+ [@Artist, @Album, @Tag].each do |m|
41
+ m.dataset.extend(ds_mod)
42
+ end
43
+ @Artist.columns :id, :name
44
+ @Album.columns :id, :name, :artist_id
45
+ @Tag.columns :id, :name
46
+ @Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id
47
+ @Album.many_to_one :artist, :class=>@Artist
48
+ @Album.many_to_many :tags, :class=>@Tag, :left_key=>:album_id, :right_key=>:tag_id, :join_table=>:at
49
+ @Artist.nested_attributes :albums, :destroy=>true, :remove=>true
50
+ @Album.nested_attributes :artist, :tags, :destroy=>true, :remove=>true
51
+ end
52
+
53
+ it "should support creating new many_to_one objects" do
54
+ a = @Album.new({:name=>'Al', :artist_attributes=>{:name=>'Ar'}})
55
+ @mods.should == []
56
+ a.save
57
+ @mods.should == [[:is, :artists, {:name=>"Ar"}, 1], [:is, :albums, {:name=>"Al", :artist_id=>1}, 2]]
58
+ end
59
+
60
+ it "should support creating new one_to_many objects" do
61
+ a = @Artist.new({:name=>'Ar', :albums_attributes=>[{:name=>'Al'}]})
62
+ @mods.should == []
63
+ a.save
64
+ @mods.should == [[:is, :artists, {:name=>"Ar"}, 1], [:is, :albums, {:name=>"Al", :artist_id=>1}, 2]]
65
+ end
66
+
67
+ it "should support creating new many_to_many objects" do
68
+ a = @Album.new({:name=>'Al', :tags_attributes=>[{:name=>'T'}]})
69
+ @mods.should == []
70
+ a.save
71
+ @mods.should == [[:is, :albums, {:name=>"Al"}, 1], [:is, :tags, {:name=>"T"}, 2], [:i, :at, {:album_id=>1, :tag_id=>2}, 3]]
72
+ end
73
+
74
+ it "should support updating many_to_one objects" do
75
+ al = @Album.load(:id=>10, :name=>'Al')
76
+ ar = @Artist.load(:id=>20, :name=>'Ar')
77
+ al.associations[:artist] = ar
78
+ al.set(:artist_attributes=>{:id=>'20', :name=>'Ar2'})
79
+ @mods.should == []
80
+ al.save
81
+ @mods.should == [[:u, :albums, {:name=>"Al"}, '(id = 10)'], [:u, :artists, {:name=>"Ar2"}, '(id = 20)']]
82
+ end
83
+
84
+ it "should support updating one_to_many objects" do
85
+ al = @Album.load(:id=>10, :name=>'Al')
86
+ ar = @Artist.load(:id=>20, :name=>'Ar')
87
+ ar.associations[:albums] = [al]
88
+ ar.set(:albums_attributes=>[{:id=>10, :name=>'Al2'}])
89
+ @mods.should == []
90
+ ar.save
91
+ @mods.should == [[:u, :artists, {:name=>"Ar"}, '(id = 20)'], [:u, :albums, {:name=>"Al2"}, '(id = 10)']]
92
+ end
93
+
94
+ it "should support updating many_to_many objects" do
95
+ a = @Album.load(:id=>10, :name=>'Al')
96
+ t = @Tag.load(:id=>20, :name=>'T')
97
+ a.associations[:tags] = [t]
98
+ a.set(:tags_attributes=>[{:id=>20, :name=>'T2'}])
99
+ @mods.should == []
100
+ a.save
101
+ @mods.should == [[:u, :albums, {:name=>"Al"}, '(id = 10)'], [:u, :tags, {:name=>"T2"}, '(id = 20)']]
102
+ end
103
+
104
+ it "should support removing many_to_one objects" do
105
+ al = @Album.load(:id=>10, :name=>'Al')
106
+ ar = @Artist.load(:id=>20, :name=>'Ar')
107
+ al.associations[:artist] = ar
108
+ al.set(:artist_attributes=>{:id=>'20', :_remove=>'1'})
109
+ @mods.should == []
110
+ al.save
111
+ @mods.should == [[:u, :albums, {:artist_id=>nil, :name=>'Al'}, '(id = 10)']]
112
+ end
113
+
114
+ it "should support removing one_to_many objects" do
115
+ al = @Album.load(:id=>10, :name=>'Al')
116
+ ar = @Artist.load(:id=>20, :name=>'Ar')
117
+ ar.associations[:albums] = [al]
118
+ ar.set(:albums_attributes=>[{:id=>10, :_remove=>'t'}])
119
+ @mods.should == []
120
+ ar.save
121
+ @mods.should == [[:u, :albums, {:name=>"Al", :artist_id=>nil}, '(id = 10)'], [:u, :artists, {:name=>"Ar"}, '(id = 20)']]
122
+ end
123
+
124
+ it "should support removing many_to_many objects" do
125
+ a = @Album.load(:id=>10, :name=>'Al')
126
+ t = @Tag.load(:id=>20, :name=>'T')
127
+ a.associations[:tags] = [t]
128
+ a.set(:tags_attributes=>[{:id=>20, :_remove=>true}])
129
+ @mods.should == []
130
+ a.save
131
+ @mods.should == [[:d, :at, '((album_id = 10) AND (tag_id = 20))'], [:u, :albums, {:name=>"Al"}, '(id = 10)']]
132
+ end
133
+
134
+ it "should support destroying many_to_one objects" do
135
+ al = @Album.load(:id=>10, :name=>'Al')
136
+ ar = @Artist.load(:id=>20, :name=>'Ar')
137
+ al.associations[:artist] = ar
138
+ al.set(:artist_attributes=>{:id=>'20', :_delete=>'1'})
139
+ @mods.should == []
140
+ al.save
141
+ @mods.should == [[:u, :albums, {:artist_id=>nil, :name=>'Al'}, '(id = 10)'], [:d, :artists, '(id = 20)']]
142
+ end
143
+
144
+ it "should support destroying one_to_many objects" do
145
+ al = @Album.load(:id=>10, :name=>'Al')
146
+ ar = @Artist.load(:id=>20, :name=>'Ar')
147
+ ar.associations[:albums] = [al]
148
+ ar.set(:albums_attributes=>[{:id=>10, :_delete=>'t'}])
149
+ @mods.should == []
150
+ ar.save
151
+ @mods.should == [[:u, :albums, {:name=>"Al", :artist_id=>nil}, '(id = 10)'], [:u, :artists, {:name=>"Ar"}, '(id = 20)'], [:d, :albums, '(id = 10)']]
152
+ end
153
+
154
+ it "should support destroying many_to_many objects" do
155
+ a = @Album.load(:id=>10, :name=>'Al')
156
+ t = @Tag.load(:id=>20, :name=>'T')
157
+ a.associations[:tags] = [t]
158
+ a.set(:tags_attributes=>[{:id=>20, :_delete=>true}])
159
+ @mods.should == []
160
+ a.save
161
+ @mods.should == [[:d, :at, '((album_id = 10) AND (tag_id = 20))'], [:u, :albums, {:name=>"Al"}, '(id = 10)'], [:d, :tags, '(id = 20)']]
162
+ end
163
+
164
+ it "should support both string and symbol keys in nested attribute hashes" do
165
+ a = @Album.load(:id=>10, :name=>'Al')
166
+ t = @Tag.load(:id=>20, :name=>'T')
167
+ a.associations[:tags] = [t]
168
+ a.set('tags_attributes'=>[{'id'=>20, '_delete'=>true}])
169
+ @mods.should == []
170
+ a.save
171
+ @mods.should == [[:d, :at, '((album_id = 10) AND (tag_id = 20))'], [:u, :albums, {:name=>"Al"}, '(id = 10)'], [:d, :tags, '(id = 20)']]
172
+ end
173
+
174
+ it "should support using a hash instead of an array for to_many nested attributes" do
175
+ a = @Album.load(:id=>10, :name=>'Al')
176
+ t = @Tag.load(:id=>20, :name=>'T')
177
+ a.associations[:tags] = [t]
178
+ a.set('tags_attributes'=>{'1'=>{'id'=>20, '_delete'=>true}})
179
+ @mods.should == []
180
+ a.save
181
+ @mods.should == [[:d, :at, '((album_id = 10) AND (tag_id = 20))'], [:u, :albums, {:name=>"Al"}, '(id = 10)'], [:d, :tags, '(id = 20)']]
182
+ end
183
+
184
+ it "should only allow destroying associated objects if :destroy option is used in the nested_attributes call" do
185
+ a = @Album.load(:id=>10, :name=>'Al')
186
+ ar = @Artist.load(:id=>20, :name=>'Ar')
187
+ a.associations[:artist] = ar
188
+ @Album.nested_attributes :artist
189
+ proc{a.set(:artist_attributes=>{:id=>'20', :_delete=>'1'})}.should raise_error(Sequel::Error)
190
+ @Album.nested_attributes :artist, :destroy=>true
191
+ proc{a.set(:artist_attributes=>{:id=>'20', :_delete=>'1'})}.should_not raise_error(Sequel::Error)
192
+ end
193
+
194
+ it "should only allow removing associated objects if :remove option is used in the nested_attributes call" do
195
+ a = @Album.load(:id=>10, :name=>'Al')
196
+ ar = @Artist.load(:id=>20, :name=>'Ar')
197
+ a.associations[:artist] = ar
198
+ @Album.nested_attributes :artist
199
+ proc{a.set(:artist_attributes=>{:id=>'20', :_remove=>'1'})}.should raise_error(Sequel::Error)
200
+ @Album.nested_attributes :artist, :remove=>true
201
+ proc{a.set(:artist_attributes=>{:id=>'20', :_remove=>'1'})}.should_not raise_error(Sequel::Error)
202
+ end
203
+
204
+ it "should raise an Error if a primary key is given in a nested attribute hash, but no matching associated object exists" do
205
+ al = @Album.load(:id=>10, :name=>'Al')
206
+ ar = @Artist.load(:id=>20, :name=>'Ar')
207
+ ar.associations[:albums] = [al]
208
+ proc{ar.set(:albums_attributes=>[{:id=>30, :_delete=>'t'}])}.should raise_error(Sequel::Error)
209
+ proc{ar.set(:albums_attributes=>[{:id=>10, :_delete=>'t'}])}.should_not raise_error(Sequel::Error)
210
+ end
211
+
212
+ it "should not raise an Error if an unmatched primary key is given, if the :strict=>false option is used" do
213
+ @Artist.nested_attributes :albums, :strict=>false
214
+ al = @Album.load(:id=>10, :name=>'Al')
215
+ ar = @Artist.load(:id=>20, :name=>'Ar')
216
+ ar.associations[:albums] = [al]
217
+ ar.set(:albums_attributes=>[{:id=>30, :_delete=>'t'}])
218
+ @mods.should == []
219
+ ar.save
220
+ @mods.should == [[:u, :artists, {:name=>"Ar"}, '(id = 20)']]
221
+ end
222
+
223
+ it "should not save if nested attribute is not validate and should include nested attribute validation errors in the main object's validation errors" do
224
+ @Artist.class_eval do
225
+ def validate
226
+ super
227
+ errors.add(:name, 'cannot be Ar') if name == 'Ar'
228
+ end
229
+ end
230
+ a = @Album.new(:name=>'Al', :artist_attributes=>{:name=>'Ar'})
231
+ @mods.should == []
232
+ proc{a.save}.should raise_error(Sequel::ValidationFailed)
233
+ a.errors.full_messages.should == ['artist name cannot be Ar']
234
+ @mods.should == []
235
+ end
236
+
237
+ it "should not accept nested attributes unless explicitly specified" do
238
+ @Artist.many_to_many :tags, :class=>@Tag, :left_key=>:album_id, :right_key=>:tag_id, :join_table=>:at
239
+ proc{@Artist.create({:name=>'Ar', :tags_attributes=>[{:name=>'T'}]})}.should raise_error(Sequel::Error)
240
+ @mods.should == []
241
+ end
242
+
243
+ it "should save when save_changes or update is called if nested attribute associated objects changed but there are no changes to the main object" do
244
+ al = @Album.load(:id=>10, :name=>'Al')
245
+ ar = @Artist.load(:id=>20, :name=>'Ar')
246
+ al.associations[:artist] = ar
247
+ al.update(:artist_attributes=>{:id=>'20', :name=>'Ar2'})
248
+ @mods.should == [[:u, :artists, {:name=>"Ar2"}, '(id = 20)']]
249
+ end
250
+
251
+ it "should have a :limit option limiting the amount of entries" do
252
+ @Album.nested_attributes :tags, :limit=>2
253
+ arr = [{:name=>'T'}]
254
+ proc{@Album.new({:name=>'Al', :tags_attributes=>arr*3})}.should raise_error(Sequel::Error)
255
+ a = @Album.new({:name=>'Al', :tags_attributes=>arr*2})
256
+ @mods.should == []
257
+ a.save
258
+ @mods.should == [[:is, :albums, {:name=>"Al"}, 1], [:is, :tags, {:name=>"T"}, 2], [:i, :at, {:album_id=>1, :tag_id=>2}, 3], [:is, :tags, {:name=>"T"}, 4], [:i, :at, {:album_id=>1, :tag_id=>4}, 5]]
259
+ end
260
+
261
+ it "should accept a block that each hash gets passed to determine if it should be processed" do
262
+ @Album.nested_attributes(:tags){|h| h[:name].empty?}
263
+ a = @Album.new({:name=>'Al', :tags_attributes=>[{:name=>'T'}, {:name=>''}, {:name=>'T2'}]})
264
+ @mods.should == []
265
+ a.save
266
+ @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]]
267
+ end
268
+
269
+ it "should deal properly with accessing the same nested attribute associated object with conflicting actions" do
270
+
271
+ end
272
+ end