sequel 4.6.0 → 4.7.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/association_basics.rdoc +18 -0
  4. data/doc/migration.rdoc +30 -0
  5. data/doc/release_notes/4.7.0.txt +103 -0
  6. data/doc/security.rdoc +5 -0
  7. data/doc/sql.rdoc +21 -12
  8. data/doc/validations.rdoc +10 -2
  9. data/doc/virtual_rows.rdoc +22 -29
  10. data/lib/sequel/adapters/jdbc.rb +4 -1
  11. data/lib/sequel/adapters/jdbc/h2.rb +5 -0
  12. data/lib/sequel/adapters/odbc.rb +0 -1
  13. data/lib/sequel/adapters/postgres.rb +8 -1
  14. data/lib/sequel/adapters/shared/db2.rb +5 -0
  15. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  16. data/lib/sequel/adapters/shared/oracle.rb +5 -0
  17. data/lib/sequel/adapters/shared/postgres.rb +5 -0
  18. data/lib/sequel/adapters/shared/sqlanywhere.rb +1 -1
  19. data/lib/sequel/adapters/shared/sqlite.rb +6 -1
  20. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +1 -1
  21. data/lib/sequel/database/schema_methods.rb +1 -0
  22. data/lib/sequel/database/transactions.rb +11 -26
  23. data/lib/sequel/dataset/actions.rb +3 -3
  24. data/lib/sequel/dataset/features.rb +5 -0
  25. data/lib/sequel/dataset/sql.rb +16 -1
  26. data/lib/sequel/model/associations.rb +22 -7
  27. data/lib/sequel/model/base.rb +2 -2
  28. data/lib/sequel/plugins/auto_validations.rb +5 -1
  29. data/lib/sequel/plugins/instance_hooks.rb +21 -3
  30. data/lib/sequel/plugins/pg_array_associations.rb +21 -5
  31. data/lib/sequel/plugins/update_or_create.rb +60 -0
  32. data/lib/sequel/plugins/validation_helpers.rb +5 -2
  33. data/lib/sequel/sql.rb +55 -9
  34. data/lib/sequel/version.rb +1 -1
  35. data/spec/adapters/postgres_spec.rb +25 -4
  36. data/spec/core/database_spec.rb +1 -1
  37. data/spec/core/dataset_spec.rb +1 -1
  38. data/spec/core/expression_filters_spec.rb +53 -1
  39. data/spec/extensions/auto_validations_spec.rb +18 -0
  40. data/spec/extensions/instance_hooks_spec.rb +14 -0
  41. data/spec/extensions/pg_array_associations_spec.rb +40 -0
  42. data/spec/extensions/to_dot_spec.rb +1 -1
  43. data/spec/extensions/update_or_create_spec.rb +81 -0
  44. data/spec/extensions/validation_helpers_spec.rb +15 -11
  45. data/spec/integration/associations_test.rb +1 -1
  46. data/spec/integration/database_test.rb +8 -0
  47. data/spec/integration/dataset_test.rb +15 -10
  48. data/spec/integration/type_test.rb +4 -0
  49. data/spec/model/associations_spec.rb +20 -0
  50. data/spec/spec_config.rb +1 -1
  51. metadata +364 -360
@@ -0,0 +1,60 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The update_or_create plugin adds a couple of methods that make it easier
4
+ # to deal with objects which may or may not yet exist in the database.
5
+ # The first method is update_or_create, which updates an object if it
6
+ # exists in the database, or creates the object if it does not.
7
+ #
8
+ # You can call create_or_update with a block:
9
+ #
10
+ # Album.update_or_create(:name=>'Hello') do |album|
11
+ # album.num_copies_sold = 1000
12
+ # end
13
+ #
14
+ # or provide two hashes, with the second one being the attributes
15
+ # to set.
16
+ #
17
+ # Album.update_or_create({:name=>'Hello'}, {:num_copies_sold=>1000})
18
+ #
19
+ # In both cases, this will check the database to find the album with
20
+ # the name "Hello". If such an album exists, it will be updated to set
21
+ # num_copies_sold to 1000. If no such album exists, an album with the
22
+ # name "Hello" and num_copies_sold 1000 will be created.
23
+ #
24
+ # The second method is find_or_new, which returns the object from the
25
+ # database if it exists, or returns a new (unsaved) object if not. It
26
+ # has the same API as update_or_create, and operates identically to
27
+ # update_or_create except that it doesn't persist any changes.
28
+ #
29
+ # Usage:
30
+ #
31
+ # # Make all model subclass support update_or_create
32
+ # Sequel::Model.plugin :update_or_create
33
+ #
34
+ # # Make the Album class support update_or_create
35
+ # Album.plugin :update_or_create
36
+ module UpdateOrCreate
37
+ module ClassMethods
38
+ # Attempt to find an record with the +attrs+, which should be a
39
+ # hash with column symbol keys. If such an record exists, update it
40
+ # with the values given in +set_attrs+. If no such record exists,
41
+ # create a new record with the columns specified by both +attrs+ and
42
+ # +set_attrs+, with the ones in +set_attrs+ taking priority. If
43
+ # a block is given, the object is yielded to the block before the
44
+ # object is saved.
45
+ def update_or_create(attrs, set_attrs=nil, &block)
46
+ find_or_new(attrs, set_attrs, &block).save_changes
47
+ end
48
+
49
+ # Operates the same as +update_or_create+, but returns the objects
50
+ # without persisting changes (no UPDATE/INSERT queries).
51
+ def find_or_new(attrs, set_attrs=nil, &block)
52
+ obj = find(attrs) || new(attrs)
53
+ obj.set(set_attrs) if set_attrs
54
+ yield obj if block_given?
55
+ obj
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -202,6 +202,8 @@ module Sequel
202
202
  # since it can deal with a grouping of multiple attributes.
203
203
  #
204
204
  # Possible Options:
205
+ # :dataset :: The base dataset to use for the unique query, defaults to the
206
+ # model's dataset.
205
207
  # :message :: The message to use (default: 'is already taken')
206
208
  # :only_if_modified :: Only check the uniqueness if the object is new or
207
209
  # one of the columns has been modified.
@@ -231,12 +233,13 @@ module Sequel
231
233
  arr = Array(a)
232
234
  next if arr.any?{|x| errors.on(x)}
233
235
  next if opts[:only_if_modified] && !new? && !arr.any?{|x| changed_columns.include?(x)}
236
+ ds = opts[:dataset] || model.dataset
234
237
  ds = if where
235
- where.call(model.dataset, self, arr)
238
+ where.call(ds, self, arr)
236
239
  else
237
240
  vals = arr.map{|x| send(x)}
238
241
  next if vals.any?{|v| v.nil?}
239
- model.where(arr.zip(vals))
242
+ ds.where(arr.zip(vals))
240
243
  end
241
244
  ds = yield(ds) if block_given?
242
245
  ds = ds.exclude(pk_hash) unless new?
@@ -48,6 +48,17 @@ module Sequel
48
48
  t = now
49
49
  local(t.year, t.month, t.day, hour, minute, second, usec)
50
50
  end
51
+
52
+ # Return a string in HH:MM:SS format representing the time.
53
+ def to_s(*args)
54
+ if args.empty?
55
+ strftime('%H:%M:%S')
56
+ else
57
+ # Superclass may have defined a method that takes a format string,
58
+ # and we shouldn't override in that case.
59
+ super
60
+ end
61
+ end
51
62
  end
52
63
 
53
64
  # The SQL module holds classes whose instances represent SQL fragments.
@@ -1200,6 +1211,10 @@ module Sequel
1200
1211
 
1201
1212
  # Represents an SQL function call.
1202
1213
  class Function < GenericExpression
1214
+ WILDCARD = LiteralString.new('*').freeze
1215
+ DISTINCT = ["DISTINCT ".freeze].freeze
1216
+ COMMA_ARRAY = [LiteralString.new(', ').freeze].freeze
1217
+
1203
1218
  # The SQL function to call
1204
1219
  attr_reader :f
1205
1220
 
@@ -1211,6 +1226,28 @@ module Sequel
1211
1226
  @f, @args = f, args
1212
1227
  end
1213
1228
 
1229
+ # If no arguments are given, return a new function with the wildcard prepended to the arguments.
1230
+ #
1231
+ # Sequel.function(:count).* # count(*)
1232
+ # Sequel.function(:count, 1).* # count(*, 1)
1233
+ def *(ce=(arg=false;nil))
1234
+ if arg == false
1235
+ Function.new(f, WILDCARD, *args)
1236
+ else
1237
+ super(ce)
1238
+ end
1239
+ end
1240
+
1241
+ # Return a new function with DISTINCT before the method arguments.
1242
+ def distinct
1243
+ Function.new(f, PlaceholderLiteralString.new(DISTINCT + COMMA_ARRAY * (args.length-1), args))
1244
+ end
1245
+
1246
+ # Create a WindowFunction using the receiver and the appropriate options for the window.
1247
+ def over(opts=OPTS)
1248
+ WindowFunction.new(self, Window.new(opts))
1249
+ end
1250
+
1214
1251
  to_s_method :function_sql
1215
1252
  end
1216
1253
 
@@ -1245,6 +1282,12 @@ module Sequel
1245
1282
  def initialize(value)
1246
1283
  @value = value
1247
1284
  end
1285
+
1286
+ # Create a Function using this identifier as the functions name, with
1287
+ # the given args.
1288
+ def function(*args)
1289
+ Function.new(self, *args)
1290
+ end
1248
1291
 
1249
1292
  to_s_method :quote_identifier, '@value'
1250
1293
  end
@@ -1399,6 +1442,12 @@ module Sequel
1399
1442
  @table, @column = table, column
1400
1443
  end
1401
1444
 
1445
+ # Create a Function using this identifier as the functions name, with
1446
+ # the given args.
1447
+ def function(*args)
1448
+ Function.new(self, *args)
1449
+ end
1450
+
1402
1451
  to_s_method :qualified_identifier_sql, "@table, @column"
1403
1452
  end
1404
1453
 
@@ -1584,12 +1633,8 @@ module Sequel
1584
1633
  #
1585
1634
  # For a more detailed explanation, see the {Virtual Rows guide}[rdoc-ref:doc/virtual_rows.rdoc].
1586
1635
  class VirtualRow < BasicObject
1587
- WILDCARD = LiteralString.new('*').freeze
1588
1636
  QUESTION_MARK = LiteralString.new('?').freeze
1589
- COMMA_SEPARATOR = LiteralString.new(', ').freeze
1590
1637
  DOUBLE_UNDERSCORE = '__'.freeze
1591
- DISTINCT = ["DISTINCT ".freeze].freeze
1592
- COMMA_ARRAY = [COMMA_SEPARATOR].freeze
1593
1638
 
1594
1639
  include OperatorBuilders
1595
1640
 
@@ -1616,13 +1661,14 @@ module Sequel
1616
1661
  else
1617
1662
  case args.shift
1618
1663
  when :*
1619
- Function.new(m, WILDCARD)
1664
+ Function.new(m, *args).*
1620
1665
  when :distinct
1621
- Function.new(m, PlaceholderLiteralString.new(DISTINCT + COMMA_ARRAY * (args.length-1), args))
1666
+ Function.new(m, *args).distinct
1622
1667
  when :over
1623
- opts = args.shift || {}
1624
- fun_args = ::Kernel.Array(opts[:*] ? WILDCARD : opts[:args])
1625
- WindowFunction.new(Function.new(m, *fun_args), Window.new(opts))
1668
+ opts = args.shift || OPTS
1669
+ f = Function.new(m, *::Kernel.Array(opts[:args]))
1670
+ f = f.* if opts[:*]
1671
+ f.over(opts)
1626
1672
  else
1627
1673
  Kernel.raise(Error, 'unsupported VirtualRow method argument used with block')
1628
1674
  end
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 4
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 6
6
+ MINOR = 7
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0
@@ -170,6 +170,16 @@ describe "A PostgreSQL database" do
170
170
  @db.server_version.should > 70000
171
171
  end
172
172
 
173
+ specify "should support functions with and without quoting" do
174
+ ds = @db[:public__testfk]
175
+ ds.insert
176
+ ds.get{sum(1)}.should == 1
177
+ ds.get{Sequel.function('pg_catalog.sum', 1)}.should == 1
178
+ ds.get{sum.function(1)}.should == 1
179
+ ds.get{pg_catalog__sum.function(1)}.should == 1
180
+ ds.delete
181
+ end
182
+
173
183
  specify "should support a :qualify option to tables and views" do
174
184
  @db.tables(:qualify=>true).should include(Sequel.qualify(:public, :testfk))
175
185
  begin
@@ -1345,13 +1355,13 @@ if DB.dataset.supports_window_functions?
1345
1355
  end
1346
1356
 
1347
1357
  specify "should give correct results for window functions" do
1348
- @ds.window(:win, :partition=>:group_id, :order=>:id).select(:id){sum(:over, :args=>amount, :window=>win){}}.all.should ==
1358
+ @ds.window(:win, :partition=>:group_id, :order=>:id).select(:id){sum(:amount).over(:window=>win)}.all.should ==
1349
1359
  [{:sum=>1, :id=>1}, {:sum=>11, :id=>2}, {:sum=>111, :id=>3}, {:sum=>1000, :id=>4}, {:sum=>11000, :id=>5}, {:sum=>111000, :id=>6}]
1350
- @ds.window(:win, :partition=>:group_id).select(:id){sum(:over, :args=>amount, :window=>win, :order=>id){}}.all.should ==
1360
+ @ds.window(:win, :partition=>:group_id).select(:id){sum(:amount).over(:window=>win, :order=>id)}.all.should ==
1351
1361
  [{:sum=>1, :id=>1}, {:sum=>11, :id=>2}, {:sum=>111, :id=>3}, {:sum=>1000, :id=>4}, {:sum=>11000, :id=>5}, {:sum=>111000, :id=>6}]
1352
- @ds.window(:win, {}).select(:id){sum(:over, :args=>amount, :window=>:win, :order=>id){}}.all.should ==
1362
+ @ds.window(:win, {}).select(:id){sum(:amount).over(:window=>:win, :order=>id)}.all.should ==
1353
1363
  [{:sum=>1, :id=>1}, {:sum=>11, :id=>2}, {:sum=>111, :id=>3}, {:sum=>1111, :id=>4}, {:sum=>11111, :id=>5}, {:sum=>111111, :id=>6}]
1354
- @ds.window(:win, :partition=>:group_id).select(:id){sum(:over, :args=>amount, :window=>:win, :order=>id, :frame=>:all){}}.all.should ==
1364
+ @ds.window(:win, :partition=>:group_id).select(:id){sum(:amount).over(:window=>:win, :order=>id, :frame=>:all)}.all.should ==
1355
1365
  [{:sum=>111, :id=>1}, {:sum=>111, :id=>2}, {:sum=>111, :id=>3}, {:sum=>111000, :id=>4}, {:sum=>111000, :id=>5}, {:sum=>111000, :id=>6}]
1356
1366
  end
1357
1367
  end
@@ -1456,6 +1466,17 @@ if DB.adapter_scheme == :postgres
1456
1466
  @ds.all.should == @ds.use_cursor.all
1457
1467
  end
1458
1468
 
1469
+ specify "should not swallow errors if closing cursor raises an error" do
1470
+ proc do
1471
+ @db.synchronize do |c|
1472
+ @ds.use_cursor.each do |r|
1473
+ @db.run "CLOSE sequel_cursor"
1474
+ raise ArgumentError
1475
+ end
1476
+ end
1477
+ end.should raise_error(ArgumentError)
1478
+ end
1479
+
1459
1480
  specify "should respect the :rows_per_fetch option" do
1460
1481
  @db.sqls.clear
1461
1482
  @ds.use_cursor.all
@@ -825,7 +825,7 @@ shared_examples_for "Database#transaction" do
825
825
  @db.sqls.should == ['BEGIN', 'BEGIN -- test', 'DROP TABLE test;', 'COMMIT -- test', 'COMMIT']
826
826
  end
827
827
 
828
- if (!defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby' or RUBY_ENGINE == 'rbx') and RUBY_VERSION < '1.9'
828
+ if (!defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby' or RUBY_ENGINE == 'rbx') and !RUBY_VERSION.start_with?('1.9')
829
829
  specify "should handle Thread#kill for transactions inside threads" do
830
830
  q = Queue.new
831
831
  q1 = Queue.new
@@ -3582,7 +3582,7 @@ describe "Sequel::Dataset#qualify" do
3582
3582
 
3583
3583
  specify "should handle SQL::WindowFunctions" do
3584
3584
  meta_def(@ds, :supports_window_functions?){true}
3585
- @ds.select{sum(:over, :args=>:a, :partition=>:b, :order=>:c){}}.qualify.sql.should == 'SELECT sum(t.a) OVER (PARTITION BY t.b ORDER BY t.c) FROM t'
3585
+ @ds.select{sum(:a).over(:partition=>:b, :order=>:c)}.qualify.sql.should == 'SELECT sum(t.a) OVER (PARTITION BY t.b ORDER BY t.c) FROM t'
3586
3586
  end
3587
3587
 
3588
3588
  specify "should handle SQL::DelayedEvaluation" do
@@ -419,8 +419,15 @@ describe Sequel::SQL::VirtualRow do
419
419
  @d.l{version{}}.should == 'version()'
420
420
  end
421
421
 
422
- it "should treat methods with a block and a leading argument :* as a function call with the SQL wildcard" do
422
+ it "should treat methods with a block and a leading argument :* as a function call starting with the SQL wildcard" do
423
423
  @d.l{count(:*){}}.should == 'count(*)'
424
+ @d.l{count(:*, 1){}}.should == 'count(*, 1)'
425
+ end
426
+
427
+ it "should support * method on functions to add * as the first argument" do
428
+ @d.l{count{}.*}.should == 'count(*)'
429
+ @d.l{count(1).*}.should == 'count(*, 1)'
430
+ @d.literal(Sequel.expr{count(1) * 2}).should == '(count(1) * 2)'
424
431
  end
425
432
 
426
433
  it "should treat methods with a block and a leading argument :distinct as a function call with DISTINCT and the additional method arguments" do
@@ -428,6 +435,11 @@ describe Sequel::SQL::VirtualRow do
428
435
  @d.l{count(:distinct, column1, column2){}}.should == 'count(DISTINCT "column1", "column2")'
429
436
  end
430
437
 
438
+ it "should support distinct methods on functions to use DISTINCT before the arguments" do
439
+ @d.l{count(column1).distinct}.should == 'count(DISTINCT "column1")'
440
+ @d.l{count(column1, column2).distinct}.should == 'count(DISTINCT "column1", "column2")'
441
+ end
442
+
431
443
  it "should raise an error if an unsupported argument is used with a block" do
432
444
  proc{@d.where{count(:blah){}}}.should raise_error(Sequel::Error)
433
445
  end
@@ -479,6 +491,11 @@ describe Sequel::SQL::VirtualRow do
479
491
  @d.l{count(:over, :* =>true, :partition=>a, :order=>b, :window=>:win, :frame=>:rows){}}.should == 'count(*) OVER ("win" PARTITION BY "a" ORDER BY "b" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)'
480
492
  end
481
493
 
494
+ it "should support over method on functions to create window functions" do
495
+ @d.l{rank{}.over}.should == 'rank() OVER ()'
496
+ @d.l{sum(c).over(:partition=>a, :order=>b, :window=>:win, :frame=>:rows)}.should == 'sum("c") OVER ("win" PARTITION BY "a" ORDER BY "b" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)'
497
+ end
498
+
482
499
  it "should raise an error if window functions are not supported" do
483
500
  class << @d; remove_method :supports_window_functions? end
484
501
  meta_def(@d, :supports_window_functions?){false}
@@ -486,6 +503,25 @@ describe Sequel::SQL::VirtualRow do
486
503
  proc{Sequel.mock.dataset.filter{count(:over, :* =>true, :partition=>a, :order=>b, :window=>:win, :frame=>:rows){}}.sql}.should raise_error(Sequel::Error)
487
504
  end
488
505
 
506
+ it "should support function method on identifiers to create functions" do
507
+ @d.l{rank.function}.should == 'rank()'
508
+ @d.l{sum.function(c)}.should == 'sum("c")'
509
+ @d.l{sum.function(c, 1)}.should == 'sum("c", 1)'
510
+ end
511
+
512
+ it "should support function method on qualified identifiers to create functions" do
513
+ @d.l{sch__rank.function}.should == 'sch.rank()'
514
+ @d.l{sch__sum.function(c)}.should == 'sch.sum("c")'
515
+ @d.l{sch__sum.function(c, 1)}.should == 'sch.sum("c", 1)'
516
+ @d.l{Sequel.qualify(sch__sum, :x__y).function(c, 1)}.should == 'sch.sum.x.y("c", 1)'
517
+ end
518
+
519
+ it "should handle quoted function names" do
520
+ def @d.supports_quoted_function_names?; true; end
521
+ @d.l{rank.function}.should == '"rank"()'
522
+ @d.l{sch__rank.function}.should == '"sch"."rank"()'
523
+ end
524
+
489
525
  it "should deal with classes without requiring :: prefix" do
490
526
  @d.l{date < Date.today}.should == "(\"date\" < '#{Date.today}')"
491
527
  @d.l{date < Sequel::CURRENT_DATE}.should == "(\"date\" < CURRENT_DATE)"
@@ -903,6 +939,22 @@ describe "Sequel::SQLTime" do
903
939
  @db.literal(Sequel::SQLTime.create(1, 2, 3)).should == "'01:02:03.000000'"
904
940
  @db.literal(Sequel::SQLTime.create(1, 2, 3, 500000)).should == "'01:02:03.500000'"
905
941
  end
942
+
943
+ specify "#to_s should include hour, minute, and second by default" do
944
+ Sequel::SQLTime.create(1, 2, 3).to_s.should == "01:02:03"
945
+ Sequel::SQLTime.create(1, 2, 3, 500000).to_s.should == "01:02:03"
946
+ end
947
+
948
+ specify "#to_s should handle arguments with super" do
949
+ t = Sequel::SQLTime.create(1, 2, 3)
950
+ begin
951
+ Time.now.to_s('%F')
952
+ rescue
953
+ proc{t.to_s('%F')}.should raise_error
954
+ else
955
+ proc{t.to_s('%F')}.should_not raise_error
956
+ end
957
+ end
906
958
  end
907
959
 
908
960
  describe "Sequel::SQL::Wrapper" do
@@ -106,6 +106,24 @@ describe "Sequel::Plugins::AutoValidations" do
106
106
  @m.errors.should == {[:name, :num]=>["is already taken"]}
107
107
  end
108
108
 
109
+ it "should work correctly in STI subclasses" do
110
+ @c.plugin(:single_table_inheritance, :num, :model_map=>{1=>@c}, :key_map=>proc{[1, 2]})
111
+ sc = Class.new(@c)
112
+ @m = sc.new
113
+ @m.valid?.should == false
114
+ @m.errors.should == {:d=>["is not present"], :name=>["is not present"]}
115
+
116
+ @m.set(:d=>'/', :num=>'a', :name=>'1')
117
+ @m.valid?.should == false
118
+ @m.errors.should == {:d=>["is not a valid date"], :num=>["is not a valid integer"]}
119
+
120
+ @m.db.sqls
121
+ @m.set(:d=>Date.today, :num=>1)
122
+ @m.valid?.should == false
123
+ @m.errors.should == {[:name, :num]=>["is already taken"]}
124
+ @m.db.sqls.should == ["SELECT count(*) AS count FROM test WHERE ((name = '1') AND (num = 1)) LIMIT 1"]
125
+ end
126
+
109
127
  it "should work correctly when changing the dataset" do
110
128
  @c.set_dataset(@c.db[:foo])
111
129
  @c.new.valid?.should == true
@@ -177,6 +177,20 @@ describe "InstanceHooks plugin" do
177
177
  @r.should == [2, 1, 4, 3]
178
178
  end
179
179
 
180
+ it "should not clear validations hooks on successful save" do
181
+ @x.after_validation_hook{@x.errors.add(:id, 'a') if @x.id == 1; r 1}
182
+ @x.before_validation_hook{r 2}
183
+ @x.save.should == nil
184
+ @r.should == [2, 1]
185
+ @x.save.should == nil
186
+ @r.should == [2, 1, 2, 1]
187
+ @x.id = 2
188
+ @x.save.should == @x
189
+ @r.should == [2, 1, 2, 1, 2, 1]
190
+ @x.save.should == @x
191
+ @r.should == [2, 1, 2, 1, 2, 1]
192
+ end
193
+
180
194
  it "should not allow addition of instance hooks to frozen instances" do
181
195
  @x.after_destroy_hook{r 1}
182
196
  @x.before_destroy_hook{r 2}
@@ -644,4 +644,44 @@ describe Sequel::Model, "pg_array_associations" do
644
644
  @o2.add_artist(@c1.load(:id=>1))
645
645
  DB.sqls.should == ["UPDATE artists SET tag_ids = ARRAY[2]::int8[] WHERE (id = 1)"]
646
646
  end
647
+
648
+ it "should not validate the current/associated object in add_ and remove_ if the :validate=>false option is used" do
649
+ @c1.pg_array_to_many :tags, :clone=>:tags, :validate=>false, :save_after_modify=>true
650
+ @c2.many_to_pg_array :artists, :clone=>:artists, :validate=>false
651
+ a = @c1.load(:id=>1)
652
+ t = @c2.load(:id=>2)
653
+ def a.validate() errors.add(:id, 'foo') end
654
+ a.associations[:tags] = []
655
+ a.add_tag(t).should == t
656
+ a.tags.should == [t]
657
+ a.remove_tag(t).should == t
658
+ a.tags.should == []
659
+
660
+ t.associations[:artists] = []
661
+ t.add_artist(a).should == a
662
+ t.artists.should == [a]
663
+ t.remove_artist(a).should == a
664
+ t.artists.should == []
665
+ end
666
+
667
+ it "should not raise exception in add_ and remove_ if the :raise_on_save_failure=>false option is used" do
668
+ @c1.pg_array_to_many :tags, :clone=>:tags, :raise_on_save_failure=>false, :save_after_modify=>true
669
+ @c2.many_to_pg_array :artists, :clone=>:artists, :raise_on_save_failure=>false
670
+ a = @c1.load(:id=>1)
671
+ t = @c2.load(:id=>2)
672
+ def a.validate() errors.add(:id, 'foo') end
673
+ a.associations[:tags] = []
674
+ a.add_tag(t).should == nil
675
+ a.tags.should == []
676
+ a.associations[:tags] = [t]
677
+ a.remove_tag(t).should == nil
678
+ a.tags.should == [t]
679
+
680
+ t.associations[:artists] = []
681
+ t.add_artist(a).should == nil
682
+ t.artists.should == []
683
+ t.associations[:artists] = [a]
684
+ t.remove_artist(a).should == nil
685
+ t.artists.should == [a]
686
+ end
647
687
  end