sequel 3.3.0 → 3.4.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 (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