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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/ChangeLog +0 -6595
- data/History.rdoc +156 -0
- data/Manifest.txt +8 -2
- data/README-Windows.rdoc +4 -4
- data/README.ja.rdoc +1 -2
- data/README.rdoc +55 -9
- data/Rakefile +9 -7
- data/Rakefile.cross +58 -57
- data/ext/errorcodes.def +68 -0
- data/ext/errorcodes.rb +1 -1
- data/ext/errorcodes.txt +19 -2
- data/ext/extconf.rb +7 -5
- data/ext/pg.c +141 -98
- data/ext/pg.h +64 -21
- data/ext/pg_binary_decoder.c +82 -15
- data/ext/pg_binary_encoder.c +13 -12
- data/ext/pg_coder.c +73 -12
- data/ext/pg_connection.c +625 -346
- data/ext/pg_copy_coder.c +16 -8
- data/ext/pg_record_coder.c +491 -0
- data/ext/pg_result.c +571 -191
- data/ext/pg_text_decoder.c +606 -40
- data/ext/pg_text_encoder.c +185 -54
- data/ext/pg_tuple.c +549 -0
- data/ext/pg_type_map.c +1 -1
- data/ext/pg_type_map_all_strings.c +4 -4
- data/ext/pg_type_map_by_class.c +9 -4
- data/ext/pg_type_map_by_column.c +7 -6
- data/ext/pg_type_map_by_mri_type.c +1 -1
- data/ext/pg_type_map_by_oid.c +3 -2
- data/ext/pg_type_map_in_ruby.c +1 -1
- data/ext/{util.c → pg_util.c} +10 -10
- data/ext/{util.h → pg_util.h} +2 -2
- data/lib/pg.rb +8 -6
- data/lib/pg/basic_type_mapping.rb +121 -25
- data/lib/pg/binary_decoder.rb +23 -0
- data/lib/pg/coder.rb +23 -2
- data/lib/pg/connection.rb +22 -3
- data/lib/pg/constants.rb +2 -1
- data/lib/pg/exceptions.rb +2 -1
- data/lib/pg/result.rb +14 -2
- data/lib/pg/text_decoder.rb +21 -26
- data/lib/pg/text_encoder.rb +32 -8
- data/lib/pg/tuple.rb +30 -0
- data/lib/pg/type_map_by_column.rb +3 -2
- data/spec/helpers.rb +52 -20
- data/spec/pg/basic_type_mapping_spec.rb +362 -37
- data/spec/pg/connection_spec.rb +376 -146
- data/spec/pg/connection_sync_spec.rb +41 -0
- data/spec/pg/result_spec.rb +240 -15
- data/spec/pg/tuple_spec.rb +333 -0
- data/spec/pg/type_map_by_class_spec.rb +2 -2
- data/spec/pg/type_map_by_column_spec.rb +6 -2
- data/spec/pg/type_map_by_mri_type_spec.rb +1 -1
- data/spec/pg/type_map_by_oid_spec.rb +3 -3
- data/spec/pg/type_map_in_ruby_spec.rb +1 -1
- data/spec/pg/type_map_spec.rb +1 -1
- data/spec/pg/type_spec.rb +363 -17
- data/spec/pg_spec.rb +1 -1
- metadata +47 -47
- 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
|
data/spec/pg/result_spec.rb
CHANGED
@@ -1,19 +1,68 @@
|
|
1
|
-
|
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
|
-
|
43
|
-
|
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
|
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.
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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(
|
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
|