sequel 3.35.0 → 3.36.0

Sign up to get free protection for your applications and to get access to all the features.
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