sequel 3.6.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -153,6 +153,8 @@ module Sequel
153
153
  #
154
154
  # Possible Options:
155
155
  # * :message - The message to use (default: 'is already taken')
156
+ # * :only_if_modified - Only check the uniqueness if the object is new or
157
+ # one of the columns has been modified.
156
158
  def validates_unique(*atts)
157
159
  opts = default_validation_helpers_options(:unique)
158
160
  if atts.last.is_a?(Hash)
@@ -160,9 +162,12 @@ module Sequel
160
162
  end
161
163
  message = validation_error_message(opts[:message])
162
164
  atts.each do |a|
163
- ds = model.filter(Array(a).map{|x| [x, send(x)]})
165
+ arr = Array(a)
166
+ next if opts[:only_if_modified] && !new? && !arr.any?{|x| changed_columns.include?(x)}
167
+ ds = model.filter(arr.map{|x| [x, send(x)]})
164
168
  ds = yield(ds) if block_given?
165
- errors.add(a, message) unless (new? ? ds : ds.exclude(pk_hash)).count == 0
169
+ ds = ds.exclude(pk_hash) unless new?
170
+ errors.add(a, message) unless ds.count == 0
166
171
  end
167
172
  end
168
173
 
data/lib/sequel/sql.rb CHANGED
@@ -446,6 +446,10 @@ module Sequel
446
446
  new(:AND, new(:>=, l, r.begin), new(r.exclude_end? ? :< : :<=, l, r.end))
447
447
  when Array, ::Sequel::Dataset, SQLArray
448
448
  new(:IN, l, r)
449
+ when NegativeBooleanConstant
450
+ new(:"IS NOT", l, r.constant)
451
+ when BooleanConstant
452
+ new(:IS, l, r.constant)
449
453
  when NilClass, TrueClass, FalseClass
450
454
  new(:IS, l, r)
451
455
  when Regexp
@@ -553,6 +557,20 @@ module Sequel
553
557
 
554
558
  to_s_method :constant_sql, '@constant'
555
559
  end
560
+
561
+ # Represents boolean constants such as NULL, NOTNULL, TRUE, and FALSE.
562
+ class BooleanConstant < Constant
563
+ # The underlying constant related for this object.
564
+ attr_reader :constant
565
+
566
+ to_s_method :boolean_constant_sql, '@constant'
567
+ end
568
+
569
+ # Represents inverse boolean constants (currently only NOTNULL). A
570
+ # special class to allow for special behavior
571
+ class NegativeBooleanConstant < BooleanConstant
572
+ to_s_method :negative_boolean_constant_sql, '@constant'
573
+ end
556
574
 
557
575
  # Holds default generic constants that can be referenced. These
558
576
  # are included in the Sequel top level module and are also available
@@ -562,6 +580,10 @@ module Sequel
562
580
  CURRENT_DATE = Constant.new(:CURRENT_DATE)
563
581
  CURRENT_TIME = Constant.new(:CURRENT_TIME)
564
582
  CURRENT_TIMESTAMP = Constant.new(:CURRENT_TIMESTAMP)
583
+ SQLTRUE = TRUE = BooleanConstant.new(true)
584
+ SQLFALSE = FALSE = BooleanConstant.new(false)
585
+ NULL = BooleanConstant.new(nil)
586
+ NOTNULL = NegativeBooleanConstant.new(nil)
565
587
  end
566
588
 
567
589
  # Represents an SQL function call.
@@ -1,6 +1,6 @@
1
1
  module Sequel
2
2
  MAJOR = 3
3
- MINOR = 6
3
+ MINOR = 7
4
4
  TINY = 0
5
5
 
6
6
  VERSION = [MAJOR, MINOR, TINY].join('.')
@@ -147,6 +147,10 @@ context "A PostgreSQL dataset" do
147
147
  @d.lock('EXCLUSIVE'){@d.insert(:name=>'a')}.should == nil
148
148
  POSTGRES_DB.transaction{@d.lock('EXCLUSIVE').should == nil; @d.insert(:name=>'a')}
149
149
  end
150
+
151
+ specify "should raise an error if attempting to update a joined dataset with a single FROM table" do
152
+ proc{POSTGRES_DB[:test].join(:test2, [:name]).update(:name=>'a')}.should raise_error(Sequel::Error, 'Need multiple FROM tables if updating/deleting a dataset with JOINs')
153
+ end
150
154
  end
151
155
 
152
156
  context "A PostgreSQL dataset with a timestamp field" do
@@ -235,6 +239,18 @@ context "A PostgreSQL database" do
235
239
  @db[:posts].order(:a).map(:a).should == [1, 2, 10, 20, 21]
236
240
  end
237
241
 
242
+ specify "should support specifying Integer/Bignum/Fixnum types in primary keys and have them be auto incrementing" do
243
+ @db.create_table(:posts){primary_key :a, :type=>Integer}
244
+ @db[:posts].insert.should == 1
245
+ @db[:posts].insert.should == 2
246
+ @db.create_table!(:posts){primary_key :a, :type=>Fixnum}
247
+ @db[:posts].insert.should == 1
248
+ @db[:posts].insert.should == 2
249
+ @db.create_table!(:posts){primary_key :a, :type=>Bignum}
250
+ @db[:posts].insert.should == 1
251
+ @db[:posts].insert.should == 2
252
+ end
253
+
238
254
  specify "should not raise an error if attempting to resetting the primary key sequence for a table without a primary key" do
239
255
  @db.create_table(:posts){Integer :a}
240
256
  @db.reset_primary_key_sequence(:posts).should == nil
@@ -1489,6 +1489,25 @@ context "Dataset#group_and_count" do
1489
1489
  specify "should format column aliases in the select clause but not in the group clause" do
1490
1490
  @ds.group_and_count(:name___n).sql.should ==
1491
1491
  "SELECT name AS n, count(*) AS count FROM test GROUP BY name ORDER BY count"
1492
+ @ds.group_and_count(:name__n).sql.should ==
1493
+ "SELECT name.n, count(*) AS count FROM test GROUP BY name.n ORDER BY count"
1494
+ end
1495
+
1496
+ specify "should handle identifiers" do
1497
+ @ds.group_and_count(:name___n.identifier).sql.should ==
1498
+ "SELECT name___n, count(*) AS count FROM test GROUP BY name___n ORDER BY count"
1499
+ end
1500
+
1501
+ specify "should handle literal strings" do
1502
+ @ds.group_and_count("name".lit).sql.should ==
1503
+ "SELECT name, count(*) AS count FROM test GROUP BY name ORDER BY count"
1504
+ end
1505
+
1506
+ specify "should handle aliased expressions" do
1507
+ @ds.group_and_count(:name.as(:n)).sql.should ==
1508
+ "SELECT name AS n, count(*) AS count FROM test GROUP BY name ORDER BY count"
1509
+ @ds.group_and_count(:name.identifier.as(:n)).sql.should ==
1510
+ "SELECT name AS n, count(*) AS count FROM test GROUP BY name ORDER BY count"
1492
1511
  end
1493
1512
  end
1494
1513
 
@@ -2095,6 +2114,15 @@ context "Dataset compound operations" do
2095
2114
  "SELECT * FROM (SELECT * FROM b WHERE (z = 2) EXCEPT ALL SELECT * FROM a WHERE (z = 1)) AS t1"
2096
2115
  end
2097
2116
 
2117
+ specify "should support :alias option for specifying identifier" do
2118
+ @a.union(@b, :alias=>:xx).sql.should == \
2119
+ "SELECT * FROM (SELECT * FROM a WHERE (z = 1) UNION SELECT * FROM b WHERE (z = 2)) AS xx"
2120
+ @a.intersect(@b, :alias=>:xx).sql.should == \
2121
+ "SELECT * FROM (SELECT * FROM a WHERE (z = 1) INTERSECT SELECT * FROM b WHERE (z = 2)) AS xx"
2122
+ @a.except(@b, :alias=>:xx).sql.should == \
2123
+ "SELECT * FROM (SELECT * FROM a WHERE (z = 1) EXCEPT SELECT * FROM b WHERE (z = 2)) AS xx"
2124
+ end
2125
+
2098
2126
  specify "should support :from_self=>false option to not wrap the compound in a SELECT * FROM (...)" do
2099
2127
  @b.union(@a, :from_self=>false).sql.should == \
2100
2128
  "SELECT * FROM b WHERE (z = 2) UNION SELECT * FROM a WHERE (z = 1)"
@@ -3239,6 +3267,30 @@ describe Sequel::SQL::Constants do
3239
3267
  @db.literal(Sequel::SQL::Constants::CURRENT_TIMESTAMP) == 'CURRENT_TIMESTAMP'
3240
3268
  @db.literal(Sequel::CURRENT_TIMESTAMP) == 'CURRENT_TIMESTAMP'
3241
3269
  end
3270
+
3271
+ it "should have NULL" do
3272
+ @db.literal(Sequel::SQL::Constants::NULL) == 'NULL'
3273
+ @db.literal(Sequel::NULL) == 'NULL'
3274
+ end
3275
+
3276
+ it "should have NOTNULL" do
3277
+ @db.literal(Sequel::SQL::Constants::NOTNULL) == 'NOT NULL'
3278
+ @db.literal(Sequel::NOTNULL) == 'NOT NULL'
3279
+ end
3280
+
3281
+ it "should have TRUE and SQLTRUE" do
3282
+ @db.literal(Sequel::SQL::Constants::TRUE) == '1'
3283
+ @db.literal(Sequel::TRUE) == '1'
3284
+ @db.literal(Sequel::SQL::Constants::SQLTRUE) == '1'
3285
+ @db.literal(Sequel::SQLTRUE) == '1'
3286
+ end
3287
+
3288
+ it "should have FALSE and SQLFALSE" do
3289
+ @db.literal(Sequel::SQL::Constants::FALSE) == '0'
3290
+ @db.literal(Sequel::FALSE) == '0'
3291
+ @db.literal(Sequel::SQL::Constants::SQLFALSE) == '0'
3292
+ @db.literal(Sequel::SQLFALSE) == '0'
3293
+ end
3242
3294
  end
3243
3295
 
3244
3296
  describe "Sequel timezone support" do
@@ -3385,3 +3437,138 @@ describe "Sequel timezone support" do
3385
3437
  Sequel.typecast_timezone.should == :utc
3386
3438
  end
3387
3439
  end
3440
+
3441
+ context "Sequel::Dataset#select_map" do
3442
+ before do
3443
+ @ds = MockDatabase.new[:t]
3444
+ def @ds.fetch_rows(sql)
3445
+ db << sql
3446
+ yield({:c=>1})
3447
+ yield({:c=>2})
3448
+ end
3449
+ @ds.db.reset
3450
+ end
3451
+
3452
+ specify "should do select and map in one step" do
3453
+ @ds.select_map(:a).should == [1, 2]
3454
+ @ds.db.sqls.should == ['SELECT a FROM t']
3455
+ end
3456
+
3457
+ specify "should handle implicit qualifiers in arguments" do
3458
+ @ds.select_map(:a__b).should == [1, 2]
3459
+ @ds.db.sqls.should == ['SELECT a.b FROM t']
3460
+ end
3461
+
3462
+ specify "should handle implicit aliases in arguments" do
3463
+ @ds.select_map(:a___b).should == [1, 2]
3464
+ @ds.db.sqls.should == ['SELECT a AS b FROM t']
3465
+ end
3466
+
3467
+ specify "should handle other objects" do
3468
+ @ds.select_map("a".lit.as(:b)).should == [1, 2]
3469
+ @ds.db.sqls.should == ['SELECT a AS b FROM t']
3470
+ end
3471
+
3472
+ specify "should accept a block" do
3473
+ @ds.select_map{a(t__c)}.should == [1, 2]
3474
+ @ds.db.sqls.should == ['SELECT a(t.c) FROM t']
3475
+ end
3476
+ end
3477
+
3478
+ context "Sequel::Dataset#select_order_map" do
3479
+ before do
3480
+ @ds = MockDatabase.new[:t]
3481
+ def @ds.fetch_rows(sql)
3482
+ db << sql
3483
+ yield({:c=>1})
3484
+ yield({:c=>2})
3485
+ end
3486
+ @ds.db.reset
3487
+ end
3488
+
3489
+ specify "should do select and map in one step" do
3490
+ @ds.select_order_map(:a).should == [1, 2]
3491
+ @ds.db.sqls.should == ['SELECT a FROM t ORDER BY a']
3492
+ end
3493
+
3494
+ specify "should handle implicit qualifiers in arguments" do
3495
+ @ds.select_order_map(:a__b).should == [1, 2]
3496
+ @ds.db.sqls.should == ['SELECT a.b FROM t ORDER BY a.b']
3497
+ end
3498
+
3499
+ specify "should handle implicit aliases in arguments" do
3500
+ @ds.select_order_map(:a___b).should == [1, 2]
3501
+ @ds.db.sqls.should == ['SELECT a AS b FROM t ORDER BY a']
3502
+ end
3503
+
3504
+ specify "should handle implicit qualifiers and aliases in arguments" do
3505
+ @ds.select_order_map(:t__a___b).should == [1, 2]
3506
+ @ds.db.sqls.should == ['SELECT t.a AS b FROM t ORDER BY t.a']
3507
+ end
3508
+
3509
+ specify "should handle AliasedExpressions" do
3510
+ @ds.select_order_map("a".lit.as(:b)).should == [1, 2]
3511
+ @ds.db.sqls.should == ['SELECT a AS b FROM t ORDER BY a']
3512
+ end
3513
+
3514
+ specify "should accept a block" do
3515
+ @ds.select_order_map{a(t__c)}.should == [1, 2]
3516
+ @ds.db.sqls.should == ['SELECT a(t.c) FROM t ORDER BY a(t.c)']
3517
+ end
3518
+ end
3519
+
3520
+ context "Sequel::Dataset#select_hash" do
3521
+ before do
3522
+ @ds = MockDatabase.new[:t]
3523
+ def @ds.set_fr_yield(hs)
3524
+ @hs = hs
3525
+ end
3526
+ def @ds.fetch_rows(sql)
3527
+ db << sql
3528
+ @hs.each{|h| yield h}
3529
+ end
3530
+ @ds.db.reset
3531
+ end
3532
+
3533
+ specify "should do select and map in one step" do
3534
+ @ds.set_fr_yield([{:a=>1, :b=>2}, {:a=>3, :b=>4}])
3535
+ @ds.select_hash(:a, :b).should == {1=>2, 3=>4}
3536
+ @ds.db.sqls.should == ['SELECT a, b FROM t']
3537
+ end
3538
+
3539
+ specify "should handle implicit qualifiers in arguments" do
3540
+ @ds.set_fr_yield([{:a=>1, :b=>2}, {:a=>3, :b=>4}])
3541
+ @ds.select_hash(:t__a, :t__b).should == {1=>2, 3=>4}
3542
+ @ds.db.sqls.should == ['SELECT t.a, t.b FROM t']
3543
+ end
3544
+
3545
+ specify "should handle implicit aliases in arguments" do
3546
+ @ds.set_fr_yield([{:a=>1, :b=>2}, {:a=>3, :b=>4}])
3547
+ @ds.select_hash(:c___a, :d___b).should == {1=>2, 3=>4}
3548
+ @ds.db.sqls.should == ['SELECT c AS a, d AS b FROM t']
3549
+ end
3550
+
3551
+ specify "should handle implicit qualifiers and aliases in arguments" do
3552
+ @ds.set_fr_yield([{:a=>1, :b=>2}, {:a=>3, :b=>4}])
3553
+ @ds.select_hash(:t__c___a, :t__d___b).should == {1=>2, 3=>4}
3554
+ @ds.db.sqls.should == ['SELECT t.c AS a, t.d AS b FROM t']
3555
+ end
3556
+ end
3557
+
3558
+ context "Modifying joined datasets" do
3559
+ before do
3560
+ @ds = MockDatabase.new.from(:b, :c).join(:d, [:id]).where(:id => 2)
3561
+ @ds.meta_def(:supports_modifying_joins?){true}
3562
+ @ds.db.reset
3563
+ end
3564
+
3565
+ specify "should allow deleting from joined datasets" do
3566
+ @ds.delete
3567
+ @ds.db.sqls.should == ['DELETE FROM b, c WHERE (id = 2)']
3568
+ end
3569
+
3570
+ specify "should allow updating joined datasets" do
3571
+ @ds.update(:a=>1)
3572
+ @ds.db.sqls.should == ['UPDATE b, c INNER JOIN d USING (id) SET a = 1 WHERE (id = 2)']
3573
+ end
3574
+ end
@@ -399,6 +399,31 @@ context "Blockless Ruby Filters" do
399
399
  y.lit.should == y
400
400
  end
401
401
 
402
+ it "should return have .sql_literal operate like .to_s" do
403
+ y = :x + 1
404
+ y.sql_literal(@d).should == '(x + 1)'
405
+ y.sql_literal(@d).should == y.to_s(@d)
406
+ y.sql_literal(@d).should == @d.literal(y)
407
+ end
408
+
409
+ it "should support SQL::Constants" do
410
+ @d.l({:x => Sequel::NULL}).should == '(x IS NULL)'
411
+ @d.l({:x => Sequel::NOTNULL}).should == '(x IS NOT NULL)'
412
+ @d.l({:x => Sequel::TRUE}).should == '(x IS TRUE)'
413
+ @d.l({:x => Sequel::FALSE}).should == '(x IS FALSE)'
414
+ @d.l({:x => Sequel::SQLTRUE}).should == '(x IS TRUE)'
415
+ @d.l({:x => Sequel::SQLFALSE}).should == '(x IS FALSE)'
416
+ end
417
+
418
+ it "should support negation of SQL::Constants" do
419
+ @d.l(~{:x => Sequel::NULL}).should == '(x IS NOT NULL)'
420
+ @d.l(~{:x => Sequel::NOTNULL}).should == '(x IS NULL)'
421
+ @d.l(~{:x => Sequel::TRUE}).should == '(x IS NOT TRUE)'
422
+ @d.l(~{:x => Sequel::FALSE}).should == '(x IS NOT FALSE)'
423
+ @d.l(~{:x => Sequel::SQLTRUE}).should == '(x IS NOT TRUE)'
424
+ @d.l(~{:x => Sequel::SQLFALSE}).should == '(x IS NOT FALSE)'
425
+ end
426
+
402
427
  it "should raise an error if trying to create an invalid complex expression" do
403
428
  proc{Sequel::SQL::ComplexExpression.new(:BANG, 1, 2)}.should raise_error(Sequel::Error)
404
429
  end
@@ -80,6 +80,8 @@ describe "Sequel::Database dump methods" do
80
80
  [:c2, {:db_type=>'datetime', :allow_null=>false}]]
81
81
  when :t5
82
82
  [[:c1, {:db_type=>'blahblah', :allow_null=>true}]]
83
+ when :t6
84
+ [[:c1, {:db_type=>'bigint', :primary_key=>true, :allow_null=>true}]]
83
85
  end
84
86
  end
85
87
  end
@@ -88,6 +90,10 @@ describe "Sequel::Database dump methods" do
88
90
  @d.dump_table_schema(:t1).should == "create_table(:t1) do\n primary_key :c1\n String :c2, :size=>20\nend"
89
91
  end
90
92
 
93
+ it "should dump non-Integer primary key columns with explicit :type" do
94
+ @d.dump_table_schema(:t6).should == "create_table(:t6) do\n primary_key :c1, :type=>Bignum\nend"
95
+ end
96
+
91
97
  it "should use a composite primary_key calls if there is a composite primary key" do
92
98
  @d.dump_table_schema(:t2).should == "create_table(:t2) do\n Integer :c1, :null=>false\n BigDecimal :c2, :null=>false\n \n primary_key [:c1, :c2]\nend"
93
99
  end
@@ -377,4 +377,39 @@ describe "Sequel::Plugins::ValidationHelpers" do
377
377
  MODEL_DB.sqls.should == ["SELECT COUNT(*) AS count FROM items WHERE ((username = '0records') AND active) LIMIT 1",
378
378
  "SELECT COUNT(*) AS count FROM items WHERE (((username = '0records') AND active) AND (id != 3)) LIMIT 1"]
379
379
  end
380
+
381
+ it "should support :only_if_modified option for validates_unique, and not check uniqueness for existing records if values haven't changed" do
382
+ @c.columns(:id, :username, :password)
383
+ @c.set_dataset MODEL_DB[:items]
384
+ @c.set_validations{validates_unique([:username, :password], :only_if_modified=>true)}
385
+
386
+ @c.dataset.extend(Module.new {
387
+ def fetch_rows (sql)
388
+ @db << sql
389
+ yield({:v => 0})
390
+ end
391
+ })
392
+
393
+ MODEL_DB.reset
394
+ @c.new(:username => "0records", :password => "anothertest").should be_valid
395
+ MODEL_DB.sqls.should == ["SELECT COUNT(*) AS count FROM items WHERE ((username = '0records') AND (password = 'anothertest')) LIMIT 1"]
396
+ MODEL_DB.reset
397
+ m = @c.load(:id=>3, :username => "0records", :password => "anothertest")
398
+ m.should be_valid
399
+ MODEL_DB.sqls.should == []
400
+
401
+ m.username = '1'
402
+ m.should be_valid
403
+ MODEL_DB.sqls.should == ["SELECT COUNT(*) AS count FROM items WHERE (((username = '1') AND (password = 'anothertest')) AND (id != 3)) LIMIT 1"]
404
+
405
+ m = @c.load(:id=>3, :username => "0records", :password => "anothertest")
406
+ MODEL_DB.reset
407
+ m.password = '1'
408
+ m.should be_valid
409
+ MODEL_DB.sqls.should == ["SELECT COUNT(*) AS count FROM items WHERE (((username = '0records') AND (password = '1')) AND (id != 3)) LIMIT 1"]
410
+ MODEL_DB.reset
411
+ m.username = '2'
412
+ m.should be_valid
413
+ MODEL_DB.sqls.should == ["SELECT COUNT(*) AS count FROM items WHERE (((username = '2') AND (password = '1')) AND (id != 3)) LIMIT 1"]
414
+ end
380
415
  end
@@ -870,3 +870,40 @@ describe "Dataset defaults and overrides" do
870
870
  @ds.all.should == [{:a=>10}, {:a=>10}]
871
871
  end
872
872
  end
873
+
874
+ if INTEGRATION_DB.dataset.supports_modifying_joins?
875
+ describe "Modifying joined datasets" do
876
+ before do
877
+ @db = INTEGRATION_DB
878
+ @db.create_table!(:a){Integer :a; Integer :d}
879
+ @db.create_table!(:b){Integer :b; Integer :e}
880
+ @db.create_table!(:c){Integer :c; Integer :f}
881
+ @ds = @db.from(:a, :b).join(:c, :c=>:e.identifier).where(:d=>:b, :f=>6)
882
+ @db[:a].insert(1, 2)
883
+ @db[:a].insert(3, 4)
884
+ @db[:b].insert(2, 5)
885
+ @db[:c].insert(5, 6)
886
+ @db[:b].insert(4, 7)
887
+ @db[:c].insert(7, 8)
888
+ end
889
+ after do
890
+ @db.drop_table(:a, :b, :c)
891
+ end
892
+
893
+ it "#update should allow updating joined datasets" do
894
+ @ds.update(:a=>10)
895
+ @ds.all.should == [{:c=>5, :b=>2, :a=>10, :d=>2, :e=>5, :f=>6}]
896
+ @db[:a].order(:a).all.should == [{:a=>3, :d=>4}, {:a=>10, :d=>2}]
897
+ @db[:b].order(:b).all.should == [{:b=>2, :e=>5}, {:b=>4, :e=>7}]
898
+ @db[:c].order(:c).all.should == [{:c=>5, :f=>6}, {:c=>7, :f=>8}]
899
+ end
900
+
901
+ it "#delete should allow deleting from joined datasets" do
902
+ @ds.delete
903
+ @ds.all.should == []
904
+ @db[:a].order(:a).all.should == [{:a=>3, :d=>4}]
905
+ @db[:b].order(:b).all.should == [{:b=>2, :e=>5}, {:b=>4, :e=>7}]
906
+ @db[:c].order(:c).all.should == [{:c=>5, :f=>6}, {:c=>7, :f=>8}]
907
+ end
908
+ end
909
+ end