sequel 3.37.0 → 3.38.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 (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