sequel 3.37.0 → 3.38.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) hide show
  1. data/CHANGELOG +56 -0
  2. data/README.rdoc +82 -58
  3. data/Rakefile +6 -5
  4. data/bin/sequel +1 -1
  5. data/doc/active_record.rdoc +67 -52
  6. data/doc/advanced_associations.rdoc +33 -48
  7. data/doc/association_basics.rdoc +41 -51
  8. data/doc/cheat_sheet.rdoc +21 -21
  9. data/doc/core_extensions.rdoc +374 -0
  10. data/doc/dataset_basics.rdoc +5 -5
  11. data/doc/dataset_filtering.rdoc +47 -43
  12. data/doc/mass_assignment.rdoc +1 -1
  13. data/doc/migration.rdoc +4 -5
  14. data/doc/model_hooks.rdoc +3 -3
  15. data/doc/object_model.rdoc +31 -25
  16. data/doc/opening_databases.rdoc +19 -5
  17. data/doc/prepared_statements.rdoc +2 -2
  18. data/doc/querying.rdoc +109 -52
  19. data/doc/reflection.rdoc +6 -6
  20. data/doc/release_notes/3.38.0.txt +234 -0
  21. data/doc/schema_modification.rdoc +22 -13
  22. data/doc/sharding.rdoc +8 -9
  23. data/doc/sql.rdoc +154 -112
  24. data/doc/testing.rdoc +47 -7
  25. data/doc/thread_safety.rdoc +1 -1
  26. data/doc/transactions.rdoc +1 -1
  27. data/doc/validations.rdoc +1 -1
  28. data/doc/virtual_rows.rdoc +29 -43
  29. data/lib/sequel/adapters/do/postgres.rb +1 -4
  30. data/lib/sequel/adapters/jdbc.rb +14 -3
  31. data/lib/sequel/adapters/jdbc/db2.rb +9 -0
  32. data/lib/sequel/adapters/jdbc/derby.rb +41 -4
  33. data/lib/sequel/adapters/jdbc/jtds.rb +11 -0
  34. data/lib/sequel/adapters/jdbc/postgresql.rb +3 -6
  35. data/lib/sequel/adapters/mock.rb +10 -4
  36. data/lib/sequel/adapters/postgres.rb +1 -28
  37. data/lib/sequel/adapters/shared/mssql.rb +23 -13
  38. data/lib/sequel/adapters/shared/postgres.rb +46 -0
  39. data/lib/sequel/adapters/swift.rb +21 -13
  40. data/lib/sequel/adapters/swift/mysql.rb +1 -0
  41. data/lib/sequel/adapters/swift/postgres.rb +4 -5
  42. data/lib/sequel/adapters/swift/sqlite.rb +2 -1
  43. data/lib/sequel/adapters/tinytds.rb +14 -2
  44. data/lib/sequel/adapters/utils/pg_types.rb +5 -0
  45. data/lib/sequel/core.rb +29 -17
  46. data/lib/sequel/database/query.rb +1 -1
  47. data/lib/sequel/database/schema_generator.rb +3 -0
  48. data/lib/sequel/dataset/actions.rb +5 -6
  49. data/lib/sequel/dataset/query.rb +7 -7
  50. data/lib/sequel/dataset/sql.rb +5 -18
  51. data/lib/sequel/extensions/core_extensions.rb +8 -12
  52. data/lib/sequel/extensions/pg_array.rb +59 -33
  53. data/lib/sequel/extensions/pg_array_ops.rb +32 -4
  54. data/lib/sequel/extensions/pg_auto_parameterize.rb +1 -1
  55. data/lib/sequel/extensions/pg_hstore.rb +32 -17
  56. data/lib/sequel/extensions/pg_hstore_ops.rb +32 -3
  57. data/lib/sequel/extensions/pg_inet.rb +1 -2
  58. data/lib/sequel/extensions/pg_interval.rb +0 -1
  59. data/lib/sequel/extensions/pg_json.rb +41 -23
  60. data/lib/sequel/extensions/pg_range.rb +36 -11
  61. data/lib/sequel/extensions/pg_range_ops.rb +32 -4
  62. data/lib/sequel/extensions/pg_row.rb +572 -0
  63. data/lib/sequel/extensions/pg_row_ops.rb +164 -0
  64. data/lib/sequel/extensions/query.rb +3 -3
  65. data/lib/sequel/extensions/schema_dumper.rb +7 -8
  66. data/lib/sequel/extensions/select_remove.rb +1 -1
  67. data/lib/sequel/model/base.rb +1 -0
  68. data/lib/sequel/no_core_ext.rb +1 -1
  69. data/lib/sequel/plugins/pg_row.rb +121 -0
  70. data/lib/sequel/plugins/pg_typecast_on_load.rb +65 -0
  71. data/lib/sequel/plugins/validation_helpers.rb +31 -0
  72. data/lib/sequel/sql.rb +64 -44
  73. data/lib/sequel/version.rb +1 -1
  74. data/spec/adapters/mssql_spec.rb +37 -12
  75. data/spec/adapters/mysql_spec.rb +39 -75
  76. data/spec/adapters/oracle_spec.rb +11 -11
  77. data/spec/adapters/postgres_spec.rb +414 -237
  78. data/spec/adapters/spec_helper.rb +1 -1
  79. data/spec/adapters/sqlite_spec.rb +14 -14
  80. data/spec/core/database_spec.rb +6 -6
  81. data/spec/core/dataset_spec.rb +169 -205
  82. data/spec/core/expression_filters_spec.rb +182 -295
  83. data/spec/core/object_graph_spec.rb +6 -6
  84. data/spec/core/schema_spec.rb +14 -14
  85. data/spec/core/spec_helper.rb +1 -0
  86. data/spec/{extensions/core_extensions_spec.rb → core_extensions_spec.rb} +208 -14
  87. data/spec/extensions/columns_introspection_spec.rb +5 -5
  88. data/spec/extensions/hook_class_methods_spec.rb +28 -36
  89. data/spec/extensions/many_through_many_spec.rb +4 -4
  90. data/spec/extensions/pg_array_ops_spec.rb +15 -7
  91. data/spec/extensions/pg_array_spec.rb +81 -48
  92. data/spec/extensions/pg_auto_parameterize_spec.rb +2 -2
  93. data/spec/extensions/pg_hstore_ops_spec.rb +13 -9
  94. data/spec/extensions/pg_hstore_spec.rb +66 -65
  95. data/spec/extensions/pg_inet_spec.rb +2 -4
  96. data/spec/extensions/pg_interval_spec.rb +2 -3
  97. data/spec/extensions/pg_json_spec.rb +20 -18
  98. data/spec/extensions/pg_range_ops_spec.rb +11 -4
  99. data/spec/extensions/pg_range_spec.rb +30 -7
  100. data/spec/extensions/pg_row_ops_spec.rb +48 -0
  101. data/spec/extensions/pg_row_plugin_spec.rb +45 -0
  102. data/spec/extensions/pg_row_spec.rb +323 -0
  103. data/spec/extensions/pg_typecast_on_load_spec.rb +58 -0
  104. data/spec/extensions/query_literals_spec.rb +11 -11
  105. data/spec/extensions/query_spec.rb +3 -3
  106. data/spec/extensions/schema_dumper_spec.rb +20 -4
  107. data/spec/extensions/schema_spec.rb +18 -41
  108. data/spec/extensions/select_remove_spec.rb +4 -4
  109. data/spec/extensions/spec_helper.rb +4 -8
  110. data/spec/extensions/to_dot_spec.rb +5 -5
  111. data/spec/extensions/validation_class_methods_spec.rb +28 -16
  112. data/spec/integration/associations_test.rb +20 -20
  113. data/spec/integration/dataset_test.rb +98 -98
  114. data/spec/integration/eager_loader_test.rb +13 -27
  115. data/spec/integration/plugin_test.rb +5 -5
  116. data/spec/integration/prepared_statement_test.rb +22 -13
  117. data/spec/integration/schema_test.rb +28 -18
  118. data/spec/integration/spec_helper.rb +1 -1
  119. data/spec/integration/timezone_test.rb +2 -2
  120. data/spec/integration/type_test.rb +15 -6
  121. data/spec/model/association_reflection_spec.rb +1 -1
  122. data/spec/model/associations_spec.rb +4 -4
  123. data/spec/model/base_spec.rb +5 -5
  124. data/spec/model/eager_loading_spec.rb +15 -15
  125. data/spec/model/model_spec.rb +32 -32
  126. data/spec/model/record_spec.rb +16 -0
  127. data/spec/model/spec_helper.rb +2 -6
  128. data/spec/model/validations_spec.rb +1 -1
  129. metadata +16 -4
@@ -4,9 +4,7 @@ describe "pg_inet extension" do
4
4
  ipv6_broken = (IPAddr.new('::1'); false) rescue true
5
5
  before do
6
6
  @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
7
- @db.extend(Module.new{def bound_variable_arg(arg, conn) arg end})
8
- @db.extension(:pg_array)
9
- @db.extension(:pg_inet)
7
+ @db.extension(:pg_array, :pg_inet)
10
8
  end
11
9
 
12
10
  it "should literalize IPAddr v4 instances to strings correctly" do
@@ -31,7 +29,7 @@ describe "pg_inet extension" do
31
29
  end
32
30
 
33
31
  it "should support using IPAddr instances in array types in bound variables" do
34
- @db.bound_variable_arg([IPAddr.new('127.0.0.1')].pg_array, nil).should == '{"127.0.0.1/32"}'
32
+ @db.bound_variable_arg(Sequel.pg_array([IPAddr.new('127.0.0.1')]), nil).should == '{"127.0.0.1/32"}'
35
33
  end
36
34
 
37
35
  it "should parse inet/cidr type from the schema correctly" do
@@ -8,7 +8,6 @@ else
8
8
  describe "pg_interval extension" do
9
9
  before do
10
10
  @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
11
- @db.extend(Module.new{def bound_variable_arg(arg, conn) arg end})
12
11
  @db.extension(:pg_array, :pg_interval)
13
12
  end
14
13
 
@@ -37,8 +36,8 @@ describe "pg_interval extension" do
37
36
  end
38
37
 
39
38
  it "should support using ActiveSupport::Duration instances in array types in bound variables" do
40
- @db.bound_variable_arg([ActiveSupport::Duration.new(0, [[:seconds, 0]])].pg_array, nil).should == '{"0"}'
41
- @db.bound_variable_arg([ActiveSupport::Duration.new(0, [[:seconds, -10.000001], [:minutes, -20], [:days, -3], [:months, -4], [:years, -6]])].pg_array, nil).should == '{"-6 years -4 months -3 days -20 minutes -10.000001 seconds "}'
39
+ @db.bound_variable_arg(Sequel.pg_array([ActiveSupport::Duration.new(0, [[:seconds, 0]])]), nil).should == '{"0"}'
40
+ @db.bound_variable_arg(Sequel.pg_array([ActiveSupport::Duration.new(0, [[:seconds, -10.000001], [:minutes, -20], [:days, -3], [:months, -4], [:years, -6]])]), nil).should == '{"-6 years -4 months -3 days -20 minutes -10.000001 seconds "}'
42
41
  end
43
42
 
44
43
  it "should parse interval type from the schema correctly" do
@@ -20,7 +20,6 @@ describe "pg_json extension" do
20
20
  end
21
21
  before do
22
22
  @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
23
- @db.extend(Module.new{def bound_variable_arg(arg, conn) arg end})
24
23
  @db.extension(:pg_array, :pg_json)
25
24
  end
26
25
 
@@ -43,36 +42,39 @@ describe "pg_json extension" do
43
42
  end
44
43
 
45
44
  it "should literalize HStores to strings correctly" do
46
- @db.literal([].pg_json).should == "'[]'::json"
47
- @db.literal([1, [2], {'a'=>'b'}].pg_json).should == "'[1,[2],{\"a\":\"b\"}]'::json"
48
- @db.literal({}.pg_json).should == "'{}'::json"
49
- @db.literal({'a'=>'b'}.pg_json).should == "'{\"a\":\"b\"}'::json"
45
+ @db.literal(Sequel.pg_json([])).should == "'[]'::json"
46
+ @db.literal(Sequel.pg_json([1, [2], {'a'=>'b'}])).should == "'[1,[2],{\"a\":\"b\"}]'::json"
47
+ @db.literal(Sequel.pg_json({})).should == "'{}'::json"
48
+ @db.literal(Sequel.pg_json('a'=>'b')).should == "'{\"a\":\"b\"}'::json"
50
49
  end
51
50
 
52
- it "should have Hash#pg_json method for creating JSONHash instances" do
53
- {}.pg_json.should be_a_kind_of(@hc)
51
+ it "should have Sequel.pg_json return JSONHash and JSONArray as is" do
52
+ a = Sequel.pg_json({})
53
+ Sequel.pg_json(a).should equal(a)
54
+ a = Sequel.pg_json([])
55
+ Sequel.pg_json(a).should equal(a)
54
56
  end
55
57
 
56
- it "should have Array#pg_json method for creating JSONArray instances" do
57
- [].pg_json.should be_a_kind_of(@ac)
58
+ it "should have Sequel.pg_json raise an Error if called with a non-hash or array" do
59
+ proc{Sequel.pg_json(:a)}.should raise_error(Sequel::Error)
58
60
  end
59
61
 
60
62
  it "should have JSONHash#to_hash method for getting underlying hash" do
61
- {}.pg_json.to_hash.should be_a_kind_of(Hash)
63
+ Sequel.pg_json({}).to_hash.should be_a_kind_of(Hash)
62
64
  end
63
65
 
64
66
  it "should have JSONArray#to_a method for getting underlying array" do
65
- [].pg_json.to_a.should be_a_kind_of(Array)
67
+ Sequel.pg_json([]).to_a.should be_a_kind_of(Array)
66
68
  end
67
69
 
68
70
  it "should support using JSONHash and JSONArray as bound variables" do
69
71
  @db.bound_variable_arg(1, nil).should == 1
70
- @db.bound_variable_arg([1].pg_json, nil).should == '[1]'
71
- @db.bound_variable_arg({'a'=>'b'}.pg_json, nil).should == '{"a":"b"}'
72
+ @db.bound_variable_arg(Sequel.pg_json([1]), nil).should == '[1]'
73
+ @db.bound_variable_arg(Sequel.pg_json('a'=>'b'), nil).should == '{"a":"b"}'
72
74
  end
73
75
 
74
76
  it "should support using json[] types in bound variables" do
75
- @db.bound_variable_arg([[{"a"=>1}].pg_json, {"b"=>[1, 2]}.pg_json].pg_array, nil).should == '{"[{\\"a\\":1}]","{\\"b\\":[1,2]}"}'
77
+ @db.bound_variable_arg(Sequel.pg_array([Sequel.pg_json([{"a"=>1}]), Sequel.pg_json("b"=>[1, 2])]), nil).should == '{"[{\\"a\\":1}]","{\\"b\\":[1,2]}"}'
76
78
  end
77
79
 
78
80
  it "should parse json type from the schema correctly" do
@@ -81,16 +83,16 @@ describe "pg_json extension" do
81
83
  end
82
84
 
83
85
  it "should support typecasting for the json type" do
84
- h = {1=>2}.pg_json
85
- a = [1].pg_json
86
+ h = Sequel.pg_json(1=>2)
87
+ a = Sequel.pg_json([1])
86
88
  @db.typecast_value(:json, h).should equal(h)
87
89
  @db.typecast_value(:json, h.to_hash).should == h
88
90
  @db.typecast_value(:json, h.to_hash).should be_a_kind_of(@hc)
89
91
  @db.typecast_value(:json, a).should equal(a)
90
92
  @db.typecast_value(:json, a.to_a).should == a
91
93
  @db.typecast_value(:json, a.to_a).should be_a_kind_of(@ac)
92
- @db.typecast_value(:json, '[]').should == [].pg_json
93
- @db.typecast_value(:json, '{"a": "b"}').should == {"a"=>"b"}.pg_json
94
+ @db.typecast_value(:json, '[]').should == Sequel.pg_json([])
95
+ @db.typecast_value(:json, '{"a": "b"}').should == Sequel.pg_json("a"=>"b")
94
96
  proc{@db.typecast_value(:json, '')}.should raise_error(Sequel::InvalidValue)
95
97
  proc{@db.typecast_value(:json, 1)}.should raise_error(Sequel::InvalidValue)
96
98
  end
@@ -3,21 +3,28 @@ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
3
3
  describe "Sequel::Postgres::RangeOp" do
4
4
  before do
5
5
  @ds = Sequel.connect('mock://postgres', :quote_identifiers=>false).dataset
6
- @h = :h.pg_range
6
+ @h = Sequel.pg_range_op(:h)
7
7
  end
8
8
 
9
9
  it "#pg_range should return self" do
10
10
  @h.pg_range.should equal(@h)
11
11
  end
12
12
 
13
- it "#pg_range should return a RangeOp for symbols, literal strings, and expressions" do
14
- @ds.literal(:h.pg_range.lower).should == "lower(h)"
13
+ it "Sequel.pg_range_op should return argument if already a RangeOp" do
14
+ Sequel.pg_range_op(@h).should equal(@h)
15
+ end
16
+
17
+ it "Sequel.pg_range should return a new RangeOp if not given a range" do
18
+ @ds.literal(Sequel.pg_range(:h).lower).should == "lower(h)"
19
+ end
20
+
21
+ it "#pg_range should return a RangeOp for literal strings, and expressions" do
15
22
  @ds.literal(Sequel.function(:b, :h).pg_range.lower).should == "lower(b(h))"
16
23
  @ds.literal(Sequel.lit('h').pg_range.lower).should == "lower(h)"
17
24
  end
18
25
 
19
26
  it "PGRange#op should return a RangeOp" do
20
- @ds.literal((1..2).pg_range(:numrange).op.lower).should == "lower('[1,2]'::numrange)"
27
+ @ds.literal(Sequel.pg_range(1..2, :numrange).op.lower).should == "lower('[1,2]'::numrange)"
21
28
  end
22
29
 
23
30
  it "should define methods for all of the the PostgreSQL range operators" do
@@ -1,13 +1,18 @@
1
1
  require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
2
 
3
3
  describe "pg_range extension" do
4
+ before(:all) do
5
+ @pg_types = Sequel::Postgres::PG_TYPES.dup
6
+ end
7
+ after(:all) do
8
+ Sequel::Postgres::PG_TYPES.replace(@pg_types)
9
+ end
10
+
4
11
  before do
5
12
  @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
6
13
  @R = Sequel::Postgres::PGRange
7
- @db.extend(Module.new{def get_conversion_procs(conn) {} end; def bound_variable_arg(arg, conn) arg end})
8
14
  @db.extend_datasets(Module.new{def supports_timestamp_timezones?; false; end; def supports_timestamp_usecs?; false; end})
9
- @db.extension(:pg_array)
10
- @db.extension(:pg_range)
15
+ @db.extension(:pg_array, :pg_range)
11
16
  end
12
17
 
13
18
  it "should literalize Range instances to strings correctly" do
@@ -204,13 +209,13 @@ describe "pg_range extension" do
204
209
  end
205
210
 
206
211
  it "should set appropriate timestamp range conversion procs when getting conversion procs" do
207
- procs = @db.send(:get_conversion_procs, nil)
212
+ procs = @db.conversion_procs
208
213
  procs[3908].call('[2011-10-20 11:12:13,2011-10-20 11:12:14]').should == (Time.local(2011, 10, 20, 11, 12, 13)..(Time.local(2011, 10, 20, 11, 12, 14)))
209
214
  procs[3910].call('[2011-10-20 11:12:13,2011-10-20 11:12:14]').should == (Time.local(2011, 10, 20, 11, 12, 13)..(Time.local(2011, 10, 20, 11, 12, 14)))
210
215
  end
211
216
 
212
217
  it "should set appropriate timestamp range array conversion procs when getting conversion procs" do
213
- procs = @db.send(:get_conversion_procs, nil)
218
+ procs = @db.conversion_procs
214
219
  procs[3909].call('{"[2011-10-20 11:12:13,2011-10-20 11:12:14]"}').should == [Time.local(2011, 10, 20, 11, 12, 13)..Time.local(2011, 10, 20, 11, 12, 14)]
215
220
  procs[3911].call('{"[2011-10-20 11:12:13,2011-10-20 11:12:14]"}').should == [Time.local(2011, 10, 20, 11, 12, 13)..Time.local(2011, 10, 20, 11, 12, 14)]
216
221
  end
@@ -240,8 +245,26 @@ describe "pg_range extension" do
240
245
  @r3.db_type.should == 'int8range'
241
246
  end
242
247
 
243
- it "should be able to be created by Range#pg_range" do
244
- (1..2).pg_range.should == @r1
248
+ it "should be able to be created by Sequel.pg_range" do
249
+ Sequel.pg_range(1..2).should == @r1
250
+ end
251
+
252
+ it "should have Sequel.pg_range be able to take a database type" do
253
+ Sequel.pg_range(1..2, :int4range).should == @R.new(1, 2, :db_type=>:int4range)
254
+ end
255
+
256
+ it "should have Sequel.pg_range return a PGRange as is" do
257
+ a = Sequel.pg_range(1..2)
258
+ Sequel.pg_range(a).should equal(a)
259
+ end
260
+
261
+ it "should have Sequel.pg_range return a new PGRange if the database type differs" do
262
+ a = Sequel.pg_range(1..2, :int4range)
263
+ b = Sequel.pg_range(a, :int8range)
264
+ a.to_range.should == b.to_range
265
+ a.should_not equal(b)
266
+ a.db_type.should == :int4range
267
+ b.db_type.should == :int8range
245
268
  end
246
269
 
247
270
  it "should have #initialize raise if requesting an empty range with beginning or ending" do
@@ -0,0 +1,48 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ describe "Sequel::Postgres::PGRowOp" do
4
+ before do
5
+ @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
6
+ @a = Sequel.pg_row_op(:a)
7
+ end
8
+
9
+ it "#[] should access members of the composite type" do
10
+ @db.literal(@a[:b]).should == "(a).b"
11
+ end
12
+
13
+ it "#[] should be chainable" do
14
+ @db.literal(@a[:b][:c]).should == "((a).b).c"
15
+ end
16
+
17
+ it "#[] should support array access if not given an identifier" do
18
+ @db.literal(@a[:b][1]).should == "(a).b[1]"
19
+ end
20
+
21
+ it "#[] should be chainable with array access" do
22
+ @db.literal(@a[1][:b]).should == "(a[1]).b"
23
+ end
24
+
25
+ it "#splat should return a splatted argument" do
26
+ @db.literal(@a.splat).should == "(a.*)"
27
+ end
28
+
29
+ it "#splat(type) should return a splatted argument cast to given type" do
30
+ @db.literal(@a.splat(:b)).should == "(a.*)::b"
31
+ end
32
+
33
+ it "#splat should not work on an already accessed composite type" do
34
+ proc{@a[:a].splat(:b)}.should raise_error(Sequel::Error)
35
+ end
36
+
37
+ it "#pg_row should be callable on literal strings" do
38
+ @db.literal(Sequel.lit('a').pg_row[:b]).should == "(a).b"
39
+ end
40
+
41
+ it "#pg_row should be callable on Sequel expressions" do
42
+ @db.literal(Sequel.function(:a).pg_row[:b]).should == "(a()).b"
43
+ end
44
+
45
+ it "Sequel.pg_row should work as well if the pg_row extension is loaded" do
46
+ @db.literal(Sequel.pg_row(Sequel.function(:a))[:b]).should == "(a()).b"
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ describe "Sequel::Plugins::PgRow" do
4
+ before(:all) do
5
+ @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
6
+ @db.extension(:pg_array)
7
+ @c = Class.new(Sequel::Model(@db[:address]))
8
+ @c.columns :street, :city
9
+ @c.db_schema[:street][:type] = :string
10
+ @c.db_schema[:city][:type] = :string
11
+ @db.fetch = [[{:oid=>1098, :typrelid=>2, :typarray=>3}], [{:attname=>'street', :atttypid=>1324}, {:attname=>'city', :atttypid=>1324}]]
12
+ @c.plugin :pg_row
13
+
14
+ @c2 = Class.new(Sequel::Model(@db[:company]))
15
+ @c2.columns :address
16
+ @c2.db_schema[:address].merge!(:type=>:pg_row_address)
17
+ end
18
+
19
+ it "should set up a parser for the type that creates a model class" do
20
+ @db.conversion_procs[1098].call('(123 Foo St,Bar City)').should == @c.load(:street=>'123 Foo St', :city=>'Bar City')
21
+ end
22
+
23
+ it "should set up type casting for the type" do
24
+ @c2.new(:address=>{'street'=>123, 'city'=>:Bar}).address.should == @c.load(:street=>'123', :city=>'Bar')
25
+ end
26
+
27
+ it "should return model instances as is when typecasting to rows" do
28
+ o = @c.load(:street=>'123', :city=>'Bar')
29
+ @c2.new(:address=>o).address.should equal(o)
30
+ end
31
+
32
+ it "should handle literalizing model instances" do
33
+ @db.literal(@c.load(:street=>'123 Foo St', :city=>'Bar City')).should == "ROW('123 Foo St', 'Bar City')::address"
34
+ end
35
+
36
+ it "should handle model instances in bound variables" do
37
+ @db.bound_variable_arg(1, nil).should == 1
38
+ @db.bound_variable_arg(@c.load(:street=>'123 Foo St', :city=>'Bar City'), nil).should == '("123 Foo St","Bar City")'
39
+ end
40
+
41
+ it "should handle model instances in arrays of bound variables" do
42
+ @db.bound_variable_arg(1, nil).should == 1
43
+ @db.bound_variable_arg(Sequel.pg_array([@c.load(:street=>'123 Foo St', :city=>'Bar City')]), nil).should == '{"(\\"123 Foo St\\",\\"Bar City\\")"}'
44
+ end
45
+ end
@@ -0,0 +1,323 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ describe "pg_row extension" do
4
+ before do
5
+ @db = Sequel.connect('mock://postgres', :quote_identifiers=>false)
6
+ @db.extension(:pg_array, :pg_row)
7
+ @m = Sequel::Postgres::PGRow
8
+ @db.sqls
9
+ end
10
+
11
+ it "should parse record objects as arrays" do
12
+ a = Sequel::Postgres::PG_TYPES[2249].call("(a,b,c)")
13
+ a.should be_a_kind_of(@m::ArrayRow)
14
+ a.to_a.should be_a_kind_of(Array)
15
+ a[0].should == 'a'
16
+ a.should == %w'a b c'
17
+ a.db_type.should == nil
18
+ @db.literal(a).should == "ROW('a', 'b', 'c')"
19
+ end
20
+
21
+ it "should parse arrays of record objects as arrays of arrays" do
22
+ as = Sequel::Postgres::PG_TYPES[2287].call('{"(a,b,c)","(d,e,f)"}')
23
+ as.should == [%w'a b c', %w'd e f']
24
+ as.each do |a|
25
+ a.should be_a_kind_of(@m::ArrayRow)
26
+ a.to_a.should be_a_kind_of(Array)
27
+ a.db_type.should == nil
28
+ end
29
+ @db.literal(as).should == "ARRAY[ROW('a', 'b', 'c'),ROW('d', 'e', 'f')]::record[]"
30
+ end
31
+
32
+ it "should be able to register custom parsing of row types as array-like objects" do
33
+ klass = @m::ArrayRow.subclass(:foo)
34
+ parser = @m::Parser.new(:converter=>klass)
35
+ a = parser.call("(a,b,c)")
36
+ a.should be_a_kind_of(klass)
37
+ a.to_a.should be_a_kind_of(Array)
38
+ a[0].should == 'a'
39
+ a.should == %w'a b c'
40
+ a.db_type.should == :foo
41
+ @db.literal(a).should == "ROW('a', 'b', 'c')::foo"
42
+ end
43
+
44
+ it "should be able to register custom parsing of row types as hash-like objects" do
45
+ klass = @m::HashRow.subclass(:foo, [:a, :b, :c])
46
+ parser = @m::Parser.new(:converter=>klass, :columns=>[:a, :b, :c])
47
+ a = parser.call("(a,b,c)")
48
+ a.should be_a_kind_of(klass)
49
+ a.to_hash.should be_a_kind_of(Hash)
50
+ a[:a].should == 'a'
51
+ a.should == {:a=>'a', :b=>'b', :c=>'c'}
52
+ a.db_type.should == :foo
53
+ a.columns.should == [:a, :b, :c]
54
+ @db.literal(a).should == "ROW('a', 'b', 'c')::foo"
55
+ end
56
+
57
+ it "should raise an error if attempting to literalize a HashRow without column information" do
58
+ h = @m::HashRow.call(:a=>'a', :b=>'b', :c=>'c')
59
+ proc{@db.literal(h)}.should raise_error(Sequel::Error)
60
+ end
61
+
62
+ it "should be able to manually override db_type per ArrayRow instance" do
63
+ a = @m::ArrayRow.call(%w'a b c')
64
+ a.db_type = :foo
65
+ @db.literal(a).should == "ROW('a', 'b', 'c')::foo"
66
+ end
67
+
68
+ it "should be able to manually override db_type and columns per HashRow instance" do
69
+ h = @m::HashRow.call(:a=>'a', :c=>'c', :b=>'b')
70
+ h.db_type = :foo
71
+ h.columns = [:a, :b, :c]
72
+ @db.literal(h).should == "ROW('a', 'b', 'c')::foo"
73
+ end
74
+
75
+ it "should correctly split an empty row" do
76
+ @m::Splitter.new("()").parse.should == [nil]
77
+ end
78
+
79
+ it "should correctly split a row with a single value" do
80
+ @m::Splitter.new("(1)").parse.should == %w'1'
81
+ end
82
+
83
+ it "should correctly split a row with multiple values" do
84
+ @m::Splitter.new("(1,2)").parse.should == %w'1 2'
85
+ end
86
+
87
+ it "should correctly NULL values when splitting" do
88
+ @m::Splitter.new("(1,)").parse.should == ['1', nil]
89
+ end
90
+
91
+ it "should correctly empty string values when splitting" do
92
+ @m::Splitter.new('(1,"")').parse.should == ['1', '']
93
+ end
94
+
95
+ it "should handle quoted values when splitting" do
96
+ @m::Splitter.new('("1","2")').parse.should == %w'1 2'
97
+ end
98
+
99
+ it "should handle escaped backslashes in quoted values when splitting" do
100
+ @m::Splitter.new('("\\\\1","2\\\\")').parse.should == ['\\1', '2\\']
101
+ end
102
+
103
+ it "should handle doubled quotes in quoted values when splitting" do
104
+ @m::Splitter.new('("""1","2""")').parse.should == ['"1', '2"']
105
+ end
106
+
107
+ it "should correctly convert types when parsing into an array" do
108
+ @m::Parser.new(:column_converters=>[proc{|s| s*2}, proc{|s| s*3}, proc{|s| s*4}]).call("(a,b,c)").should == %w'aa bbb cccc'
109
+ end
110
+
111
+ it "should correctly convert types into hashes if columns are known" do
112
+ @m::Parser.new(:columns=>[:a, :b, :c]).call("(a,b,c)").should == {:a=>'a', :b=>'b', :c=>'c'}
113
+ end
114
+
115
+ it "should correctly handle type conversion when converting into hashes" do
116
+ @m::Parser.new(:column_converters=>[proc{|s| s*2}, proc{|s| s*3}, proc{|s| s*4}], :columns=>[:a, :b, :c]).call("(a,b,c)").should == {:a=>'aa', :b=>'bbb', :c=>'cccc'}
117
+ end
118
+
119
+ it "should correctly wrap arrays when converting" do
120
+ @m::Parser.new(:converter=>proc{|s| [:foo, s]}).call("(a,b,c)").should == [:foo, %w'a b c']
121
+ end
122
+
123
+ it "should correctly wrap hashes when converting" do
124
+ @m::Parser.new(:converter=>proc{|s| [:foo, s]}, :columns=>[:a, :b, :c]).call("(a,b,c)").should == [:foo, {:a=>'a', :b=>'b', :c=>'c'}]
125
+ end
126
+
127
+ it "should have parser store reflection information" do
128
+ p = @m::Parser.new(:oid=>1, :column_oids=>[2], :columns=>[:a], :converter=>Array, :typecaster=>Hash, :column_converters=>[Array])
129
+ p.oid.should == 1
130
+ p.column_oids.should == [2]
131
+ p.columns.should == [:a]
132
+ p.converter.should == Array
133
+ p.typecaster.should == Hash
134
+ p.column_converters.should == [Array]
135
+ end
136
+
137
+ it "should reload registered row types when reseting conversion procs" do
138
+ db = Sequel.mock(:host=>'postgres')
139
+ db.extension(:pg_row)
140
+ db.conversion_procs[4] = p4 = proc{|s| s.to_i}
141
+ db.conversion_procs[5] = p5 = proc{|s| s * 2}
142
+ db.sqls
143
+ db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
144
+ db.register_row_type(:foo)
145
+ db.sqls.should == ["SELECT pg_type.oid, typrelid, typarray FROM pg_type WHERE ((typtype = 'c') AND (typname = 'foo')) LIMIT 1",
146
+ "SELECT attname, atttypid FROM pg_attribute WHERE ((attrelid = 2) AND (attnum > 0) AND NOT attisdropped) ORDER BY attnum"]
147
+ end
148
+
149
+ it "should handle ArrayRows and HashRows in bound variables" do
150
+ @db.bound_variable_arg(1, nil).should == 1
151
+ @db.bound_variable_arg(@m::ArrayRow.call(["1", "abc\\'\","]), nil).should == '("1","abc\\\\\'\\",")'
152
+ @db.bound_variable_arg(@m::HashRow.subclass(nil, [:a, :b]).call(:a=>"1", :b=>"abc\\'\","), nil).should == '("1","abc\\\\\'\\",")'
153
+ end
154
+
155
+ it "should handle ArrayRows and HashRows in arrays in bound variables" do
156
+ @db.bound_variable_arg(1, nil).should == 1
157
+ @db.bound_variable_arg([@m::ArrayRow.call(["1", "abc\\'\","])], nil).should == '{"(\\"1\\",\\"abc\\\\\\\\\'\\\\\\",\\")"}'
158
+ @db.bound_variable_arg([@m::HashRow.subclass(nil, [:a, :b]).call(:a=>"1", :b=>"abc\\'\",")], nil).should == '{"(\\"1\\",\\"abc\\\\\\\\\'\\\\\\",\\")"}'
159
+ end
160
+
161
+ it "should allow registering row type parsers by introspecting system tables" do
162
+ @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
163
+ @db.conversion_procs[5] = p5 = proc{|s| s * 2}
164
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
165
+ @db.register_row_type(:foo)
166
+ @db.sqls.should == ["SELECT pg_type.oid, typrelid, typarray FROM pg_type WHERE ((typtype = 'c') AND (typname = 'foo')) LIMIT 1",
167
+ "SELECT attname, atttypid FROM pg_attribute WHERE ((attrelid = 2) AND (attnum > 0) AND NOT attisdropped) ORDER BY attnum"]
168
+ p1 = @db.conversion_procs[1]
169
+ p1.columns.should == [:bar, :baz]
170
+ p1.column_oids.should == [4, 5]
171
+ p1.column_converters.should == [p4, p5]
172
+ p1.oid.should == 1
173
+ @db.send(:schema_column_type, 'foo').should == :pg_row_foo
174
+ @db.send(:schema_column_type, 'integer').should == :integer
175
+
176
+ c = p1.converter
177
+ c.superclass.should == @m::HashRow
178
+ c.columns.should == [:bar, :baz]
179
+ c.db_type.should == :foo
180
+ p1.typecaster.should == c
181
+
182
+ p1.call('(1,b)').should == {:bar=>1, :baz=>'bb'}
183
+ @db.typecast_value(:pg_row_foo, %w'1 b').should be_a_kind_of(@m::HashRow)
184
+ @db.typecast_value(:pg_row_foo, %w'1 b').should == {:bar=>'1', :baz=>'b'}
185
+ @db.typecast_value(:pg_row_foo, :bar=>'1', :baz=>'b').should == {:bar=>'1', :baz=>'b'}
186
+ @db.literal(p1.call('(1,b)')).should == "ROW(1, 'bb')::foo"
187
+ end
188
+
189
+ it "should allow registering row type parsers for schema qualify types" do
190
+ @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
191
+ @db.conversion_procs[5] = p5 = proc{|s| s * 2}
192
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
193
+ @db.register_row_type(:foo__bar)
194
+ @db.sqls.should == ["SELECT pg_type.oid, typrelid, typarray FROM pg_type INNER JOIN pg_namespace ON ((pg_namespace.oid = pg_type.typnamespace) AND (pg_namespace.nspname = 'foo')) WHERE ((typtype = 'c') AND (typname = 'bar')) LIMIT 1",
195
+ "SELECT attname, atttypid FROM pg_attribute WHERE ((attrelid = 2) AND (attnum > 0) AND NOT attisdropped) ORDER BY attnum"]
196
+ p1 = @db.conversion_procs[1]
197
+ p1.columns.should == [:bar, :baz]
198
+ p1.column_oids.should == [4, 5]
199
+ p1.column_converters.should == [p4, p5]
200
+ p1.oid.should == 1
201
+
202
+ c = p1.converter
203
+ c.superclass.should == @m::HashRow
204
+ c.columns.should == [:bar, :baz]
205
+ c.db_type.should == :foo__bar
206
+ p1.typecaster.should == c
207
+
208
+ p1.call('(1,b)').should == {:bar=>1, :baz=>'bb'}
209
+ @db.typecast_value(:pg_row_foo__bar, %w'1 b').should == {:bar=>'1', :baz=>'b'}
210
+ @db.typecast_value(:pg_row_foo__bar, :bar=>'1', :baz=>'b').should == {:bar=>'1', :baz=>'b'}
211
+ @db.literal(p1.call('(1,b)')).should == "ROW(1, 'bb')::foo.bar"
212
+ end
213
+
214
+ it "should allow registering with a custom converter" do
215
+ @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
216
+ @db.conversion_procs[5] = p5 = proc{|s| s * 2}
217
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
218
+ c = proc{|h| [h]}
219
+ @db.register_row_type(:foo, :converter=>c)
220
+ o = @db.conversion_procs[1].call('(1,b)')
221
+ o.should == [{:bar=>1, :baz=>'bb'}]
222
+ o.first.should be_a_kind_of(Hash)
223
+ end
224
+
225
+ it "should allow registering with a custom typecaster" do
226
+ @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
227
+ @db.conversion_procs[5] = p5 = proc{|s| s * 2}
228
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
229
+ @db.register_row_type(:foo, :typecaster=>proc{|h| {:bar=>(h[:bar]||0).to_i, :baz=>(h[:baz] || 'a')*2}})
230
+ @db.typecast_value(:pg_row_foo, %w'1 b').should be_a_kind_of(Hash)
231
+ @db.typecast_value(:pg_row_foo, %w'1 b').should == {:bar=>1, :baz=>'bb'}
232
+ @db.typecast_value(:pg_row_foo, :bar=>'1', :baz=>'b').should == {:bar=>1, :baz=>'bb'}
233
+ @db.typecast_value(:pg_row_foo, 'bar'=>'1', 'baz'=>'b').should == {:bar=>0, :baz=>'aa'}
234
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
235
+ @db.register_row_type(:foo, :typecaster=>proc{|h| {:bar=>(h[:bar] || h['bar'] || 0).to_i, :baz=>(h[:baz] || h['baz'] || 'a')*2}})
236
+ @db.typecast_value(:pg_row_foo, %w'1 b').should == {:bar=>1, :baz=>'bb'}
237
+ @db.typecast_value(:pg_row_foo, :bar=>'1', :baz=>'b').should == {:bar=>1, :baz=>'bb'}
238
+ @db.typecast_value(:pg_row_foo, 'bar'=>'1', 'baz'=>'b').should == {:bar=>1, :baz=>'bb'}
239
+ end
240
+
241
+ it "should handle conversion procs that aren't added until later" do
242
+ @db.conversion_procs[5] = p5 = proc{|s| s * 2}
243
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
244
+ c = proc{|h| [h]}
245
+ @db.register_row_type(:foo, :converter=>c)
246
+ @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
247
+ @db.conversion_procs[1].call('(1,b)').should == [{:bar=>1, :baz=>'bb'}]
248
+ end
249
+
250
+ it "should registering array type for row type if type has an array oid" do
251
+ @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
252
+ @db.conversion_procs[5] = p5 = proc{|s| s * 2}
253
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
254
+ @db.register_row_type(:foo, :typecaster=>proc{|h| {:bar=>(h[:bar]||0).to_i, :baz=>(h[:baz] || 'a')*2}})
255
+ p3 = @db.conversion_procs[3]
256
+
257
+ p3.call('{"(1,b)"}').should == [{:bar=>1, :baz=>'bb'}]
258
+ @db.literal(p3.call('{"(1,b)"}')).should == "ARRAY[ROW(1, 'bb')::foo]::foo[]"
259
+ @db.typecast_value(:foo_array, [{:bar=>'1', :baz=>'b'}]).should == [{:bar=>1, :baz=>'bb'}]
260
+ end
261
+
262
+ it "should allow creating unregisted row types via Database#row_type" do
263
+ @db.literal(@db.row_type(:foo, [1, 2])).should == 'ROW(1, 2)::foo'
264
+ end
265
+
266
+ it "should allow typecasting of registered row types via Database#row_type" do
267
+ @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
268
+ @db.conversion_procs[5] = p5 = proc{|s| s * 2}
269
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
270
+ @db.register_row_type(:foo, :typecaster=>proc{|h| @m::HashRow.subclass(:foo, [:bar, :baz]).new({:bar=>(h[:bar]||0).to_i, :baz=>(h[:baz] || 'a')*2})})
271
+ @db.literal(@db.row_type(:foo, ['1', 'b'])).should == "ROW(1, 'bb')::foo"
272
+ @db.literal(@db.row_type(:foo, {:bar=>'1', :baz=>'b'})).should == "ROW(1, 'bb')::foo"
273
+ end
274
+
275
+ it "should allow parsing when typecasting registered row types via Database#row_type" do
276
+ @db.conversion_procs[4] = p4 = proc{|s| s.to_i}
277
+ @db.conversion_procs[5] = p5 = proc{|s| s * 2}
278
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}], [{:attname=>'bar', :atttypid=>4}, {:attname=>'baz', :atttypid=>5}]]
279
+ @db.register_row_type(:foo, :typecaster=>proc{|h| @m::HashRow.subclass(:foo, [:bar, :baz]).new(:bar=>(h[:bar]||0).to_i, :baz=>(h[:baz] || 'a')*2)})
280
+ @db.literal(@db.row_type(:foo, ['1', 'b'])).should == "ROW(1, 'bb')::foo"
281
+ end
282
+
283
+ it "should raise an error if attempt to use Database#row_type with an unregistered type and hash" do
284
+ proc{@db.literal(@db.row_type(:foo, {:bar=>'1', :baz=>'b'}))}.should raise_error(Sequel::Error)
285
+ end
286
+
287
+ it "should raise an error if attempt to use Database#row_type with an unhandled type" do
288
+ proc{@db.literal(@db.row_type(:foo, 1))}.should raise_error(Sequel::Error)
289
+ end
290
+
291
+ it "should return ArrayRow and HashRow values as-is" do
292
+ h = @m::HashRow.call(:a=>1)
293
+ a = @m::ArrayRow.call([1])
294
+ @db.row_type(:foo, h).should equal(h)
295
+ @db.row_type(:foo, a).should equal(a)
296
+ end
297
+
298
+ it "should have Sequel.pg_row return a plain ArrayRow" do
299
+ @db.literal(Sequel.pg_row([1, 2, 3])).should == 'ROW(1, 2, 3)'
300
+ end
301
+
302
+ it "should raise an error if attempting to typecast a hash for a parser without columns" do
303
+ proc{@m::Parser.new.typecast(:a=>1)}.should raise_error(Sequel::Error)
304
+ end
305
+
306
+ it "should raise an error if attempting to typecast a unhandled value for a parser" do
307
+ proc{@m::Parser.new.typecast(1)}.should raise_error(Sequel::Error)
308
+ end
309
+
310
+ it "should handle typecasting for a parser without a typecaster" do
311
+ @m::Parser.new.typecast([1]).should == [1]
312
+ end
313
+
314
+ it "should raise an error if no columns are returned when registering a custom row type" do
315
+ @db.fetch = [[{:oid=>1, :typrelid=>2, :typarray=>3}]]
316
+ proc{@db.register_row_type(:foo)}.should raise_error(Sequel::Error)
317
+ end
318
+
319
+ it "should raise an error when registering a custom row type if the type is found found" do
320
+ @db.fetch = []
321
+ proc{@db.register_row_type(:foo)}.should raise_error(Sequel::Error)
322
+ end
323
+ end