sequel 4.6.0 → 4.7.0

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