sequel 3.39.0 → 3.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/CHANGELOG +30 -0
  2. data/README.rdoc +4 -3
  3. data/doc/active_record.rdoc +1 -1
  4. data/doc/opening_databases.rdoc +7 -0
  5. data/doc/release_notes/3.40.0.txt +73 -0
  6. data/lib/sequel/adapters/ado.rb +29 -3
  7. data/lib/sequel/adapters/ado/access.rb +334 -0
  8. data/lib/sequel/adapters/ado/mssql.rb +0 -6
  9. data/lib/sequel/adapters/cubrid.rb +143 -0
  10. data/lib/sequel/adapters/jdbc.rb +26 -18
  11. data/lib/sequel/adapters/jdbc/cubrid.rb +52 -0
  12. data/lib/sequel/adapters/jdbc/derby.rb +7 -7
  13. data/lib/sequel/adapters/jdbc/hsqldb.rb +5 -0
  14. data/lib/sequel/adapters/jdbc/mysql.rb +9 -4
  15. data/lib/sequel/adapters/mysql.rb +0 -3
  16. data/lib/sequel/adapters/mysql2.rb +0 -3
  17. data/lib/sequel/adapters/oracle.rb +4 -1
  18. data/lib/sequel/adapters/postgres.rb +4 -4
  19. data/lib/sequel/adapters/shared/access.rb +205 -3
  20. data/lib/sequel/adapters/shared/cubrid.rb +216 -0
  21. data/lib/sequel/adapters/shared/db2.rb +7 -2
  22. data/lib/sequel/adapters/shared/mssql.rb +3 -34
  23. data/lib/sequel/adapters/shared/mysql.rb +4 -33
  24. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +11 -0
  25. data/lib/sequel/adapters/shared/oracle.rb +5 -0
  26. data/lib/sequel/adapters/shared/postgres.rb +2 -1
  27. data/lib/sequel/adapters/utils/split_alter_table.rb +36 -0
  28. data/lib/sequel/database/connecting.rb +1 -1
  29. data/lib/sequel/database/query.rb +30 -7
  30. data/lib/sequel/database/schema_methods.rb +7 -2
  31. data/lib/sequel/dataset/query.rb +9 -10
  32. data/lib/sequel/dataset/sql.rb +14 -26
  33. data/lib/sequel/extensions/pg_hstore.rb +19 -0
  34. data/lib/sequel/extensions/pg_row.rb +5 -5
  35. data/lib/sequel/plugins/association_pks.rb +121 -18
  36. data/lib/sequel/plugins/json_serializer.rb +19 -0
  37. data/lib/sequel/sql.rb +11 -0
  38. data/lib/sequel/version.rb +1 -1
  39. data/spec/adapters/postgres_spec.rb +42 -0
  40. data/spec/core/database_spec.rb +17 -0
  41. data/spec/core/dataset_spec.rb +11 -0
  42. data/spec/core/expression_filters_spec.rb +13 -0
  43. data/spec/extensions/association_pks_spec.rb +163 -3
  44. data/spec/extensions/pg_hstore_spec.rb +6 -0
  45. data/spec/extensions/pg_row_spec.rb +17 -0
  46. data/spec/integration/associations_test.rb +1 -1
  47. data/spec/integration/dataset_test.rb +13 -13
  48. data/spec/integration/plugin_test.rb +232 -7
  49. data/spec/integration/schema_test.rb +8 -12
  50. data/spec/integration/spec_helper.rb +1 -1
  51. data/spec/integration/type_test.rb +6 -0
  52. metadata +9 -2
@@ -61,6 +61,25 @@ module Sequel
61
61
  #
62
62
  # Album.to_json(:array=>[Album[1], Album[2]])
63
63
  #
64
+ # Note that active_support/json makes incompatible changes to the to_json API,
65
+ # and breaks some aspects of the json_serializer plugin. You can undo the damage
66
+ # done by active_support/json by doing:
67
+ #
68
+ # class Array
69
+ # def to_json(options = {})
70
+ # JSON.generate(self)
71
+ # end
72
+ # end
73
+ #
74
+ # class Hash
75
+ # def to_json(options = {})
76
+ # JSON.generate(self)
77
+ # end
78
+ # end
79
+ #
80
+ # Note that this will probably cause active_support/json to no longer work
81
+ # correctly in some cases.
82
+ #
64
83
  # Usage:
65
84
  #
66
85
  # # Add JSON output capability to all model subclass instances (called before loading subclasses)
data/lib/sequel/sql.rb CHANGED
@@ -1030,6 +1030,17 @@ module Sequel
1030
1030
  !@no_expression
1031
1031
  end
1032
1032
 
1033
+ # Merge the CASE expression into the conditions, useful for databases that
1034
+ # don't support CASE expressions.
1035
+ def with_merged_expression
1036
+ if expression?
1037
+ e = expression
1038
+ CaseExpression.new(conditions.map{|c, r| [::Sequel::SQL::BooleanExpression.new(:'=', e, c), r]}, default)
1039
+ else
1040
+ self
1041
+ end
1042
+ end
1043
+
1033
1044
  to_s_method :case_expression_sql
1034
1045
  end
1035
1046
 
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 3
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 39
6
+ MINOR = 40
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
@@ -1320,6 +1320,26 @@ if POSTGRES_DB.adapter_scheme == :postgres && SEQUEL_POSTGRES_USES_PG && POSTGRE
1320
1320
  end.should == 'foo'
1321
1321
  called.should be_true
1322
1322
 
1323
+ # Check weird identifier names
1324
+ called = false
1325
+ @db.listen('FOO bar', :after_listen=>proc{@db.notify('FOO bar')}) do |ev, pid, payload|
1326
+ ev.should == 'FOO bar'
1327
+ pid.should == notify_pid
1328
+ ['', nil].should include(payload)
1329
+ called = true
1330
+ end.should == 'FOO bar'
1331
+ called.should be_true
1332
+
1333
+ # Check identifier symbols
1334
+ called = false
1335
+ @db.listen(:foo, :after_listen=>proc{@db.notify(:foo)}) do |ev, pid, payload|
1336
+ ev.should == 'foo'
1337
+ pid.should == notify_pid
1338
+ ['', nil].should include(payload)
1339
+ called = true
1340
+ end.should == 'foo'
1341
+ called.should be_true
1342
+
1323
1343
  called = false
1324
1344
  @db.listen('foo', :after_listen=>proc{@db.notify('foo', :payload=>'bar')}) do |ev, pid, payload|
1325
1345
  ev.should == 'foo'
@@ -1592,6 +1612,11 @@ describe 'PostgreSQL array handling' do
1592
1612
  @ds.filter(:i=>:$i).call(:first, :i=>[1,2]).should == {:i=>[1,2]}
1593
1613
  @ds.filter(:i=>:$i).call(:first, :i=>[1,3]).should == nil
1594
1614
 
1615
+ # NULL values
1616
+ @ds.delete
1617
+ @ds.call(:insert, {:i=>[nil,nil]}, {:i=>:$i})
1618
+ @ds.first.should == {:i=>[nil, nil]}
1619
+
1595
1620
  @db.create_table!(:items) do
1596
1621
  column :i, 'text[]'
1597
1622
  end
@@ -1751,6 +1776,10 @@ describe 'PostgreSQL hstore handling' do
1751
1776
  @ds.filter(:i=>:$i).call(:first, :i=>Sequel.hstore(@h)).should == {:i=>@h}
1752
1777
  @ds.filter(:i=>:$i).call(:first, :i=>Sequel.hstore({})).should == nil
1753
1778
 
1779
+ @ds.delete
1780
+ @ds.call(:insert, {:i=>Sequel.hstore('a'=>nil)}, {:i=>:$i})
1781
+ @ds.get(:i).should == Sequel.hstore('a'=>nil)
1782
+
1754
1783
  @ds.delete
1755
1784
  @ds.call(:insert, {:i=>@h}, {:i=>:$i})
1756
1785
  @ds.get(:i).should == @h
@@ -2014,6 +2043,10 @@ describe 'PostgreSQL json type' do
2014
2043
  @ds.filter(Sequel.cast(:i, String)=>:$i).call(:first, :i=>Sequel.pg_json(@a)).should == {:i=>@a}
2015
2044
  @ds.filter(Sequel.cast(:i, String)=>:$i).call(:first, :i=>Sequel.pg_json([])).should == nil
2016
2045
 
2046
+ @ds.delete
2047
+ @ds.call(:insert, {:i=>Sequel.pg_json('a'=>nil)}, {:i=>:$i})
2048
+ @ds.get(:i).should == Sequel.pg_json('a'=>nil)
2049
+
2017
2050
  @db.create_table!(:items){column :i, 'json[]'}
2018
2051
  j = Sequel.pg_array([Sequel.pg_json('a'=>1), Sequel.pg_json(['b', 2])], :text)
2019
2052
  @ds.call(:insert, {:i=>j}, {:i=>:$i})
@@ -2499,6 +2532,10 @@ describe 'PostgreSQL row-valued/composite types' do
2499
2532
  @ds.get(:address).should == {:street=>'123 Sesame St', :city=>'Somewhere', :zip=>'12345'}
2500
2533
  @ds.filter(:address=>Sequel.cast(:$address, :address)).call(:first, :address=>Sequel.pg_row(['123 Sesame St', 'Somewhere', '12345']))[:id].should == 1
2501
2534
  @ds.filter(:address=>Sequel.cast(:$address, :address)).call(:first, :address=>Sequel.pg_row(['123 Sesame St', 'Somewhere', '12356'])).should == nil
2535
+
2536
+ @ds.delete
2537
+ @ds.call(:insert, {:address=>Sequel.pg_row([nil, nil, nil])}, {:address=>:$address, :id=>1})
2538
+ @ds.get(:address).should == {:street=>nil, :city=>nil, :zip=>nil}
2502
2539
  end if POSTGRES_DB.adapter_scheme == :postgres && SEQUEL_POSTGRES_USES_PG
2503
2540
 
2504
2541
  specify 'use arrays of row types in bound variables' do
@@ -2507,6 +2544,11 @@ describe 'PostgreSQL row-valued/composite types' do
2507
2544
  @ds.get(:company).should == {:id=>1, :employees=>[{:id=>1, :address=>{:street=>'123 Sesame St', :city=>'Somewhere', :zip=>'12345'}}]}
2508
2545
  @ds.filter(:employees=>Sequel.cast(:$employees, 'person[]')).call(:first, :employees=>Sequel.pg_array([@db.row_type(:person, [1, Sequel.pg_row(['123 Sesame St', 'Somewhere', '12345'])])]))[:id].should == 1
2509
2546
  @ds.filter(:employees=>Sequel.cast(:$employees, 'person[]')).call(:first, :employees=>Sequel.pg_array([@db.row_type(:person, [1, Sequel.pg_row(['123 Sesame St', 'Somewhere', '12356'])])])).should == nil
2547
+
2548
+
2549
+ @ds.delete
2550
+ @ds.call(:insert, {:employees=>Sequel.pg_array([@db.row_type(:person, [1, Sequel.pg_row([nil, nil, nil])])])}, {:employees=>:$employees, :id=>1})
2551
+ @ds.get(:employees).should == [{:address=>{:city=>nil, :zip=>nil, :street=>nil}, :id=>1}]
2510
2552
  end if POSTGRES_DB.adapter_scheme == :postgres && SEQUEL_POSTGRES_USES_PG
2511
2553
 
2512
2554
  specify 'operations/functions with pg_row_ops' do
@@ -617,6 +617,18 @@ shared_examples_for "Database#transaction" do
617
617
  'BEGIN', 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE', 'DROP TABLE serializable', 'COMMIT']
618
618
  end
619
619
 
620
+ specify "should support :disconnect=>:retry option for automatically retrying on disconnect" do
621
+ a = []
622
+ @db.transaction(:disconnect=>:retry){a << 1; raise Sequel::DatabaseDisconnectError if a.length < 2}
623
+ @db.sqls.should == ['BEGIN', 'ROLLBACK', 'BEGIN', 'COMMIT']
624
+ a.should == [1, 1]
625
+ end
626
+
627
+ specify "should raise an error if attempting to use :disconnect=>:retry inside another transaction" do
628
+ proc{@db.transaction{@db.transaction(:disconnect=>:retry){}}}.should raise_error(Sequel::Error)
629
+ @db.sqls.should == ['BEGIN', 'ROLLBACK']
630
+ end
631
+
620
632
  specify "should handle returning inside of the block by committing" do
621
633
  def @db.ret_commit
622
634
  transaction do
@@ -2067,12 +2079,16 @@ describe "Database#column_schema_to_ruby_default" do
2067
2079
  db = Sequel::Database.new
2068
2080
  p = lambda{|d,t| db.send(:column_schema_to_ruby_default, d, t)}
2069
2081
  p[nil, :integer].should be_nil
2082
+ p[1, :integer].should == 1
2070
2083
  p['1', :integer].should == 1
2071
2084
  p['-1', :integer].should == -1
2085
+ p[1.0, :float].should == 1.0
2072
2086
  p['1.0', :float].should == 1.0
2073
2087
  p['-1.0', :float].should == -1.0
2074
2088
  p['1.0', :decimal].should == BigDecimal.new('1.0')
2075
2089
  p['-1.0', :decimal].should == BigDecimal.new('-1.0')
2090
+ p[true, :boolean].should == true
2091
+ p[false, :boolean].should == false
2076
2092
  p['1', :boolean].should == true
2077
2093
  p['0', :boolean].should == false
2078
2094
  p['true', :boolean].should == true
@@ -2085,6 +2101,7 @@ describe "Database#column_schema_to_ruby_default" do
2085
2101
  p["''", :string].should == ''
2086
2102
  p["'\\a''b'", :string].should == "\\a'b"
2087
2103
  p["'NULL'", :string].should == "NULL"
2104
+ p[Date.today, :date].should == Date.today
2088
2105
  p["'2009-10-29'", :date].should == Date.new(2009,10,29)
2089
2106
  p["CURRENT_TIMESTAMP", :date].should == Sequel::CURRENT_DATE
2090
2107
  p["CURRENT_DATE", :date].should == Sequel::CURRENT_DATE
@@ -3020,6 +3020,11 @@ describe "Dataset#insert_sql" do
3020
3020
  @ds.insert_sql('c' => 'd').should == "INSERT INTO items (c) VALUES ('d')"
3021
3021
  end
3022
3022
 
3023
+ specify "should quote string keys" do
3024
+ @ds.quote_identifiers = true
3025
+ @ds.insert_sql('c' => 'd').should == "INSERT INTO \"items\" (\"c\") VALUES ('d')"
3026
+ end
3027
+
3023
3028
  specify "should accept array subscript references" do
3024
3029
  @ds.insert_sql((Sequel.subscript(:day, 1)) => 'd').should == "INSERT INTO items (day[1]) VALUES ('d')"
3025
3030
  end
@@ -3666,6 +3671,12 @@ describe "Sequel::Dataset #with and #with_recursive" do
3666
3671
  @ds.with_recursive(:t, @db[:x], @db[:t], :args=>[:b, :c]).sql.should == 'WITH t(b, c) AS (SELECT * FROM x UNION ALL SELECT * FROM t) SELECT * FROM t'
3667
3672
  end
3668
3673
 
3674
+ specify "#with and #with_recursive should quote the columns in the :args option" do
3675
+ @ds.quote_identifiers = true
3676
+ @ds.with(:t, @db[:x], :args=>[:b]).sql.should == 'WITH "t"("b") AS (SELECT * FROM x) SELECT * FROM "t"'
3677
+ @ds.with_recursive(:t, @db[:x], @db[:t], :args=>[:b, :c]).sql.should == 'WITH "t"("b", "c") AS (SELECT * FROM x UNION ALL SELECT * FROM t) SELECT * FROM "t"'
3678
+ end
3679
+
3669
3680
  specify "#with_recursive should take an :union_all=>false option" do
3670
3681
  @ds.with_recursive(:t, @db[:x], @db[:t], :union_all=>false).sql.should == 'WITH t AS (SELECT * FROM x UNION SELECT * FROM t) SELECT * FROM t'
3671
3682
  end
@@ -942,6 +942,19 @@ describe Sequel::SQL::Subscript do
942
942
  end
943
943
  end
944
944
 
945
+ describe Sequel::SQL::CaseExpression, "#with_merged_expression" do
946
+ specify "should return self if it has no expression" do
947
+ c = Sequel.case({1=>0}, 3)
948
+ c.with_merged_expression.should equal(c)
949
+ end
950
+
951
+ specify "should merge expression into conditions if it has an expression" do
952
+ db = Sequel::Database.new
953
+ c = Sequel.case({1=>0}, 3, 4)
954
+ db.literal(c.with_merged_expression).should == db.literal(Sequel.case({{4=>1}=>0}, 3))
955
+ end
956
+ end
957
+
945
958
  describe "Sequel.recursive_map" do
946
959
  specify "should recursively convert an array using a callable" do
947
960
  Sequel.recursive_map(['1'], proc{|s| s.to_i}).should == [1]
@@ -12,16 +12,49 @@ describe "Sequel::Plugins::AssociationPks" do
12
12
  a << {:tag_id=>2} if $1 != '3'
13
13
  a << {:tag_id=>3} if $1 == '2'
14
14
  a
15
+ when "SELECT first, last FROM vocalists WHERE (vocalists.album_id = 1)"
16
+ [{:first=>"F1", :last=>"L1"}, {:first=>"F2", :last=>"L2"}]
17
+ when /SELECT first, last FROM albums_vocalists WHERE \(album_id = (\d)\)/
18
+ a = []
19
+ a << {:first=>"F1", :last=>"L1"} if $1 == '1'
20
+ a << {:first=>"F2", :last=>"L2"} if $1 != '3'
21
+ a << {:first=>"F3", :last=>"L3"} if $1 == '2'
22
+ a
23
+ when "SELECT id FROM instruments WHERE ((instruments.first = 'F1') AND (instruments.last = 'L1'))"
24
+ [{:id=>1}, {:id=>2}]
25
+ when /SELECT instrument_id FROM vocalists_instruments WHERE \(\((?:first|last) = '?[FL1](\d)/
26
+ a = []
27
+ a << {:instrument_id=>1} if $1 == "1"
28
+ a << {:instrument_id=>2} if $1 != "3"
29
+ a << {:instrument_id=>3} if $1 == "2"
30
+ a
31
+ when "SELECT year, week FROM hits WHERE ((hits.first = 'F1') AND (hits.last = 'L1'))"
32
+ [{:year=>1997, :week=>1}, {:year=>1997, :week=>2}]
33
+ when /SELECT year, week FROM vocalists_hits WHERE \(\((?:first|last) = '?[FL1](\d)/
34
+ a = []
35
+ a << {:year=>1997, :week=>1} if $1 == "1"
36
+ a << {:year=>1997, :week=>2} if $1 != "3"
37
+ a << {:year=>1997, :week=>3} if $1 == "2"
38
+ a
15
39
  end
16
40
  end)
17
41
  @Artist = Class.new(Sequel::Model(@db[:artists]))
18
42
  @Artist.columns :id
19
- @Album= Class.new(Sequel::Model(@db[:albums]))
43
+ @Album = Class.new(Sequel::Model(@db[:albums]))
20
44
  @Album.columns :id, :artist_id
21
45
  @Tag = Class.new(Sequel::Model(@db[:tags]))
22
46
  @Tag.columns :id
47
+ @Vocalist = Class.new(Sequel::Model(@db[:vocalists]))
48
+ @Vocalist.columns :first, :last, :album_id
49
+ @Vocalist.set_primary_key [:first, :last]
50
+ @Instrument = Class.new(Sequel::Model(@db[:instruments]))
51
+ @Instrument.columns :id, :first, :last
52
+ @Hit = Class.new(Sequel::Model(@db[:hits]))
53
+ @Hit.columns :year, :week, :first, :last
54
+ @Hit.set_primary_key [:year, :week]
23
55
  @Artist.plugin :association_pks
24
56
  @Album.plugin :association_pks
57
+ @Vocalist.plugin :association_pks
25
58
  @Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id
26
59
  @Album.many_to_many :tags, :class=>@Tag, :join_table=>:albums_tags, :left_key=>:album_id
27
60
  @db.sqls
@@ -60,6 +93,103 @@ describe "Sequel::Plugins::AssociationPks" do
60
93
  sqls.length.should == 3
61
94
  end
62
95
 
96
+ specify "should return correct right-side associated cpks for one_to_many associations" do
97
+ @Album.one_to_many :vocalists, :class=>@Vocalist, :key=>:album_id
98
+ @Album.load(:id=>1).vocalist_pks.should == [["F1", "L1"], ["F2", "L2"]]
99
+ @Album.load(:id=>2).vocalist_pks.should == []
100
+ end
101
+
102
+ specify "should return correct right-side associated cpks for many_to_many associations" do
103
+ @Album.many_to_many :vocalists, :class=>@Vocalist, :join_table=>:albums_vocalists, :left_key=>:album_id, :right_key=>[:first, :last]
104
+ @Album.load(:id=>1).vocalist_pks.should == [["F1", "L1"], ["F2", "L2"]]
105
+ @Album.load(:id=>2).vocalist_pks.should == [["F2", "L2"], ["F3", "L3"]]
106
+ @Album.load(:id=>3).vocalist_pks.should == []
107
+ end
108
+
109
+ specify "should set associated right-side cpks correctly for a one_to_many association" do
110
+ @Album.one_to_many :vocalists, :class=>@Vocalist, :key=>:album_id
111
+ @Album.load(:id=>1).vocalist_pks = [["F1", "L1"], ["F2", "L2"]]
112
+ @db.sqls.should == ["UPDATE vocalists SET album_id = 1 WHERE ((first, last) IN (('F1', 'L1'), ('F2', 'L2')))",
113
+ "UPDATE vocalists SET album_id = NULL WHERE ((vocalists.album_id = 1) AND ((first, last) NOT IN (('F1', 'L1'), ('F2', 'L2'))))"]
114
+ end
115
+
116
+ specify "should set associated right-side cpks correctly for a many_to_many association" do
117
+ @Album.many_to_many :vocalists, :class=>@Vocalist, :join_table=>:albums_vocalists, :left_key=>:album_id, :right_key=>[:first, :last]
118
+ @Album.load(:id=>2).vocalist_pks = [["F1", "L1"], ["F2", "L2"]]
119
+ sqls = @db.sqls
120
+ sqls[0].should == "DELETE FROM albums_vocalists WHERE ((album_id = 2) AND ((first, last) NOT IN (('F1', 'L1'), ('F2', 'L2'))))"
121
+ sqls[1].should == 'SELECT first, last FROM albums_vocalists WHERE (album_id = 2)'
122
+ match = sqls[2].match(/INSERT INTO albums_vocalists \((.*)\) VALUES \((.*)\)/)
123
+ Hash[match[1].split(', ').zip(match[2].split(', '))].should == {"first"=>"'F1'", "last"=>"'L1'", "album_id"=>"2"}
124
+ sqls.length.should == 3
125
+ end
126
+
127
+ specify "should return correct associated pks for left-side cpks for one_to_many associations" do
128
+ @Vocalist.one_to_many :instruments, :class=>@Instrument, :key=>[:first, :last]
129
+ @Vocalist.load(:first=>'F1', :last=>'L1').instrument_pks.should == [1, 2]
130
+ @Vocalist.load(:first=>'F2', :last=>'L2').instrument_pks.should == []
131
+ end
132
+
133
+ specify "should return correct associated pks for left-side cpks for many_to_many associations" do
134
+ @Vocalist.many_to_many :instruments, :class=>@Instrument, :join_table=>:vocalists_instruments, :left_key=>[:first, :last]
135
+ @Vocalist.load(:first=>'F1', :last=>'L1').instrument_pks.should == [1, 2]
136
+ @Vocalist.load(:first=>'F2', :last=>'L2').instrument_pks.should == [2, 3]
137
+ @Vocalist.load(:first=>'F3', :last=>'L3').instrument_pks.should == []
138
+ end
139
+
140
+ specify "should set associated pks correctly for left-side cpks for a one_to_many association" do
141
+ @Vocalist.one_to_many :instruments, :class=>@Instrument, :key=>[:first, :last]
142
+ @Vocalist.load(:first=>'F1', :last=>'L1').instrument_pks = [1, 2]
143
+ sqls = @db.sqls
144
+ sqls[0].should =~ /UPDATE instruments SET (first = 'F1', last = 'L1'|last = 'L1', first = 'F1') WHERE \(id IN \(1, 2\)\)/
145
+ sqls[1].should =~ /UPDATE instruments SET (first = NULL, last = NULL|last = NULL, first = NULL) WHERE \(\(instruments.first = 'F1'\) AND \(instruments.last = 'L1'\) AND \(id NOT IN \(1, 2\)\)\)/
146
+ sqls.length.should == 2
147
+ end
148
+
149
+ specify "should set associated pks correctly for left-side cpks for a many_to_many association" do
150
+ @Vocalist.many_to_many :instruments, :class=>@Instrument, :join_table=>:vocalists_instruments, :left_key=>[:first, :last]
151
+ @Vocalist.load(:first=>'F2', :last=>'L2').instrument_pks = [1, 2]
152
+ sqls = @db.sqls
153
+ sqls[0].should == "DELETE FROM vocalists_instruments WHERE ((first = 'F2') AND (last = 'L2') AND (instrument_id NOT IN (1, 2)))"
154
+ sqls[1].should == "SELECT instrument_id FROM vocalists_instruments WHERE ((first = 'F2') AND (last = 'L2'))"
155
+ match = sqls[2].match(/INSERT INTO vocalists_instruments \((.*)\) VALUES \((.*)\)/)
156
+ Hash[match[1].split(', ').zip(match[2].split(', '))].should == {"first"=>"'F2'", "last"=>"'L2'", "instrument_id"=>"1"}
157
+ sqls.length.should == 3
158
+ end
159
+
160
+ specify "should return correct right-side associated cpks for left-side cpks for one_to_many associations" do
161
+ @Vocalist.one_to_many :hits, :class=>@Hit, :key=>[:first, :last]
162
+ @Vocalist.load(:first=>'F1', :last=>'L1').hit_pks.should == [[1997, 1], [1997, 2]]
163
+ @Vocalist.load(:first=>'F2', :last=>'L2').hit_pks.should == []
164
+ end
165
+
166
+ specify "should return correct right-side associated cpks for left-side cpks for many_to_many associations" do
167
+ @Vocalist.many_to_many :hits, :class=>@Hit, :join_table=>:vocalists_hits, :left_key=>[:first, :last], :right_key=>[:year, :week]
168
+ @Vocalist.load(:first=>'F1', :last=>'L1').hit_pks.should == [[1997, 1], [1997, 2]]
169
+ @Vocalist.load(:first=>'F2', :last=>'L2').hit_pks.should == [[1997, 2], [1997, 3]]
170
+ @Vocalist.load(:first=>'F3', :last=>'L3').hit_pks.should == []
171
+ end
172
+
173
+ specify "should set associated right-side cpks correctly for left-side cpks for a one_to_many association" do
174
+ @Vocalist.one_to_many :hits, :class=>@Hit, :key=>[:first, :last], :order=>:week
175
+ @Vocalist.load(:first=>'F1', :last=>'L1').hit_pks = [[1997, 1], [1997, 2]]
176
+ sqls = @db.sqls
177
+ sqls[0].should =~ /UPDATE hits SET (first = 'F1', last = 'L1'|last = 'L1', first = 'F1') WHERE \(\(year, week\) IN \(\(1997, 1\), \(1997, 2\)\)\)/
178
+ sqls[1].should =~ /UPDATE hits SET (first = NULL, last = NULL|last = NULL, first = NULL) WHERE \(\(hits.first = 'F1'\) AND \(hits.last = 'L1'\) AND \(\(year, week\) NOT IN \(\(1997, 1\), \(1997, 2\)\)\)\)/
179
+ sqls.length.should == 2
180
+ end
181
+
182
+ specify "should set associated right-side cpks correctly for left-side cpks for a many_to_many association" do
183
+ @Vocalist.many_to_many :hits, :class=>@Hit, :join_table=>:vocalists_hits, :left_key=>[:first, :last], :right_key=>[:year, :week]
184
+ @Vocalist.load(:first=>'F2', :last=>'L2').hit_pks = [[1997, 1], [1997, 2]]
185
+ sqls = @db.sqls
186
+ sqls[0].should == "DELETE FROM vocalists_hits WHERE ((first = 'F2') AND (last = 'L2') AND ((year, week) NOT IN ((1997, 1), (1997, 2))))"
187
+ sqls[1].should == "SELECT year, week FROM vocalists_hits WHERE ((first = 'F2') AND (last = 'L2'))"
188
+ match = sqls[2].match(/INSERT INTO vocalists_hits \((.*)\) VALUES \((.*)\)/)
189
+ Hash[match[1].split(', ').zip(match[2].split(', '))].should == {"first"=>"'F2'", "last"=>"'L2'", "year"=>"1997", "week"=>"1"}
190
+ sqls.length.should == 3
191
+ end
192
+
63
193
  specify "should use transactions if the object is configured to use transactions" do
64
194
  artist = @Artist.load(:id=>1)
65
195
  artist.use_transactions = true
@@ -88,14 +218,14 @@ describe "Sequel::Plugins::AssociationPks" do
88
218
  "UPDATE albums SET artist_id = NULL WHERE ((albums.artist_id = 1) AND (id NOT IN (1, 2)))"]
89
219
  end
90
220
 
91
- specify "should not automatically convert keys if the primary key is not an integer for many_to_many associations" do
221
+ specify "should not automatically convert keys if the primary key is not an integer for one_to_many associations" do
92
222
  @Album.db_schema[:id][:type] = :string
93
223
  @Artist.load(:id=>1).album_pks = %w'1 2'
94
224
  @db.sqls.should == ["UPDATE albums SET artist_id = 1 WHERE (id IN ('1', '2'))",
95
225
  "UPDATE albums SET artist_id = NULL WHERE ((albums.artist_id = 1) AND (id NOT IN ('1', '2')))"]
96
226
  end
97
227
 
98
- specify "should automatically convert keys to numbers if the primary key is an integer for one_to_many associations" do
228
+ specify "should automatically convert keys to numbers if the primary key is an integer for many_to_many associations" do
99
229
  @Tag.db_schema[:id][:type] = :integer
100
230
  @Album.load(:id=>2).tag_pks = %w'1 3'
101
231
  sqls = @db.sqls
@@ -116,4 +246,34 @@ describe "Sequel::Plugins::AssociationPks" do
116
246
  sqls.length.should == 4
117
247
  end
118
248
 
249
+ specify "should automatically convert keys to numbers for appropriate integer primary key for composite key associations" do
250
+ @Hit.db_schema[:year][:type] = :integer
251
+ @Hit.db_schema[:week][:type] = :integer
252
+ @Vocalist.many_to_many :hits, :class=>@Hit, :join_table=>:vocalists_hits, :left_key=>[:first, :last], :right_key=>[:year, :week]
253
+ @Vocalist.load(:first=>'F2', :last=>'L2').hit_pks = [['1997', '1'], ['1997', '2']]
254
+ sqls = @db.sqls
255
+ sqls[0].should == "DELETE FROM vocalists_hits WHERE ((first = 'F2') AND (last = 'L2') AND ((year, week) NOT IN ((1997, 1), (1997, 2))))"
256
+ sqls[1].should == "SELECT year, week FROM vocalists_hits WHERE ((first = 'F2') AND (last = 'L2'))"
257
+ match = sqls[2].match(/INSERT INTO vocalists_hits \((.*)\) VALUES \((.*)\)/)
258
+ Hash[match[1].split(', ').zip(match[2].split(', '))].should == {"first"=>"'F2'", "last"=>"'L2'", "year"=>"1997", "week"=>"1"}
259
+ sqls.length.should == 3
260
+
261
+ @Vocalist.db_schema[:first][:type] = :integer
262
+ @Vocalist.db_schema[:last][:type] = :integer
263
+ @Album.one_to_many :vocalists, :class=>@Vocalist, :key=>:album_id
264
+ @Album.load(:id=>1).vocalist_pks = [["11", "11"], ["12", "12"]]
265
+ @db.sqls.should == ["UPDATE vocalists SET album_id = 1 WHERE ((first, last) IN ((11, 11), (12, 12)))",
266
+ "UPDATE vocalists SET album_id = NULL WHERE ((vocalists.album_id = 1) AND ((first, last) NOT IN ((11, 11), (12, 12))))"]
267
+
268
+ @Album.many_to_many :vocalists, :class=>@Vocalist, :join_table=>:albums_vocalists, :left_key=>:album_id, :right_key=>[:first, :last]
269
+ @Album.load(:id=>2).vocalist_pks = [["11", "11"], ["12", "12"]]
270
+ sqls = @db.sqls
271
+ sqls[0].should == "DELETE FROM albums_vocalists WHERE ((album_id = 2) AND ((first, last) NOT IN ((11, 11), (12, 12))))"
272
+ sqls[1].should == 'SELECT first, last FROM albums_vocalists WHERE (album_id = 2)'
273
+ match = sqls[2].match(/INSERT INTO albums_vocalists \((.*)\) VALUES \((.*)\)/)
274
+ Hash[match[1].split(', ').zip(match[2].split(', '))].should == {"first"=>"11", "last"=>"11", "album_id"=>"2"}
275
+ match = sqls[3].match(/INSERT INTO albums_vocalists \((.*)\) VALUES \((.*)\)/)
276
+ Hash[match[1].split(', ').zip(match[2].split(', '))].should == {"first"=>"12", "last"=>"12", "album_id"=>"2"}
277
+ sqls.length.should == 4
278
+ end
119
279
  end
@@ -191,4 +191,10 @@ describe "pg_hstore extension" do
191
191
  @db.typecast_value(:hstore, {'a'=>'b'}).should == Sequel.hstore("a"=>"b")
192
192
  proc{@db.typecast_value(:hstore, [])}.should raise_error(Sequel::InvalidValue)
193
193
  end
194
+
195
+ it "should be serializable" do
196
+ v = Sequel.hstore('foo'=>'bar')
197
+ dump = Marshal.dump(v)
198
+ Marshal.load(dump).should == v
199
+ end
194
200
  end
@@ -158,6 +158,13 @@ describe "pg_row extension" do
158
158
  @db.bound_variable_arg([@m::HashRow.subclass(nil, [:a, :b]).call(:a=>"1", :b=>"abc\\'\",")], nil).should == '{"(\\"1\\",\\"abc\\\\\\\\\'\\\\\\",\\")"}'
159
159
  end
160
160
 
161
+ it "should handle nils in bound variables" do
162
+ @db.bound_variable_arg(@m::ArrayRow.call([nil, nil]), nil).should == '(,)'
163
+ @db.bound_variable_arg(@m::HashRow.subclass(nil, [:a, :b]).call(:a=>nil, :b=>nil), nil).should == '(,)'
164
+ @db.bound_variable_arg([@m::ArrayRow.call([nil, nil])], nil).should == '{"(,)"}'
165
+ @db.bound_variable_arg([@m::HashRow.subclass(nil, [:a, :b]).call(:a=>nil, :b=>nil)], nil).should == '{"(,)"}'
166
+ end
167
+
161
168
  it "should allow registering row type parsers by introspecting system tables" do
162
169
  @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
163
170
  @db.conversion_procs[5] = p5 = proc{|s| s * 2}
@@ -247,6 +254,16 @@ describe "pg_row extension" do
247
254
  @db.conversion_procs[1].call('(1,b)').should == [{:bar=>1, :baz=>'bb'}]
248
255
  end
249
256
 
257
+ it "should handle nil values when converting columns" do
258
+ @db.conversion_procs[5] = p5 = proc{|s| s * 2}
259
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}]]
260
+ called = false
261
+ @db.conversion_procs[4] = proc{|s| called = true; s}
262
+ @db.register_row_type(:foo)
263
+ @db.conversion_procs[1].call('()').should == {:bar=>nil}
264
+ called.should be_false
265
+ end
266
+
250
267
  it "should registering array type for row type if type has an array oid" do
251
268
  @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
252
269
  @db.conversion_procs[5] = p5 = proc{|s| s * 2}