sequel 3.35.0 → 3.36.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 (81) hide show
  1. data/CHANGELOG +78 -0
  2. data/Rakefile +3 -3
  3. data/bin/sequel +3 -1
  4. data/doc/advanced_associations.rdoc +154 -11
  5. data/doc/migration.rdoc +18 -0
  6. data/doc/object_model.rdoc +541 -0
  7. data/doc/opening_databases.rdoc +4 -1
  8. data/doc/release_notes/3.36.0.txt +245 -0
  9. data/doc/schema_modification.rdoc +0 -6
  10. data/lib/sequel/adapters/do/mysql.rb +7 -0
  11. data/lib/sequel/adapters/jdbc.rb +11 -3
  12. data/lib/sequel/adapters/jdbc/mysql.rb +3 -5
  13. data/lib/sequel/adapters/jdbc/postgresql.rb +10 -8
  14. data/lib/sequel/adapters/jdbc/progress.rb +21 -0
  15. data/lib/sequel/adapters/mock.rb +2 -6
  16. data/lib/sequel/adapters/mysql.rb +3 -9
  17. data/lib/sequel/adapters/mysql2.rb +12 -11
  18. data/lib/sequel/adapters/postgres.rb +32 -40
  19. data/lib/sequel/adapters/shared/mssql.rb +15 -11
  20. data/lib/sequel/adapters/shared/mysql.rb +28 -3
  21. data/lib/sequel/adapters/shared/oracle.rb +5 -0
  22. data/lib/sequel/adapters/shared/postgres.rb +59 -5
  23. data/lib/sequel/adapters/shared/sqlite.rb +3 -13
  24. data/lib/sequel/adapters/sqlite.rb +0 -7
  25. data/lib/sequel/adapters/swift/mysql.rb +2 -5
  26. data/lib/sequel/adapters/tinytds.rb +1 -2
  27. data/lib/sequel/connection_pool/sharded_threaded.rb +5 -1
  28. data/lib/sequel/connection_pool/threaded.rb +9 -1
  29. data/lib/sequel/database/dataset_defaults.rb +3 -1
  30. data/lib/sequel/database/misc.rb +7 -1
  31. data/lib/sequel/database/query.rb +11 -3
  32. data/lib/sequel/database/schema_generator.rb +40 -9
  33. data/lib/sequel/database/schema_methods.rb +6 -1
  34. data/lib/sequel/dataset/actions.rb +5 -5
  35. data/lib/sequel/dataset/prepared_statements.rb +3 -1
  36. data/lib/sequel/dataset/query.rb +1 -1
  37. data/lib/sequel/extensions/migration.rb +28 -0
  38. data/lib/sequel/extensions/pg_auto_parameterize.rb +0 -9
  39. data/lib/sequel/extensions/pg_inet.rb +89 -0
  40. data/lib/sequel/extensions/pg_json.rb +178 -0
  41. data/lib/sequel/extensions/schema_dumper.rb +24 -6
  42. data/lib/sequel/model/associations.rb +19 -15
  43. data/lib/sequel/model/base.rb +11 -12
  44. data/lib/sequel/plugins/composition.rb +1 -2
  45. data/lib/sequel/plugins/eager_each.rb +59 -0
  46. data/lib/sequel/plugins/json_serializer.rb +41 -4
  47. data/lib/sequel/plugins/nested_attributes.rb +72 -52
  48. data/lib/sequel/plugins/optimistic_locking.rb +8 -0
  49. data/lib/sequel/plugins/tactical_eager_loading.rb +7 -7
  50. data/lib/sequel/version.rb +1 -1
  51. data/spec/adapters/postgres_spec.rb +271 -1
  52. data/spec/adapters/sqlite_spec.rb +11 -0
  53. data/spec/core/connection_pool_spec.rb +26 -1
  54. data/spec/core/database_spec.rb +19 -0
  55. data/spec/core/dataset_spec.rb +45 -5
  56. data/spec/core/expression_filters_spec.rb +31 -67
  57. data/spec/core/mock_adapter_spec.rb +4 -0
  58. data/spec/extensions/core_extensions_spec.rb +83 -0
  59. data/spec/extensions/eager_each_spec.rb +34 -0
  60. data/spec/extensions/inflector_spec.rb +0 -4
  61. data/spec/extensions/json_serializer_spec.rb +32 -1
  62. data/spec/extensions/migration_spec.rb +28 -0
  63. data/spec/extensions/nested_attributes_spec.rb +134 -1
  64. data/spec/extensions/optimistic_locking_spec.rb +15 -1
  65. data/spec/extensions/pg_hstore_spec.rb +1 -1
  66. data/spec/extensions/pg_inet_spec.rb +44 -0
  67. data/spec/extensions/pg_json_spec.rb +101 -0
  68. data/spec/extensions/prepared_statements_spec.rb +30 -0
  69. data/spec/extensions/rcte_tree_spec.rb +9 -0
  70. data/spec/extensions/schema_dumper_spec.rb +195 -7
  71. data/spec/extensions/serialization_spec.rb +4 -0
  72. data/spec/extensions/spec_helper.rb +9 -1
  73. data/spec/extensions/tactical_eager_loading_spec.rb +8 -0
  74. data/spec/integration/database_test.rb +5 -1
  75. data/spec/integration/prepared_statement_test.rb +20 -2
  76. data/spec/model/associations_spec.rb +27 -0
  77. data/spec/model/base_spec.rb +54 -0
  78. data/spec/model/model_spec.rb +6 -0
  79. data/spec/model/record_spec.rb +18 -0
  80. data/spec/rcov.opts +2 -0
  81. metadata +14 -3
@@ -0,0 +1,34 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ describe "Sequel::Plugins::EagerEach" do
4
+ before do
5
+ @c = Class.new(Sequel::Model(:items))
6
+ @c.columns :id, :parent_id
7
+ @c.plugin :eager_each
8
+ @c.one_to_many :children, :class=>@c, :key=>:parent_id
9
+ @c.db.sqls
10
+ end
11
+
12
+ it "should make #each on an eager dataset do eager loading" do
13
+ a = []
14
+ ds = @c.eager(:children)
15
+ ds._fetch = [{:id=>1, :parent_id=>nil}, {:id=>2, :parent_id=>nil}]
16
+ @c.dataset._fetch = [{:id=>3, :parent_id=>1}, {:id=>4, :parent_id=>1}, {:id=>5, :parent_id=>2}, {:id=>6, :parent_id=>2}]
17
+ ds.each{|c| a << c}
18
+ a.should == [@c.load(:id=>1, :parent_id=>nil), @c.load(:id=>2, :parent_id=>nil)]
19
+ a.map{|c| c.associations[:children]}.should == [[@c.load(:id=>3, :parent_id=>1), @c.load(:id=>4, :parent_id=>1)], [@c.load(:id=>5, :parent_id=>2), @c.load(:id=>6, :parent_id=>2)]]
20
+ @c.db.sqls.should == ['SELECT * FROM items', 'SELECT * FROM items WHERE (items.parent_id IN (1, 2))']
21
+ end
22
+
23
+ it "should make #each on an eager_graph dataset do eager loading" do
24
+ a = []
25
+ ds = @c.eager_graph(:children)
26
+ ds._fetch = []
27
+ ds._fetch = [{:id=>1, :parent_id=>nil, :children_id=>3, :children_parent_id=>1}, {:id=>1, :parent_id=>nil, :children_id=>4, :children_parent_id=>1}, {:id=>2, :parent_id=>nil, :children_id=>5, :children_parent_id=>2}, {:id=>2, :parent_id=>nil, :children_id=>6, :children_parent_id=>2}]
28
+ ds.each{|c| a << c}
29
+ a.should == [@c.load(:id=>1, :parent_id=>nil), @c.load(:id=>2, :parent_id=>nil)]
30
+ a.map{|c| c.associations[:children]}.should == [[@c.load(:id=>3, :parent_id=>1), @c.load(:id=>4, :parent_id=>1)], [@c.load(:id=>5, :parent_id=>2), @c.load(:id=>6, :parent_id=>2)]]
31
+ @c.db.sqls.should == ['SELECT items.id, items.parent_id, children.id AS children_id, children.parent_id AS children_parent_id FROM items LEFT OUTER JOIN items AS children ON (children.parent_id = items.id)']
32
+ end
33
+
34
+ end
@@ -1,8 +1,5 @@
1
1
  require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper')
2
2
 
3
- if defined?(ActiveSupport::Inflector)
4
- skip_warn "inflector extension: active_support inflector loaded"
5
- else
6
3
  describe String do
7
4
  it "#camelize and #camelcase should transform the word to CamelCase" do
8
5
  "egg_and_hams".camelize.should == "EggAndHams"
@@ -182,4 +179,3 @@ describe 'Default inflections' do
182
179
  end
183
180
  end
184
181
  end
185
- end
@@ -96,6 +96,22 @@ describe "Sequel::Plugins::JsonSerializer" do
96
96
  @artist.id.should == 2
97
97
  end
98
98
 
99
+ it "should support #from_json to support specific :fields" do
100
+ @album.from_json('{"name": "AS", "artist_id": 3}', :fields=>['name'])
101
+ @album.name.should == 'AS'
102
+ @album.artist_id.should == 2
103
+ end
104
+
105
+ it "should support #from_json to support :missing=>:skip option" do
106
+ @album.from_json('{"artist_id": 3}', :fields=>['name'], :missing=>:skip)
107
+ @album.name.should == 'RF'
108
+ @album.artist_id.should == 2
109
+ end
110
+
111
+ it "should support #from_json to support :missing=>:raise option" do
112
+ proc{@album.from_json('{"artist_id": 3}', :fields=>['name'], :missing=>:raise)}.should raise_error(Sequel::Error)
113
+ end
114
+
99
115
  it "should raise an exception for json keys that aren't associations, columns, or setter methods" do
100
116
  Album.send(:undef_method, :blah=)
101
117
  proc{JSON.parse(@album.to_json(:include=>:blah))}.should raise_error(Sequel::Error)
@@ -149,7 +165,22 @@ describe "Sequel::Plugins::JsonSerializer" do
149
165
  @album.to_json(:root=>true, :only => :name).to_s.should == '{"album":{"name":"RF"}}'
150
166
  end
151
167
 
152
- it "should handle the :root option to qualify a dataset of records" do
168
+ it "should handle the :root=>:both option to qualify a dataset of records" do
169
+ Album.dataset._fetch = [{:id=>1, :name=>'RF'}, {:id=>1, :name=>'RF'}]
170
+ Album.dataset.to_json(:root=>true, :only => :id).to_s.should == '{"albums":[{"album":{"id":1}},{"album":{"id":1}}]}'
171
+ end
172
+
173
+ it "should handle the :root=>:collection option to qualify just the collection" do
174
+ Album.dataset._fetch = [{:id=>1, :name=>'RF'}, {:id=>1, :name=>'RF'}]
175
+ Album.dataset.to_json(:root=>:collection, :only => :id).to_s.should == '{"albums":[{"id":1},{"id":1}]}'
176
+ end
177
+
178
+ it "should handle the :root=>:instance option to qualify just the instances" do
179
+ Album.dataset._fetch = [{:id=>1, :name=>'RF'}, {:id=>1, :name=>'RF'}]
180
+ Album.dataset.to_json(:root=>:instance, :only => :id).to_s.should == '[{"album":{"id":1}},{"album":{"id":1}}]'
181
+ end
182
+
183
+ it "should handle the :root=>true option be the same as :root=>:both for backwards compatibility" do
153
184
  Album.dataset._fetch = [{:id=>1, :name=>'RF'}, {:id=>1, :name=>'RF'}]
154
185
  Album.dataset.to_json(:root=>true, :only => :id).to_s.should == '{"albums":[{"album":{"id":1}},{"album":{"id":1}}]}'
155
186
  end
@@ -277,8 +277,20 @@ describe "Sequel::IntegerMigrator" do
277
277
  @db.creates.should == [1111, 2222, 3333]
278
278
  @db.version.should == 3
279
279
  @db.sqls.map{|x| x =~ /\AUPDATE.*(\d+)/ ? $1.to_i : nil}.compact.should == [1, 2, 3]
280
+ end
281
+
282
+ specify "should be able to tell whether there are outstanding migrations" do
283
+ Sequel::Migrator.is_current?(@db, @dirname).should be_false
284
+ Sequel::Migrator.apply(@db, @dirname)
285
+ Sequel::Migrator.is_current?(@db, @dirname).should be_true
280
286
  end
281
287
 
288
+ specify "should have #check_current raise an exception if the migrator is not current" do
289
+ proc{Sequel::Migrator.check_current(@db, @dirname)}.should raise_error(Sequel::Migrator::NotCurrentError)
290
+ Sequel::Migrator.apply(@db, @dirname)
291
+ proc{Sequel::Migrator.check_current(@db, @dirname)}.should_not raise_error
292
+ end
293
+
282
294
  specify "should apply migrations correctly in the up direction with target" do
283
295
  Sequel::Migrator.apply(@db, @dirname, 2)
284
296
  @db.creates.should == [1111, 2222]
@@ -448,6 +460,22 @@ describe "Sequel::TimestampMigrator" do
448
460
  @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb'
449
461
  end
450
462
 
463
+ specify "should not be current when there are migrations to apply" do
464
+ @dir = 'spec/files/timestamped_migrations'
465
+ @m.apply(@db, @dir)
466
+ @m.is_current?(@db, @dir).should be_true
467
+ @dir = 'spec/files/interleaved_timestamped_migrations'
468
+ @m.is_current?(@db, @dir).should be_false
469
+ end
470
+
471
+ specify "should raise an exception if the migrator is not current" do
472
+ @dir = 'spec/files/timestamped_migrations'
473
+ @m.apply(@db, @dir)
474
+ proc{@m.check_current(@db, @dir)}.should_not raise_error
475
+ @dir = 'spec/files/interleaved_timestamped_migrations'
476
+ proc{@m.check_current(@db, @dir)}.should raise_error(Sequel::Migrator::NotCurrentError)
477
+ end
478
+
451
479
  specify "should apply all missing files when migrating up" do
452
480
  @dir = 'spec/files/timestamped_migrations'
453
481
  @m.apply(@db, @dir)
@@ -22,17 +22,24 @@ describe "NestedAttributes plugin" do
22
22
  @Artist = Class.new(@c).set_dataset(:artists)
23
23
  @Album = Class.new(@c).set_dataset(:albums)
24
24
  @Tag = Class.new(@c).set_dataset(:tags)
25
+ @Concert = Class.new(@c).set_dataset(:concerts)
25
26
  @Artist.plugin :skip_create_refresh
26
27
  @Album.plugin :skip_create_refresh
27
28
  @Tag.plugin :skip_create_refresh
29
+ @Concert.plugin :skip_create_refresh
28
30
  @Artist.columns :id, :name
29
31
  @Album.columns :id, :name, :artist_id
30
32
  @Tag.columns :id, :name
33
+ @Concert.columns :tour, :date, :artist_id, :playlist
34
+ @Concert.set_primary_key([:tour, :date])
35
+ @Concert.unrestrict_primary_key
31
36
  @Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id
37
+ @Artist.one_to_many :concerts, :class=>@Concert, :key=>:artist_id
32
38
  @Artist.one_to_one :first_album, :class=>@Album, :key=>:artist_id
33
39
  @Album.many_to_one :artist, :class=>@Artist
34
40
  @Album.many_to_many :tags, :class=>@Tag, :left_key=>:album_id, :right_key=>:tag_id, :join_table=>:at
35
41
  @Artist.nested_attributes :albums, :first_album, :destroy=>true, :remove=>true
42
+ @Artist.nested_attributes :concerts, :destroy=>true, :remove=>true
36
43
  @Album.nested_attributes :artist, :tags, :destroy=>true, :remove=>true
37
44
  @db.sqls
38
45
  end
@@ -80,6 +87,56 @@ describe "NestedAttributes plugin" do
80
87
  a.albums.first.tags.should == [@Tag.new(:name=>'T')]
81
88
  end
82
89
 
90
+ it "should support creating new objects with composite primary keys" do
91
+ insert = nil
92
+ @Concert.class_eval do
93
+ define_method :_insert do
94
+ insert = values
95
+ end
96
+ def before_create # Have to define the CPK somehow.
97
+ self.tour = 'To'
98
+ self.date = '2004-04-05'
99
+ super
100
+ end
101
+ end
102
+ a = @Artist.new({:name=>'Ar', :concerts_attributes=>[{:playlist=>'Pl'}]})
103
+ @db.sqls.should == []
104
+ a.save
105
+ @db.sqls.should == ["INSERT INTO artists (name) VALUES ('Ar')"]
106
+ insert.should == {:tour=>'To', :date=>'2004-04-05', :artist_id=>1, :playlist=>'Pl'}
107
+ end
108
+
109
+ it "should support creating new objects with specific primary keys if :unmatched_pk => :create is set" do
110
+ @Artist.nested_attributes :albums, :unmatched_pk=>:create
111
+ insert = nil
112
+ @Album.class_eval do
113
+ unrestrict_primary_key
114
+ define_method :_insert do
115
+ insert = values
116
+ end
117
+ end
118
+ a = @Artist.new({:name=>'Ar', :albums_attributes=>[{:id=>7, :name=>'Al'}]})
119
+ @db.sqls.should == []
120
+ a.save
121
+ @db.sqls.should == ["INSERT INTO artists (name) VALUES ('Ar')"]
122
+ insert.should == {:artist_id=>1, :name=>'Al', :id=>7}
123
+ end
124
+
125
+ it "should support creating new objects with specific composite primary keys if :unmatched_pk => :create is set" do
126
+ insert = nil
127
+ @Artist.nested_attributes :concerts, :unmatched_pk=>:create
128
+ @Concert.class_eval do
129
+ define_method :_insert do
130
+ insert = values
131
+ end
132
+ end
133
+ a = @Artist.new({:name=>'Ar', :concerts_attributes=>[{:tour=>'To', :date=>'2004-04-05', :playlist=>'Pl'}]})
134
+ @db.sqls.should == []
135
+ a.save
136
+ @db.sqls.should == ["INSERT INTO artists (name) VALUES ('Ar')"]
137
+ insert.should == {:tour=>'To', :date=>'2004-04-05', :artist_id=>1, :playlist=>'Pl'}
138
+ end
139
+
83
140
  it "should support updating many_to_one objects" do
84
141
  al = @Album.load(:id=>10, :name=>'Al')
85
142
  ar = @Artist.load(:id=>20, :name=>'Ar')
@@ -140,6 +197,16 @@ describe "NestedAttributes plugin" do
140
197
  @db.sqls.should == ["UPDATE albums SET name = 'Al' WHERE (id = 10)", "UPDATE tags SET name = 'T2' WHERE (id = 20)"]
141
198
  end
142
199
 
200
+ it "should support updating objects with composite primary keys" do
201
+ ar = @Artist.load(:id=>10, :name=>'Ar')
202
+ co = @Concert.load(:tour=>'To', :date=>'2004-04-05', :playlist=>'Pl')
203
+ ar.associations[:concerts] = [co]
204
+ ar.set(:concerts_attributes=>[{:tour=>'To', :date=>'2004-04-05', :playlist=>'Pl2'}])
205
+ @db.sqls.should == []
206
+ ar.save
207
+ check_sql_array("UPDATE artists SET name = 'Ar' WHERE (id = 10)", ["UPDATE concerts SET playlist = 'Pl2' WHERE ((tour = 'To') AND (date = '2004-04-05'))", "UPDATE concerts SET playlist = 'Pl2' WHERE ((date = '2004-04-05') AND (tour = 'To'))"])
208
+ end
209
+
143
210
  it "should support removing many_to_one objects" do
144
211
  al = @Album.load(:id=>10, :name=>'Al')
145
212
  ar = @Artist.load(:id=>20, :name=>'Ar')
@@ -183,6 +250,19 @@ describe "NestedAttributes plugin" do
183
250
  @db.sqls.should == ["DELETE FROM at WHERE ((album_id = 10) AND (tag_id = 20))", "UPDATE albums SET name = 'Al' WHERE (id = 10)"]
184
251
  end
185
252
 
253
+ it "should support removing objects with composite primary keys" do
254
+ ar = @Artist.load(:id=>10, :name=>'Ar')
255
+ co = @Concert.load(:tour=>'To', :date=>'2004-04-05', :playlist=>'Pl')
256
+ ar.associations[:concerts] = [co]
257
+ ar.set(:concerts_attributes=>[{:tour=>'To', :date=>'2004-04-05', :_remove=>'t'}])
258
+ @db.sqls.should == []
259
+ @Concert.dataset._fetch = {:id=>1}
260
+ ar.save
261
+ check_sql_array(["SELECT 1 AS one FROM concerts WHERE ((concerts.artist_id = 10) AND (tour = 'To') AND (date = '2004-04-05')) LIMIT 1", "SELECT 1 AS one FROM concerts WHERE ((concerts.artist_id = 10) AND (date = '2004-04-05') AND (tour = 'To')) LIMIT 1"],
262
+ ["UPDATE concerts SET artist_id = NULL, playlist = 'Pl' WHERE ((tour = 'To') AND (date = '2004-04-05'))", "UPDATE concerts SET playlist = 'Pl', artist_id = NULL WHERE ((tour = 'To') AND (date = '2004-04-05'))", "UPDATE concerts SET artist_id = NULL, playlist = 'Pl' WHERE ((date = '2004-04-05') AND (tour = 'To'))", "UPDATE concerts SET playlist = 'Pl', artist_id = NULL WHERE ((date = '2004-04-05') AND (tour = 'To'))"],
263
+ "UPDATE artists SET name = 'Ar' WHERE (id = 10)")
264
+ end
265
+
186
266
  it "should support destroying many_to_one objects" do
187
267
  al = @Album.load(:id=>10, :name=>'Al')
188
268
  ar = @Artist.load(:id=>20, :name=>'Ar')
@@ -224,6 +304,16 @@ describe "NestedAttributes plugin" do
224
304
  @db.sqls.should == ["DELETE FROM at WHERE ((album_id = 10) AND (tag_id = 20))", "UPDATE albums SET name = 'Al' WHERE (id = 10)", "DELETE FROM tags WHERE (id = 20)"]
225
305
  end
226
306
 
307
+ it "should support destroying objects with composite primary keys" do
308
+ ar = @Artist.load(:id=>10, :name=>'Ar')
309
+ co = @Concert.load(:tour=>'To', :date=>'2004-04-05', :playlist=>'Pl')
310
+ ar.associations[:concerts] = [co]
311
+ ar.set(:concerts_attributes=>[{:tour=>'To', :date=>'2004-04-05', :_delete=>'t'}])
312
+ @db.sqls.should == []
313
+ ar.save
314
+ check_sql_array("UPDATE artists SET name = 'Ar' WHERE (id = 10)", ["DELETE FROM concerts WHERE ((tour = 'To') AND (date = '2004-04-05'))", "DELETE FROM concerts WHERE ((date = '2004-04-05') AND (tour = 'To'))"])
315
+ end
316
+
227
317
  it "should support both string and symbol keys in nested attribute hashes" do
228
318
  a = @Album.load(:id=>10, :name=>'Al')
229
319
  t = @Tag.load(:id=>20, :name=>'T')
@@ -283,6 +373,36 @@ describe "NestedAttributes plugin" do
283
373
  @db.sqls.should == ["UPDATE artists SET name = 'Ar' WHERE (id = 20)"]
284
374
  end
285
375
 
376
+ it "should not raise an Error if an unmatched primary key is given, if the :unmatched_pk=>:ignore option is used" do
377
+ @Artist.nested_attributes :albums, :unmatched_pk=>:ignore
378
+ al = @Album.load(:id=>10, :name=>'Al')
379
+ ar = @Artist.load(:id=>20, :name=>'Ar')
380
+ ar.associations[:albums] = [al]
381
+ ar.set(:albums_attributes=>[{:id=>30, :_delete=>'t'}])
382
+ @db.sqls.should == []
383
+ ar.save
384
+ @db.sqls.should == ["UPDATE artists SET name = 'Ar' WHERE (id = 20)"]
385
+ end
386
+
387
+ it "should raise an Error if a composite primary key is given in a nested attribute hash, but no matching associated object exists" do
388
+ ar = @Artist.load(:id=>10, :name=>'Ar')
389
+ co = @Concert.load(:tour=>'To', :date=>'2004-04-05', :playlist=>'Pl')
390
+ ar.associations[:concerts] = [co]
391
+ proc{ar.set(:concerts_attributes=>[{:tour=>'To', :date=>'2004-04-04', :_delete=>'t'}])}.should raise_error(Sequel::Error)
392
+ proc{ar.set(:concerts_attributes=>[{:tour=>'To', :date=>'2004-04-05', :_delete=>'t'}])}.should_not raise_error(Sequel::Error)
393
+ end
394
+
395
+ it "should not raise an Error if an unmatched composite primary key is given, if the :strict=>false option is used" do
396
+ @Artist.nested_attributes :concerts, :strict=>false
397
+ ar = @Artist.load(:id=>10, :name=>'Ar')
398
+ co = @Concert.load(:tour=>'To', :date=>'2004-04-05', :playlist=>'Pl')
399
+ ar.associations[:concerts] = [co]
400
+ ar.set(:concerts_attributes=>[{:tour=>'To', :date=>'2004-04-06', :_delete=>'t'}])
401
+ @db.sqls.should == []
402
+ ar.save
403
+ @db.sqls.should == ["UPDATE artists SET name = 'Ar' WHERE (id = 10)"]
404
+ end
405
+
286
406
  it "should not save if nested attribute is not valid and should include nested attribute validation errors in the main object's validation errors" do
287
407
  @Artist.class_eval do
288
408
  def validate
@@ -370,6 +490,19 @@ describe "NestedAttributes plugin" do
370
490
  ["INSERT INTO at (album_id, tag_id) VALUES (1, 4)", "INSERT INTO at (tag_id, album_id) VALUES (4, 1)"])
371
491
  end
372
492
 
493
+ it "should accept a :transform block that returns a changed attributes hash" do
494
+ @Album.nested_attributes :tags, :transform=>proc{|parent, hash| hash[:name] << parent.name; hash }
495
+ a = @Album.new(:name => 'Al')
496
+ a.set(:tags_attributes=>[{:name=>'T'}, {:name=>'T2'}])
497
+ @db.sqls.should == []
498
+ a.save
499
+ check_sql_array("INSERT INTO albums (name) VALUES ('Al')",
500
+ "INSERT INTO tags (name) VALUES ('TAl')",
501
+ ["INSERT INTO at (album_id, tag_id) VALUES (1, 2)", "INSERT INTO at (tag_id, album_id) VALUES (2, 1)"],
502
+ "INSERT INTO tags (name) VALUES ('T2Al')",
503
+ ["INSERT INTO at (album_id, tag_id) VALUES (1, 4)", "INSERT INTO at (tag_id, album_id) VALUES (4, 1)"])
504
+ end
505
+
373
506
  it "should return objects created/modified in the internal methods" do
374
507
  @Album.nested_attributes :tags, :remove=>true, :strict=>false
375
508
  objs = []
@@ -381,7 +514,7 @@ describe "NestedAttributes plugin" do
381
514
  a = @Album.new(:name=>'Al')
382
515
  a.associations[:tags] = [@Tag.load(:id=>6, :name=>'A'), @Tag.load(:id=>7, :name=>'A2')]
383
516
  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}]
384
- 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]]
517
+ objs.should == [[@Tag.load(:id=>6, :name=>'T'), :update], [@Tag.load(:id=>7, :name=>'A2'), :remove], [@Tag.new(:name=>'T3'), :create]]
385
518
  end
386
519
 
387
520
  it "should raise an error if updating modifies the associated objects keys" do
@@ -37,7 +37,7 @@ describe "optimistic_locking plugin" do
37
37
  puts sql
38
38
  end
39
39
  end
40
- @c.dataset._fetch = proc do |sql|
40
+ @c.instance_dataset._fetch = @c.dataset._fetch = proc do |sql|
41
41
  m = h[1].dup
42
42
  v = m.delete(:lock_version)
43
43
  m[lv.to_sym] = v
@@ -95,6 +95,20 @@ describe "optimistic_locking plugin" do
95
95
  proc{p2.update(:name=>'Bob')}.should raise_error(Sequel::Plugins::OptimisticLocking::Error)
96
96
  end
97
97
 
98
+ specify "should work correctly if attempting to refresh and save again after a failed save" do
99
+ p1 = @c[1]
100
+ p2 = @c[1]
101
+ p1.update(:name=>'Jim')
102
+ begin
103
+ p2.update(:name=>'Bob')
104
+ rescue Sequel::Plugins::OptimisticLocking::Error
105
+ p2.refresh
106
+ @c.db.sqls
107
+ proc{p2.update(:name=>'Bob')}.should_not raise_error
108
+ end
109
+ @c.db.sqls.first.should =~ /UPDATE people SET (name = 'Bob', lock_version = 4|lock_version = 4, name = 'Bob') WHERE \(\(id = 1\) AND \(lock_version = 3\)\)/
110
+ end
111
+
98
112
  specify "should increment the lock column when #modified! even if no columns are changed" do
99
113
  p1 = @c[1]
100
114
  p1.modified!
@@ -180,7 +180,7 @@ describe "pg_hstore extension" do
180
180
  @db.schema(:items).map{|e| e[1][:type]}.should == [:integer, :hstore]
181
181
  end
182
182
 
183
- it "should support typecasting of the various array types" do
183
+ it "should support typecasting for the hstore type" do
184
184
  @db.extend @c::DatabaseMethods
185
185
  h = {1=>2}.hstore
186
186
  @db.typecast_value(:hstore, h).should equal(h)
@@ -0,0 +1,44 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ describe "pg_inet extension" do
4
+ ipv6_broken = (IPAddr.new('::1'); false) rescue true
5
+ before do
6
+ @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
7
+ @db.extend(Module.new{def bound_variable_arg(arg, conn) arg end})
8
+ @db.extend(Sequel::Postgres::InetDatabaseMethods)
9
+ end
10
+
11
+ it "should literalize IPAddr v4 instances to strings correctly" do
12
+ @db.literal(IPAddr.new('127.0.0.1')).should == "'127.0.0.1/32'"
13
+ @db.literal(IPAddr.new('127.0.0.0/8')).should == "'127.0.0.0/8'"
14
+ end
15
+
16
+ it "should literalize IPAddr v6 instances to strings correctly" do
17
+ @db.literal(IPAddr.new('2001:4f8:3:ba::/64')).should == "'2001:4f8:3:ba::/64'"
18
+ @db.literal(IPAddr.new('2001:4f8:3:ba:2e0:81ff:fe22:d1f1')).should == "'2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128'"
19
+ end unless ipv6_broken
20
+
21
+ it "should not affect literalization of custom objects" do
22
+ o = Object.new
23
+ def o.sql_literal(ds) 'v' end
24
+ @db.literal(o).should == 'v'
25
+ end
26
+
27
+ it "should support using IPAddr as bound variables" do
28
+ @db.bound_variable_arg(1, nil).should == 1
29
+ @db.bound_variable_arg(IPAddr.new('127.0.0.1'), nil).should == '127.0.0.1/32'
30
+ end
31
+
32
+ it "should parse inet/cidr type from the schema correctly" do
33
+ @db.fetch = [{:name=>'id', :db_type=>'integer'}, {:name=>'i', :db_type=>'inet'}, {:name=>'c', :db_type=>'cidr'}]
34
+ @db.schema(:items).map{|e| e[1][:type]}.should == [:integer, :ipaddr, :ipaddr]
35
+ end
36
+
37
+ it "should support typecasting for the ipaddr type" do
38
+ ip = IPAddr.new('127.0.0.1')
39
+ @db.typecast_value(:ipaddr, ip).should equal(ip)
40
+ @db.typecast_value(:ipaddr, ip.to_s).should == ip
41
+ proc{@db.typecast_value(:ipaddr, '')}.should raise_error(Sequel::InvalidValue)
42
+ proc{@db.typecast_value(:ipaddr, 1)}.should raise_error(Sequel::InvalidValue)
43
+ end
44
+ end
@@ -0,0 +1,101 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ begin
4
+ Sequel.extension :pg_json
5
+ rescue LoadError => e
6
+ skip_warn "can't load pg_json extension (#{e.class}: #{e})"
7
+ else
8
+ describe "pg_json extension" do
9
+ before(:all) do
10
+ m = Sequel::Postgres
11
+ @m = m::JSONDatabaseMethods
12
+ @hc = m::JSONHash
13
+ @ac = m::JSONArray
14
+
15
+ # Create subclass in correct namespace for easily overriding methods
16
+ j = m::JSON = JSON.dup
17
+ j.instance_eval do
18
+ Parser = JSON::Parser
19
+ alias old_parse parse
20
+ def parse(s)
21
+ return 1 if s == '1'
22
+ old_parse(s)
23
+ end
24
+ end
25
+ end
26
+ before do
27
+ @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
28
+ @db.extend(Module.new{def bound_variable_arg(arg, conn) arg end})
29
+ end
30
+
31
+ it "should parse json strings correctly" do
32
+ @m.parse_json('[]').should be_a_kind_of(@ac)
33
+ @m.parse_json('[]').to_a.should == []
34
+ @m.parse_json('[1]').to_a.should == [1]
35
+ @m.parse_json('[1, 2]').to_a.should == [1, 2]
36
+ @m.parse_json('[1, [2], {"a": "b"}]').to_a.should == [1, [2], {'a'=>'b'}]
37
+ @m.parse_json('{}').should be_a_kind_of(@hc)
38
+ @m.parse_json('{}').to_hash.should == {}
39
+ @m.parse_json('{"a": "b"}').to_hash.should == {'a'=>'b'}
40
+ @m.parse_json('{"a": "b", "c": [1, 2, 3]}').to_hash.should == {'a'=>'b', 'c'=>[1, 2, 3]}
41
+ @m.parse_json('{"a": "b", "c": {"d": "e"}}').to_hash.should == {'a'=>'b', 'c'=>{'d'=>'e'}}
42
+ end
43
+
44
+ it "should raise an error when attempting to parse invalid json" do
45
+ proc{@m.parse_json('')}.should raise_error(Sequel::InvalidValue)
46
+ proc{@m.parse_json('1')}.should raise_error(Sequel::InvalidValue)
47
+ end
48
+
49
+ it "should literalize HStores to strings correctly" do
50
+ @db.literal([].pg_json).should == "'[]'::json"
51
+ @db.literal([1, [2], {'a'=>'b'}].pg_json).should == "'[1,[2],{\"a\":\"b\"}]'::json"
52
+ @db.literal({}.pg_json).should == "'{}'::json"
53
+ @db.literal({'a'=>'b'}.pg_json).should == "'{\"a\":\"b\"}'::json"
54
+ end
55
+
56
+ it "should have Hash#pg_json method for creating JSONHash instances" do
57
+ {}.pg_json.should be_a_kind_of(@hc)
58
+ end
59
+
60
+ it "should have Array#pg_json method for creating JSONArray instances" do
61
+ [].pg_json.should be_a_kind_of(@ac)
62
+ end
63
+
64
+ it "should have JSONHash#to_hash method for getting underlying hash" do
65
+ {}.pg_json.to_hash.should be_a_kind_of(Hash)
66
+ end
67
+
68
+ it "should have JSONArray#to_a method for getting underlying array" do
69
+ [].pg_json.to_a.should be_a_kind_of(Array)
70
+ end
71
+
72
+ it "should support using JSONHash and JSONArray as bound variables" do
73
+ @db.extend @m
74
+ @db.bound_variable_arg(1, nil).should == 1
75
+ @db.bound_variable_arg([1].pg_json, nil).should == '[1]'
76
+ @db.bound_variable_arg({'a'=>'b'}.pg_json, nil).should == '{"a":"b"}'
77
+ end
78
+
79
+ it "should parse json type from the schema correctly" do
80
+ @db.extend @m
81
+ @db.fetch = [{:name=>'id', :db_type=>'integer'}, {:name=>'i', :db_type=>'json'}]
82
+ @db.schema(:items).map{|e| e[1][:type]}.should == [:integer, :json]
83
+ end
84
+
85
+ it "should support typecasting for the json type" do
86
+ @db.extend @m
87
+ h = {1=>2}.pg_json
88
+ a = [1].pg_json
89
+ @db.typecast_value(:json, h).should equal(h)
90
+ @db.typecast_value(:json, h.to_hash).should == h
91
+ @db.typecast_value(:json, h.to_hash).should be_a_kind_of(@hc)
92
+ @db.typecast_value(:json, a).should equal(a)
93
+ @db.typecast_value(:json, a.to_a).should == a
94
+ @db.typecast_value(:json, a.to_a).should be_a_kind_of(@ac)
95
+ @db.typecast_value(:json, '[]').should == [].pg_json
96
+ @db.typecast_value(:json, '{"a": "b"}').should == {"a"=>"b"}.pg_json
97
+ proc{@db.typecast_value(:json, '')}.should raise_error(Sequel::InvalidValue)
98
+ proc{@db.typecast_value(:json, 1)}.should raise_error(Sequel::InvalidValue)
99
+ end
100
+ end
101
+ end