pg 1.0.0 → 1.2.3

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 (64) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/ChangeLog +0 -6595
  5. data/History.rdoc +156 -0
  6. data/Manifest.txt +8 -2
  7. data/README-Windows.rdoc +4 -4
  8. data/README.ja.rdoc +1 -2
  9. data/README.rdoc +55 -9
  10. data/Rakefile +9 -7
  11. data/Rakefile.cross +58 -57
  12. data/ext/errorcodes.def +68 -0
  13. data/ext/errorcodes.rb +1 -1
  14. data/ext/errorcodes.txt +19 -2
  15. data/ext/extconf.rb +7 -5
  16. data/ext/pg.c +141 -98
  17. data/ext/pg.h +64 -21
  18. data/ext/pg_binary_decoder.c +82 -15
  19. data/ext/pg_binary_encoder.c +13 -12
  20. data/ext/pg_coder.c +73 -12
  21. data/ext/pg_connection.c +625 -346
  22. data/ext/pg_copy_coder.c +16 -8
  23. data/ext/pg_record_coder.c +491 -0
  24. data/ext/pg_result.c +571 -191
  25. data/ext/pg_text_decoder.c +606 -40
  26. data/ext/pg_text_encoder.c +185 -54
  27. data/ext/pg_tuple.c +549 -0
  28. data/ext/pg_type_map.c +1 -1
  29. data/ext/pg_type_map_all_strings.c +4 -4
  30. data/ext/pg_type_map_by_class.c +9 -4
  31. data/ext/pg_type_map_by_column.c +7 -6
  32. data/ext/pg_type_map_by_mri_type.c +1 -1
  33. data/ext/pg_type_map_by_oid.c +3 -2
  34. data/ext/pg_type_map_in_ruby.c +1 -1
  35. data/ext/{util.c → pg_util.c} +10 -10
  36. data/ext/{util.h → pg_util.h} +2 -2
  37. data/lib/pg.rb +8 -6
  38. data/lib/pg/basic_type_mapping.rb +121 -25
  39. data/lib/pg/binary_decoder.rb +23 -0
  40. data/lib/pg/coder.rb +23 -2
  41. data/lib/pg/connection.rb +22 -3
  42. data/lib/pg/constants.rb +2 -1
  43. data/lib/pg/exceptions.rb +2 -1
  44. data/lib/pg/result.rb +14 -2
  45. data/lib/pg/text_decoder.rb +21 -26
  46. data/lib/pg/text_encoder.rb +32 -8
  47. data/lib/pg/tuple.rb +30 -0
  48. data/lib/pg/type_map_by_column.rb +3 -2
  49. data/spec/helpers.rb +52 -20
  50. data/spec/pg/basic_type_mapping_spec.rb +362 -37
  51. data/spec/pg/connection_spec.rb +376 -146
  52. data/spec/pg/connection_sync_spec.rb +41 -0
  53. data/spec/pg/result_spec.rb +240 -15
  54. data/spec/pg/tuple_spec.rb +333 -0
  55. data/spec/pg/type_map_by_class_spec.rb +2 -2
  56. data/spec/pg/type_map_by_column_spec.rb +6 -2
  57. data/spec/pg/type_map_by_mri_type_spec.rb +1 -1
  58. data/spec/pg/type_map_by_oid_spec.rb +3 -3
  59. data/spec/pg/type_map_in_ruby_spec.rb +1 -1
  60. data/spec/pg/type_map_spec.rb +1 -1
  61. data/spec/pg/type_spec.rb +363 -17
  62. data/spec/pg_spec.rb +1 -1
  63. metadata +47 -47
  64. metadata.gz.sig +0 -0
@@ -0,0 +1,41 @@
1
+ # -*- rspec -*-
2
+ #encoding: utf-8
3
+
4
+ require_relative '../helpers'
5
+
6
+ context "running with sync_* methods" do
7
+ before :each do
8
+ PG::Connection.async_api = false
9
+ end
10
+
11
+ after :each do
12
+ PG::Connection.async_api = true
13
+ end
14
+
15
+ fname = File.expand_path("../connection_spec.rb", __FILE__)
16
+ eval File.read(fname, encoding: __ENCODING__), binding, fname
17
+
18
+
19
+ it "enables/disables async/sync methods by #async_api" do
20
+ [true, false].each do |async|
21
+ PG::Connection.async_api = async
22
+
23
+ start = Time.now
24
+ t = Thread.new do
25
+ @conn.exec( 'select pg_sleep(1)' )
26
+ end
27
+ sleep 0.1
28
+
29
+ t.kill
30
+ t.join
31
+ dt = Time.now - start
32
+
33
+ if async
34
+ expect( dt ).to be < 1.0
35
+ else
36
+ expect( dt ).to be >= 1.0
37
+ end
38
+ end
39
+ end
40
+
41
+ end
@@ -1,19 +1,68 @@
1
- #!/usr/bin/env rspec
1
+ # -*- rspec -*-
2
2
  # encoding: utf-8
3
3
 
4
4
  require_relative '../helpers'
5
5
 
6
6
  require 'pg'
7
+ require 'objspace'
7
8
 
8
9
 
9
10
  describe PG::Result do
10
11
 
12
+ describe :field_name_type do
13
+ let!(:res) { @conn.exec('SELECT 1 AS a, 2 AS "B"') }
14
+
15
+ it "uses string field names per default" do
16
+ expect(res.field_name_type).to eq(:string)
17
+ end
18
+
19
+ it "can set string field names" do
20
+ res.field_name_type = :string
21
+ expect(res.field_name_type).to eq(:string)
22
+ end
23
+
24
+ it "can set symbol field names" do
25
+ res.field_name_type = :symbol
26
+ expect(res.field_name_type).to eq(:symbol)
27
+ end
28
+
29
+ it "can set static_symbol field names" do
30
+ res.field_name_type = :static_symbol
31
+ expect(res.field_name_type).to eq(:static_symbol)
32
+ end
33
+
34
+ it "can't set symbol field names after #fields" do
35
+ res.fields
36
+ expect{ res.field_name_type = :symbol }.to raise_error(ArgumentError, /already materialized/)
37
+ expect(res.field_name_type).to eq(:string)
38
+ end
39
+
40
+ it "can't set invalid values" do
41
+ expect{ res.field_name_type = :sym }.to raise_error(ArgumentError, /invalid argument :sym/)
42
+ expect{ res.field_name_type = "symbol" }.to raise_error(ArgumentError, /invalid argument "symbol"/)
43
+ end
44
+ end
45
+
11
46
  it "acts as an array of hashes" do
12
47
  res = @conn.exec("SELECT 1 AS a, 2 AS b")
13
48
  expect( res[0]['a'] ).to eq( '1' )
14
49
  expect( res[0]['b'] ).to eq( '2' )
15
50
  end
16
51
 
52
+ it "acts as an array of hashes with symbols" do
53
+ res = @conn.exec("SELECT 1 AS a, 2 AS b")
54
+ res.field_name_type = :symbol
55
+ expect( res[0][:a] ).to eq( '1' )
56
+ expect( res[0][:b] ).to eq( '2' )
57
+ end
58
+
59
+ it "acts as an array of hashes with static_symbols" do
60
+ res = @conn.exec("SELECT 1 AS a, 2 AS b")
61
+ res.field_name_type = :static_symbol
62
+ expect( res[0][:a] ).to eq( '1' )
63
+ expect( res[0][:b] ).to eq( '2' )
64
+ end
65
+
17
66
  it "yields a row as an array" do
18
67
  res = @conn.exec("SELECT 1 AS a, 2 AS b")
19
68
  list = []
@@ -25,7 +74,6 @@ describe PG::Result do
25
74
  res = @conn.exec("SELECT 1 AS a, 2 AS b")
26
75
  e = res.each_row
27
76
  expect( e ).to be_a_kind_of(Enumerator)
28
- pending "Rubinius doesn't define RETURN_SIZED_ENUMERATOR()" if RUBY_ENGINE=='rbx'
29
77
  expect( e.size ).to eq( 1 )
30
78
  expect( e.to_a ).to eq [['1', '2']]
31
79
  end
@@ -34,13 +82,20 @@ describe PG::Result do
34
82
  res = @conn.exec("SELECT 1 AS a, 2 AS b")
35
83
  e = res.each
36
84
  expect( e ).to be_a_kind_of(Enumerator)
37
- pending "Rubinius doesn't define RETURN_SIZED_ENUMERATOR()" if RUBY_ENGINE=='rbx'
38
85
  expect( e.size ).to eq( 1 )
39
86
  expect( e.to_a ).to eq [{'a'=>'1', 'b'=>'2'}]
40
87
  end
41
88
 
42
- context "result streaming" do
43
- it "can iterate over all tuples in single row mode" do
89
+ it "yields a row as an Enumerator of hashs with symbols" do
90
+ res = @conn.exec("SELECT 1 AS a, 2 AS b")
91
+ res.field_name_type = :symbol
92
+ expect( res.each.to_a ).to eq [{:a=>'1', :b=>'2'}]
93
+ end
94
+
95
+ context "result streaming in single row mode" do
96
+ let!(:textdec_int){ PG::TextDecoder::Integer.new name: 'INT4', oid: 23 }
97
+
98
+ it "can iterate over all rows as Hash" do
44
99
  @conn.send_query( "SELECT generate_series(2,4) AS a; SELECT 1 AS b, generate_series(5,6) AS c" )
45
100
  @conn.set_single_row_mode
46
101
  expect(
@@ -56,7 +111,32 @@ describe PG::Result do
56
111
  expect( @conn.get_result ).to be_nil
57
112
  end
58
113
 
59
- it "can iterate over all rows in single row mode" do
114
+ it "can iterate over all rows as Hash with symbols and typemap" do
115
+ @conn.send_query( "SELECT generate_series(2,4) AS a" )
116
+ @conn.set_single_row_mode
117
+ res = @conn.get_result.field_names_as(:symbol)
118
+ res.type_map = PG::TypeMapByColumn.new [textdec_int]
119
+ expect(
120
+ res.stream_each.to_a
121
+ ).to eq(
122
+ [{:a=>2}, {:a=>3}, {:a=>4}]
123
+ )
124
+ expect( @conn.get_result ).to be_nil
125
+ end
126
+
127
+ it "keeps last result on error while iterating stream_each" do
128
+ @conn.send_query( "SELECT generate_series(2,4) AS a" )
129
+ @conn.set_single_row_mode
130
+ res = @conn.get_result
131
+ expect do
132
+ res.stream_each_row do
133
+ raise ZeroDivisionError
134
+ end
135
+ end.to raise_error(ZeroDivisionError)
136
+ expect( res.values ).to eq([["2"]])
137
+ end
138
+
139
+ it "can iterate over all rows as Array" do
60
140
  @conn.send_query( "SELECT generate_series(2,4) AS a; SELECT 1 AS b, generate_series(5,6) AS c" )
61
141
  @conn.set_single_row_mode
62
142
  expect(
@@ -72,6 +152,64 @@ describe PG::Result do
72
152
  expect( @conn.get_result ).to be_nil
73
153
  end
74
154
 
155
+ it "keeps last result on error while iterating stream_each_row" do
156
+ @conn.send_query( "SELECT generate_series(2,4) AS a" )
157
+ @conn.set_single_row_mode
158
+ res = @conn.get_result
159
+ expect do
160
+ res.stream_each_row do
161
+ raise ZeroDivisionError
162
+ end
163
+ end.to raise_error(ZeroDivisionError)
164
+ expect( res.values ).to eq([["2"]])
165
+ end
166
+
167
+ it "can iterate over all rows as PG::Tuple" do
168
+ @conn.send_query( "SELECT generate_series(2,4) AS a; SELECT 1 AS b, generate_series(5,6) AS c" )
169
+ @conn.set_single_row_mode
170
+ tuples = @conn.get_result.stream_each_tuple.to_a
171
+ expect( tuples[0][0] ).to eq( "2" )
172
+ expect( tuples[1]["a"] ).to eq( "3" )
173
+ expect( tuples.size ).to eq( 3 )
174
+
175
+ tuples = @conn.get_result.enum_for(:stream_each_tuple).to_a
176
+ expect( tuples[-1][-1] ).to eq( "6" )
177
+ expect( tuples[-2]["b"] ).to eq( "1" )
178
+ expect( tuples.size ).to eq( 2 )
179
+
180
+ expect( @conn.get_result ).to be_nil
181
+ end
182
+
183
+ it "clears result on error while iterating stream_each_tuple" do
184
+ @conn.send_query( "SELECT generate_series(2,4) AS a" )
185
+ @conn.set_single_row_mode
186
+ res = @conn.get_result
187
+ expect do
188
+ res.stream_each_tuple do
189
+ raise ZeroDivisionError
190
+ end
191
+ end.to raise_error(ZeroDivisionError)
192
+ expect( res.cleared? ).to eq(true)
193
+ end
194
+
195
+ it "should reuse field names in stream_each_tuple" do
196
+ @conn.send_query( "SELECT generate_series(2,3) AS a" )
197
+ @conn.set_single_row_mode
198
+ tuple1, tuple2 = *@conn.get_result.stream_each_tuple.to_a
199
+ expect( tuple1.keys[0].object_id ).to eq(tuple2.keys[0].object_id)
200
+ end
201
+
202
+ it "can iterate over all rows as PG::Tuple with symbols and typemap" do
203
+ @conn.send_query( "SELECT generate_series(2,4) AS a" )
204
+ @conn.set_single_row_mode
205
+ res = @conn.get_result.field_names_as(:symbol)
206
+ res.type_map = PG::TypeMapByColumn.new [textdec_int]
207
+ tuples = res.stream_each_tuple.to_a
208
+ expect( tuples[0][0] ).to eq( 2 )
209
+ expect( tuples[1][:a] ).to eq( 3 )
210
+ expect( @conn.get_result ).to be_nil
211
+ end
212
+
75
213
  it "complains when not in single row mode" do
76
214
  @conn.send_query( "SELECT generate_series(2,4)" )
77
215
  expect{
@@ -96,7 +234,7 @@ describe PG::Result do
96
234
  end
97
235
 
98
236
  it "inserts nil AS NULL and return NULL as nil" do
99
- res = @conn.exec("SELECT $1::int AS n", [nil])
237
+ res = @conn.exec_params("SELECT $1::int AS n", [nil])
100
238
  expect( res[0]['n'] ).to be_nil()
101
239
  end
102
240
 
@@ -131,6 +269,17 @@ describe PG::Result do
131
269
  ).to match( /^parserOpenTable$|^RangeVarGetRelid$/ )
132
270
  end
133
271
 
272
+ it "encapsulates PG_DIAG_SEVERITY_NONLOCALIZED error in a PG::Error object", :postgresql_96 do
273
+ result = nil
274
+ begin
275
+ @conn.exec( "SELECT * FROM nonexistant_table" )
276
+ rescue PG::Error => err
277
+ result = err.result
278
+ end
279
+
280
+ expect( result.error_field(PG::PG_DIAG_SEVERITY_NONLOCALIZED) ).to eq( 'ERROR' )
281
+ end
282
+
134
283
  it "encapsulates database object names for integrity constraint violations", :postgresql_93 do
135
284
  @conn.exec( "CREATE TABLE integrity (id SERIAL PRIMARY KEY)" )
136
285
  exception = nil
@@ -151,17 +300,39 @@ describe PG::Result do
151
300
  it "detects division by zero as SQLSTATE 22012" do
152
301
  sqlstate = nil
153
302
  begin
154
- res = @conn.exec("SELECT 1/0")
303
+ @conn.exec("SELECT 1/0")
155
304
  rescue PG::Error => e
156
305
  sqlstate = e.result.result_error_field( PG::PG_DIAG_SQLSTATE ).to_i
157
306
  end
158
307
  expect( sqlstate ).to eq( 22012 )
159
308
  end
160
309
 
310
+ it "provides the error message" do
311
+ @conn.send_query("SELECT xyz")
312
+ res = @conn.get_result; @conn.get_result
313
+ expect( res.error_message ).to match(/"xyz"/)
314
+ expect( res.result_error_message ).to match(/"xyz"/)
315
+ end
316
+
317
+ it "provides a verbose error message", :postgresql_96 do
318
+ @conn.send_query("SELECT xyz")
319
+ res = @conn.get_result; @conn.get_result
320
+ # PQERRORS_TERSE should give a single line result
321
+ expect( res.verbose_error_message(PG::PQERRORS_TERSE, PG::PQSHOW_CONTEXT_ALWAYS) ).to match(/\A.*\n\z/)
322
+ # PQERRORS_VERBOSE should give a multi line result
323
+ expect( res.result_verbose_error_message(PG::PQERRORS_VERBOSE, PG::PQSHOW_CONTEXT_NEVER) ).to match(/\n.*\n/)
324
+ end
325
+
326
+ it "provides a verbose error message with SQLSTATE", :postgresql_12 do
327
+ @conn.send_query("SELECT xyz")
328
+ res = @conn.get_result; @conn.get_result
329
+ expect( res.verbose_error_message(PG::PQERRORS_SQLSTATE, PG::PQSHOW_CONTEXT_NEVER) ).to match(/42703/)
330
+ end
331
+
161
332
  it "returns the same bytes in binary format that are sent in binary format" do
162
333
  binary_file = File.join(Dir.pwd, 'spec/data', 'random_binary_data')
163
334
  bytes = File.open(binary_file, 'rb').read
164
- res = @conn.exec('VALUES ($1::bytea)',
335
+ res = @conn.exec_params('VALUES ($1::bytea)',
165
336
  [ { :value => bytes, :format => 1 } ], 1)
166
337
  expect( res[0]['column1'] ).to eq( bytes )
167
338
  expect( res.getvalue(0,0) ).to eq( bytes )
@@ -173,7 +344,7 @@ describe PG::Result do
173
344
  binary_file = File.join(Dir.pwd, 'spec/data', 'random_binary_data')
174
345
  bytes = File.open(binary_file, 'rb').read
175
346
  @conn.exec("SET standard_conforming_strings=on")
176
- res = @conn.exec("VALUES ('#{PG::Connection.escape_bytea(bytes)}'::bytea)", [], 1)
347
+ res = @conn.exec_params("VALUES ('#{PG::Connection.escape_bytea(bytes)}'::bytea)", [], 1)
177
348
  expect( res[0]['column1'] ).to eq( bytes )
178
349
  expect( res.getvalue(0,0) ).to eq( bytes )
179
350
  expect( res.values[0][0] ).to eq( bytes )
@@ -183,7 +354,7 @@ describe PG::Result do
183
354
  it "returns the same bytes in text format that are sent in binary format" do
184
355
  binary_file = File.join(Dir.pwd, 'spec/data', 'random_binary_data')
185
356
  bytes = File.open(binary_file, 'rb').read
186
- res = @conn.exec('VALUES ($1::bytea)',
357
+ res = @conn.exec_params('VALUES ($1::bytea)',
187
358
  [ { :value => bytes, :format => 1 } ])
188
359
  expect( PG::Connection.unescape_bytea(res[0]['column1']) ).to eq( bytes )
189
360
  end
@@ -194,7 +365,7 @@ describe PG::Result do
194
365
 
195
366
  out_bytes = nil
196
367
  @conn.exec("SET standard_conforming_strings=on")
197
- res = @conn.exec("VALUES ('#{PG::Connection.escape_bytea(in_bytes)}'::bytea)", [], 0)
368
+ res = @conn.exec_params("VALUES ('#{PG::Connection.escape_bytea(in_bytes)}'::bytea)", [], 0)
198
369
  out_bytes = PG::Connection.unescape_bytea(res[0]['column1'])
199
370
  expect( out_bytes ).to eq( in_bytes )
200
371
  end
@@ -205,10 +376,10 @@ describe PG::Result do
205
376
  res = @conn.describe_prepared( 'queryfinder' )
206
377
 
207
378
  expect(
208
- @conn.exec( 'SELECT format_type($1, -1)', [res.paramtype(0)] ).getvalue( 0, 0 )
379
+ @conn.exec_params( 'SELECT format_type($1, -1)', [res.paramtype(0)] ).getvalue( 0, 0 )
209
380
  ).to eq( 'name' )
210
381
  expect(
211
- @conn.exec( 'SELECT format_type($1, -1)', [res.paramtype(1)] ).getvalue( 0, 0 )
382
+ @conn.exec_params( 'SELECT format_type($1, -1)', [res.paramtype(1)] ).getvalue( 0, 0 )
212
383
  ).to eq( 'text' )
213
384
  end
214
385
 
@@ -242,6 +413,32 @@ describe PG::Result do
242
413
  expect( res.values ).to eq( [ ["bar"], ["bar2"] ] )
243
414
  end
244
415
 
416
+ it "can retrieve field names" do
417
+ res = @conn.exec('SELECT 1 AS a, 2 AS "B"')
418
+ expect(res.fields).to eq(["a", "B"])
419
+ end
420
+
421
+ it "can retrieve field names as symbols" do
422
+ res = @conn.exec('SELECT 1 AS a, 2 AS "B"')
423
+ res.field_name_type = :symbol
424
+ expect(res.fields).to eq([:a, :B])
425
+ end
426
+
427
+ it "can retrieve single field names" do
428
+ res = @conn.exec('SELECT 1 AS a, 2 AS "B"')
429
+ expect(res.fname(0)).to eq("a")
430
+ expect(res.fname(1)).to eq("B")
431
+ expect{res.fname(2)}.to raise_error(ArgumentError)
432
+ end
433
+
434
+ it "can retrieve single field names as symbol" do
435
+ res = @conn.exec('SELECT 1 AS a, 2 AS "B"')
436
+ res.field_name_type = :symbol
437
+ expect(res.fname(0)).to eq(:a)
438
+ expect(res.fname(1)).to eq(:B)
439
+ expect{res.fname(2)}.to raise_error(ArgumentError)
440
+ end
441
+
245
442
  # PQfmod
246
443
  it "can return the type modifier for a result column" do
247
444
  @conn.exec( 'CREATE TABLE fmodtest ( foo varchar(33) )' )
@@ -339,8 +536,27 @@ describe PG::Result do
339
536
  res = @conn.exec( "SELECT 1 AS x, 'a' AS y UNION ALL SELECT 2, 'b'" )
340
537
  expect( res.field_values('x') ).to eq( ['1', '2'] )
341
538
  expect( res.field_values('y') ).to eq( ['a', 'b'] )
539
+ expect( res.field_values(:x) ).to eq( ['1', '2'] )
342
540
  expect{ res.field_values('') }.to raise_error(IndexError)
343
- expect{ res.field_values(:x) }.to raise_error(TypeError)
541
+ expect{ res.field_values(0) }.to raise_error(TypeError)
542
+ end
543
+
544
+ it "can return the values of a single tuple" do
545
+ res = @conn.exec( "SELECT 1 AS x, 'a' AS y UNION ALL SELECT 2, 'b'" )
546
+ expect( res.tuple_values(0) ).to eq( ['1', 'a'] )
547
+ expect( res.tuple_values(1) ).to eq( ['2', 'b'] )
548
+ expect{ res.tuple_values(2) }.to raise_error(IndexError)
549
+ expect{ res.tuple_values(-1) }.to raise_error(IndexError)
550
+ expect{ res.tuple_values("x") }.to raise_error(TypeError)
551
+ end
552
+
553
+ it "can return the values of a single vary lazy tuple" do
554
+ res = @conn.exec( "VALUES(1),(2)" )
555
+ expect( res.tuple(0) ).to be_kind_of( PG::Tuple )
556
+ expect( res.tuple(1) ).to be_kind_of( PG::Tuple )
557
+ expect{ res.tuple(2) }.to raise_error(IndexError)
558
+ expect{ res.tuple(-1) }.to raise_error(IndexError)
559
+ expect{ res.tuple("x") }.to raise_error(TypeError)
344
560
  end
345
561
 
346
562
  it "raises a proper exception for a nonexistant table" do
@@ -410,6 +626,13 @@ describe PG::Result do
410
626
  expect( r.inspect ).to match(/cleared/)
411
627
  end
412
628
 
629
+ it "should give account about memory usage" do
630
+ r = @conn.exec "select 1"
631
+ expect( ObjectSpace.memsize_of(r) ).to be > 1000
632
+ r.clear
633
+ expect( ObjectSpace.memsize_of(r) ).to be < 100
634
+ end
635
+
413
636
  context 'result value conversions with TypeMapByColumn' do
414
637
  let!(:textdec_int){ PG::TextDecoder::Integer.new name: 'INT4', oid: 23 }
415
638
  let!(:textdec_float){ PG::TextDecoder::Float.new name: 'FLOAT4', oid: 700 }
@@ -436,6 +659,8 @@ describe PG::Result do
436
659
  expect( res.enum_for(:each).to_a ).to eq( [{'f' => 123}] )
437
660
  expect( res.column_values(0) ).to eq( [123] )
438
661
  expect( res.field_values('f') ).to eq( [123] )
662
+ expect( res.field_values(:f) ).to eq( [123] )
663
+ expect( res.tuple_values(0) ).to eq( [123] )
439
664
  end
440
665
 
441
666
  it "should be usable for several querys" do
@@ -0,0 +1,333 @@
1
+ # -*- rspec -*-
2
+ # encoding: utf-8
3
+
4
+ require_relative '../helpers'
5
+ require 'pg'
6
+ require 'objspace'
7
+
8
+ describe PG::Tuple do
9
+ let!(:typemap) { PG::BasicTypeMapForResults.new(@conn) }
10
+ let!(:result2x2) { @conn.exec( "VALUES(1, 'a'), (2, 'b')" ) }
11
+ let!(:result2x2sym) { @conn.exec( "VALUES(1, 'a'), (2, 'b')" ).field_names_as(:symbol) }
12
+ let!(:result2x3cast) do
13
+ @conn.exec( "SELECT * FROM (VALUES(1, TRUE, '3'), (2, FALSE, '4')) AS m (a, b, b)" )
14
+ .map_types!(typemap)
15
+ end
16
+ let!(:result2x3symcast) do
17
+ @conn.exec( "SELECT * FROM (VALUES(1, TRUE, '3'), (2, FALSE, '4')) AS m (a, b, b)" )
18
+ .map_types!(typemap)
19
+ .field_names_as(:symbol)
20
+ end
21
+ let!(:tuple0) { result2x2.tuple(0) }
22
+ let!(:tuple0sym) { result2x2sym.tuple(0) }
23
+ let!(:tuple1) { result2x2.tuple(1) }
24
+ let!(:tuple1sym) { result2x2sym.tuple(1) }
25
+ let!(:tuple2) { result2x3cast.tuple(0) }
26
+ let!(:tuple2sym) { result2x3symcast.tuple(0) }
27
+ let!(:tuple3) { str = Marshal.dump(result2x3cast.tuple(1)); Marshal.load(str) }
28
+ let!(:tuple_empty) { PG::Tuple.new }
29
+
30
+ describe "[]" do
31
+ it "returns nil for invalid keys" do
32
+ expect( tuple0["x"] ).to be_nil
33
+ expect( tuple0[0.5] ).to be_nil
34
+ expect( tuple0[2] ).to be_nil
35
+ expect( tuple0[-3] ).to be_nil
36
+ expect( tuple2[-4] ).to be_nil
37
+ expect{ tuple_empty[0] }.to raise_error(TypeError)
38
+ end
39
+
40
+ it "supports array like access" do
41
+ expect( tuple0[0] ).to eq( "1" )
42
+ expect( tuple0[1] ).to eq( "a" )
43
+ expect( tuple1[0] ).to eq( "2" )
44
+ expect( tuple1[1] ).to eq( "b" )
45
+ expect( tuple2[0] ).to eq( 1 )
46
+ expect( tuple2[1] ).to eq( true )
47
+ expect( tuple2[2] ).to eq( "3" )
48
+ expect( tuple3[0] ).to eq( 2 )
49
+ expect( tuple3[1] ).to eq( false )
50
+ expect( tuple3[2] ).to eq( "4" )
51
+ end
52
+
53
+ it "supports negative indices" do
54
+ expect( tuple0[-2] ).to eq( "1" )
55
+ expect( tuple0[-1] ).to eq( "a" )
56
+ expect( tuple2[-3] ).to eq( 1 )
57
+ expect( tuple2[-2] ).to eq( true )
58
+ expect( tuple2[-1] ).to eq( "3" )
59
+ end
60
+
61
+ it "supports hash like access" do
62
+ expect( tuple0["column1"] ).to eq( "1" )
63
+ expect( tuple0["column2"] ).to eq( "a" )
64
+ expect( tuple2["a"] ).to eq( 1 )
65
+ expect( tuple2["b"] ).to eq( "3" )
66
+ expect( tuple0[:b] ).to be_nil
67
+ expect( tuple0["x"] ).to be_nil
68
+ end
69
+
70
+ it "supports hash like access with symbols" do
71
+ expect( tuple0sym[:column1] ).to eq( "1" )
72
+ expect( tuple0sym[:column2] ).to eq( "a" )
73
+ expect( tuple2sym[:a] ).to eq( 1 )
74
+ expect( tuple2sym[:b] ).to eq( "3" )
75
+ expect( tuple2sym["b"] ).to be_nil
76
+ expect( tuple0sym[:x] ).to be_nil
77
+ end
78
+
79
+ it "casts lazy and caches result" do
80
+ a = []
81
+ deco = Class.new(PG::SimpleDecoder) do
82
+ define_method(:decode) do |*args|
83
+ a << args
84
+ args.last
85
+ end
86
+ end.new
87
+
88
+ result2x2.map_types!(PG::TypeMapByColumn.new([deco, deco]))
89
+ t = result2x2.tuple(1)
90
+
91
+ # cast and cache at first call to [0]
92
+ a.clear
93
+ expect( t[0] ).to eq( 0 )
94
+ expect( a ).to eq([["2", 1, 0]])
95
+
96
+ # use cache at second call to [0]
97
+ a.clear
98
+ expect( t[0] ).to eq( 0 )
99
+ expect( a ).to eq([])
100
+
101
+ # cast and cache at first call to [1]
102
+ a.clear
103
+ expect( t[1] ).to eq( 1 )
104
+ expect( a ).to eq([["b", 1, 1]])
105
+ end
106
+ end
107
+
108
+ describe "fetch" do
109
+ it "raises proper errors for invalid keys" do
110
+ expect{ tuple0.fetch("x") }.to raise_error(KeyError)
111
+ expect{ tuple0.fetch(0.5) }.to raise_error(KeyError)
112
+ expect{ tuple0.fetch(2) }.to raise_error(IndexError)
113
+ expect{ tuple0.fetch(-3) }.to raise_error(IndexError)
114
+ expect{ tuple0.fetch(-3) }.to raise_error(IndexError)
115
+ expect{ tuple2.fetch(-4) }.to raise_error(IndexError)
116
+ expect{ tuple_empty[0] }.to raise_error(TypeError)
117
+ end
118
+
119
+ it "supports array like access" do
120
+ expect( tuple0.fetch(0) ).to eq( "1" )
121
+ expect( tuple0.fetch(1) ).to eq( "a" )
122
+ expect( tuple2.fetch(0) ).to eq( 1 )
123
+ expect( tuple2.fetch(1) ).to eq( true )
124
+ expect( tuple2.fetch(2) ).to eq( "3" )
125
+ end
126
+
127
+ it "supports default value for indices" do
128
+ expect( tuple0.fetch(2, 42) ).to eq( 42 )
129
+ expect( tuple0.fetch(2){43} ).to eq( 43 )
130
+ end
131
+
132
+ it "supports negative indices" do
133
+ expect( tuple0.fetch(-2) ).to eq( "1" )
134
+ expect( tuple0.fetch(-1) ).to eq( "a" )
135
+ expect( tuple2.fetch(-3) ).to eq( 1 )
136
+ expect( tuple2.fetch(-2) ).to eq( true )
137
+ expect( tuple2.fetch(-1) ).to eq( "3" )
138
+ end
139
+
140
+ it "supports hash like access" do
141
+ expect( tuple0.fetch("column1") ).to eq( "1" )
142
+ expect( tuple0.fetch("column2") ).to eq( "a" )
143
+ expect( tuple2.fetch("a") ).to eq( 1 )
144
+ expect( tuple2.fetch("b") ).to eq( "3" )
145
+ end
146
+
147
+ it "supports default value for name keys" do
148
+ expect( tuple0.fetch("x", "defa") ).to eq("defa")
149
+ expect( tuple0.fetch("x"){"defa"} ).to eq("defa")
150
+ end
151
+ end
152
+
153
+ describe "each" do
154
+ it "can be used as an enumerator" do
155
+ expect( tuple0.each ).to be_kind_of(Enumerator)
156
+ expect( tuple0.each.to_a ).to eq( [["column1", "1"], ["column2", "a"]] )
157
+ expect( tuple1.each.to_a ).to eq( [["column1", "2"], ["column2", "b"]] )
158
+ expect( tuple2.each.to_a ).to eq( [["a", 1], ["b", true], ["b", "3"]] )
159
+ expect( tuple3.each.to_a ).to eq( [["a", 2], ["b", false], ["b", "4"]] )
160
+ expect{ tuple_empty.each }.to raise_error(TypeError)
161
+ end
162
+
163
+ it "can be used as an enumerator with symbols" do
164
+ expect( tuple0sym.each ).to be_kind_of(Enumerator)
165
+ expect( tuple0sym.each.to_a ).to eq( [[:column1, "1"], [:column2, "a"]] )
166
+ expect( tuple2sym.each.to_a ).to eq( [[:a, 1], [:b, true], [:b, "3"]] )
167
+ end
168
+
169
+ it "can be used with block" do
170
+ a = []
171
+ tuple0.each do |*v|
172
+ a << v
173
+ end
174
+ expect( a ).to eq( [["column1", "1"], ["column2", "a"]] )
175
+ end
176
+ end
177
+
178
+ describe "each_value" do
179
+ it "can be used as an enumerator" do
180
+ expect( tuple0.each_value ).to be_kind_of(Enumerator)
181
+ expect( tuple0.each_value.to_a ).to eq( ["1", "a"] )
182
+ expect( tuple1.each_value.to_a ).to eq( ["2", "b"] )
183
+ expect( tuple2.each_value.to_a ).to eq( [1, true, "3"] )
184
+ expect( tuple3.each_value.to_a ).to eq( [2, false, "4"] )
185
+ expect{ tuple_empty.each_value }.to raise_error(TypeError)
186
+ end
187
+
188
+ it "can be used with block" do
189
+ a = []
190
+ tuple0.each_value do |v|
191
+ a << v
192
+ end
193
+ expect( a ).to eq( ["1", "a"] )
194
+ end
195
+ end
196
+
197
+ it "responds to values" do
198
+ expect( tuple0.values ).to eq( ["1", "a"] )
199
+ expect( tuple3.values ).to eq( [2, false, "4"] )
200
+ expect{ tuple_empty.values }.to raise_error(TypeError)
201
+ end
202
+
203
+ it "responds to key?" do
204
+ expect( tuple1.key?("column1") ).to eq( true )
205
+ expect( tuple1.key?(:column1) ).to eq( false )
206
+ expect( tuple1.key?("other") ).to eq( false )
207
+ expect( tuple1.has_key?("column1") ).to eq( true )
208
+ expect( tuple1.has_key?("other") ).to eq( false )
209
+ end
210
+
211
+ it "responds to key? as symbol" do
212
+ expect( tuple1sym.key?(:column1) ).to eq( true )
213
+ expect( tuple1sym.key?("column1") ).to eq( false )
214
+ expect( tuple1sym.key?(:other) ).to eq( false )
215
+ expect( tuple1sym.has_key?(:column1) ).to eq( true )
216
+ expect( tuple1sym.has_key?(:other) ).to eq( false )
217
+ end
218
+
219
+ it "responds to keys" do
220
+ expect( tuple0.keys ).to eq( ["column1", "column2"] )
221
+ expect( tuple2.keys ).to eq( ["a", "b", "b"] )
222
+ end
223
+
224
+ it "responds to keys as symbol" do
225
+ expect( tuple0sym.keys ).to eq( [:column1, :column2] )
226
+ expect( tuple2sym.keys ).to eq( [:a, :b, :b] )
227
+ end
228
+
229
+ describe "each_key" do
230
+ it "can be used as an enumerator" do
231
+ expect( tuple0.each_key ).to be_kind_of(Enumerator)
232
+ expect( tuple0.each_key.to_a ).to eq( ["column1", "column2"] )
233
+ expect( tuple2.each_key.to_a ).to eq( ["a", "b", "b"] )
234
+ end
235
+
236
+ it "can be used with block" do
237
+ a = []
238
+ tuple0.each_key do |v|
239
+ a << v
240
+ end
241
+ expect( a ).to eq( ["column1", "column2"] )
242
+ end
243
+ end
244
+
245
+ it "responds to length" do
246
+ expect( tuple0.length ).to eq( 2 )
247
+ expect( tuple0.size ).to eq( 2 )
248
+ expect( tuple2.size ).to eq( 3 )
249
+ end
250
+
251
+ it "responds to index" do
252
+ expect( tuple0.index("column1") ).to eq( 0 )
253
+ expect( tuple0.index(:column1) ).to eq( nil )
254
+ expect( tuple0.index("column2") ).to eq( 1 )
255
+ expect( tuple0.index("x") ).to eq( nil )
256
+ expect( tuple2.index("a") ).to eq( 0 )
257
+ expect( tuple2.index("b") ).to eq( 2 )
258
+ end
259
+
260
+ it "responds to index with symbol" do
261
+ expect( tuple0sym.index(:column1) ).to eq( 0 )
262
+ expect( tuple0sym.index("column1") ).to eq( nil )
263
+ expect( tuple0sym.index(:column2) ).to eq( 1 )
264
+ expect( tuple0sym.index(:x) ).to eq( nil )
265
+ expect( tuple2sym.index(:a) ).to eq( 0 )
266
+ expect( tuple2sym.index(:b) ).to eq( 2 )
267
+ end
268
+
269
+ it "can be used as Enumerable" do
270
+ expect( tuple0.to_a ).to eq( [["column1", "1"], ["column2", "a"]] )
271
+ expect( tuple1.to_a ).to eq( [["column1", "2"], ["column2", "b"]] )
272
+ expect( tuple2.to_a ).to eq( [["a", 1], ["b", true], ["b", "3"]] )
273
+ expect( tuple3.to_a ).to eq( [["a", 2], ["b", false], ["b", "4"]] )
274
+ end
275
+
276
+ it "can be marshaled" do
277
+ [tuple0, tuple1, tuple2, tuple3, tuple0sym, tuple2sym].each do |t1|
278
+ str = Marshal.dump(t1)
279
+ t2 = Marshal.load(str)
280
+
281
+ expect( t2 ).to be_kind_of(t1.class)
282
+ expect( t2 ).not_to equal(t1)
283
+ expect( t2.to_a ).to eq(t1.to_a)
284
+ end
285
+ end
286
+
287
+ it "passes instance variables when marshaled" do
288
+ t1 = tuple0
289
+ t1.instance_variable_set("@a", 4711)
290
+ str = Marshal.dump(t1)
291
+ t2 = Marshal.load(str)
292
+
293
+ expect( t2.instance_variable_get("@a") ).to eq( 4711 )
294
+ end
295
+
296
+ it "can't be marshaled when empty" do
297
+ expect{ Marshal.dump(tuple_empty) }.to raise_error(TypeError)
298
+ end
299
+
300
+ it "should give account about memory usage" do
301
+ expect( ObjectSpace.memsize_of(tuple0) ).to be > 40
302
+ expect( ObjectSpace.memsize_of(tuple_empty) ).to be > 0
303
+ end
304
+
305
+ it "should override #inspect" do
306
+ expect( tuple1.inspect ).to eq('#<PG::Tuple column1: "2", column2: "b">')
307
+ expect( tuple2.inspect ).to eq('#<PG::Tuple a: 1, b: true, b: "3">')
308
+ expect( tuple2sym.inspect ).to eq('#<PG::Tuple a: 1, b: true, b: "3">')
309
+ expect{ tuple_empty.inspect }.to raise_error(TypeError)
310
+ end
311
+
312
+ context "with cleared result" do
313
+ it "should raise an error when non-materialized fields are used" do
314
+ r = result2x2
315
+ t = r.tuple(0)
316
+ t[0] # materialize first field only
317
+ r.clear
318
+
319
+ # second column should fail
320
+ expect{ t[1] }.to raise_error(PG::Error)
321
+ expect{ t.fetch(1) }.to raise_error(PG::Error)
322
+ expect{ t.fetch("column2") }.to raise_error(PG::Error)
323
+
324
+ # first column should succeed
325
+ expect( t[0] ).to eq( "1" )
326
+ expect( t.fetch(0) ).to eq( "1" )
327
+ expect( t.fetch("column1") ).to eq( "1" )
328
+
329
+ # should fail due to the second column
330
+ expect{ t.values }.to raise_error(PG::Error)
331
+ end
332
+ end
333
+ end