sequel 3.6.0 → 3.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.
@@ -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