sequel 3.39.0 → 3.40.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 (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}