pg 1.1.4

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 (77) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +3 -0
  3. data.tar.gz.sig +0 -0
  4. data/.gemtest +0 -0
  5. data/BSDL +22 -0
  6. data/ChangeLog +6595 -0
  7. data/Contributors.rdoc +46 -0
  8. data/History.rdoc +492 -0
  9. data/LICENSE +56 -0
  10. data/Manifest.txt +72 -0
  11. data/POSTGRES +23 -0
  12. data/README-OS_X.rdoc +68 -0
  13. data/README-Windows.rdoc +56 -0
  14. data/README.ja.rdoc +14 -0
  15. data/README.rdoc +178 -0
  16. data/Rakefile +215 -0
  17. data/Rakefile.cross +298 -0
  18. data/ext/errorcodes.def +968 -0
  19. data/ext/errorcodes.rb +45 -0
  20. data/ext/errorcodes.txt +478 -0
  21. data/ext/extconf.rb +94 -0
  22. data/ext/gvl_wrappers.c +17 -0
  23. data/ext/gvl_wrappers.h +241 -0
  24. data/ext/pg.c +640 -0
  25. data/ext/pg.h +365 -0
  26. data/ext/pg_binary_decoder.c +229 -0
  27. data/ext/pg_binary_encoder.c +162 -0
  28. data/ext/pg_coder.c +549 -0
  29. data/ext/pg_connection.c +4252 -0
  30. data/ext/pg_copy_coder.c +596 -0
  31. data/ext/pg_errors.c +95 -0
  32. data/ext/pg_result.c +1501 -0
  33. data/ext/pg_text_decoder.c +981 -0
  34. data/ext/pg_text_encoder.c +682 -0
  35. data/ext/pg_tuple.c +541 -0
  36. data/ext/pg_type_map.c +166 -0
  37. data/ext/pg_type_map_all_strings.c +116 -0
  38. data/ext/pg_type_map_by_class.c +239 -0
  39. data/ext/pg_type_map_by_column.c +312 -0
  40. data/ext/pg_type_map_by_mri_type.c +284 -0
  41. data/ext/pg_type_map_by_oid.c +355 -0
  42. data/ext/pg_type_map_in_ruby.c +299 -0
  43. data/ext/util.c +149 -0
  44. data/ext/util.h +65 -0
  45. data/ext/vc/pg.sln +26 -0
  46. data/ext/vc/pg_18/pg.vcproj +216 -0
  47. data/ext/vc/pg_19/pg_19.vcproj +209 -0
  48. data/lib/pg.rb +74 -0
  49. data/lib/pg/basic_type_mapping.rb +459 -0
  50. data/lib/pg/binary_decoder.rb +22 -0
  51. data/lib/pg/coder.rb +83 -0
  52. data/lib/pg/connection.rb +291 -0
  53. data/lib/pg/constants.rb +11 -0
  54. data/lib/pg/exceptions.rb +11 -0
  55. data/lib/pg/result.rb +31 -0
  56. data/lib/pg/text_decoder.rb +47 -0
  57. data/lib/pg/text_encoder.rb +69 -0
  58. data/lib/pg/tuple.rb +30 -0
  59. data/lib/pg/type_map_by_column.rb +15 -0
  60. data/spec/data/expected_trace.out +26 -0
  61. data/spec/data/random_binary_data +0 -0
  62. data/spec/helpers.rb +380 -0
  63. data/spec/pg/basic_type_mapping_spec.rb +508 -0
  64. data/spec/pg/connection_spec.rb +1872 -0
  65. data/spec/pg/connection_sync_spec.rb +41 -0
  66. data/spec/pg/result_spec.rb +491 -0
  67. data/spec/pg/tuple_spec.rb +280 -0
  68. data/spec/pg/type_map_by_class_spec.rb +138 -0
  69. data/spec/pg/type_map_by_column_spec.rb +222 -0
  70. data/spec/pg/type_map_by_mri_type_spec.rb +136 -0
  71. data/spec/pg/type_map_by_oid_spec.rb +149 -0
  72. data/spec/pg/type_map_in_ruby_spec.rb +164 -0
  73. data/spec/pg/type_map_spec.rb +22 -0
  74. data/spec/pg/type_spec.rb +949 -0
  75. data/spec/pg_spec.rb +50 -0
  76. metadata +322 -0
  77. metadata.gz.sig +0 -0
@@ -0,0 +1,1872 @@
1
+ # -*- rspec -*-
2
+ #encoding: utf-8
3
+
4
+ require_relative '../helpers'
5
+
6
+ require 'timeout'
7
+ require 'socket'
8
+ require 'pg'
9
+
10
+ describe PG::Connection do
11
+
12
+ it "can create a connection option string from a Hash of options" do
13
+ optstring = described_class.parse_connect_args(
14
+ :host => 'pgsql.example.com',
15
+ :dbname => 'db01',
16
+ 'sslmode' => 'require'
17
+ )
18
+
19
+ expect( optstring ).to be_a( String )
20
+ expect( optstring ).to match( /(^|\s)host='pgsql.example.com'/ )
21
+ expect( optstring ).to match( /(^|\s)dbname='db01'/ )
22
+ expect( optstring ).to match( /(^|\s)sslmode='require'/ )
23
+ end
24
+
25
+ it "can create a connection option string from positional parameters" do
26
+ optstring = described_class.parse_connect_args( 'pgsql.example.com', nil, '-c geqo=off', nil,
27
+ 'sales' )
28
+
29
+ expect( optstring ).to be_a( String )
30
+ expect( optstring ).to match( /(^|\s)host='pgsql.example.com'/ )
31
+ expect( optstring ).to match( /(^|\s)dbname='sales'/ )
32
+ expect( optstring ).to match( /(^|\s)options='-c geqo=off'/ )
33
+
34
+ expect( optstring ).to_not match( /port=/ )
35
+ expect( optstring ).to_not match( /tty=/ )
36
+ end
37
+
38
+ it "can create a connection option string from a mix of positional and hash parameters" do
39
+ optstring = described_class.parse_connect_args( 'pgsql.example.com',
40
+ :dbname => 'licensing', :user => 'jrandom' )
41
+
42
+ expect( optstring ).to be_a( String )
43
+ expect( optstring ).to match( /(^|\s)host='pgsql.example.com'/ )
44
+ expect( optstring ).to match( /(^|\s)dbname='licensing'/ )
45
+ expect( optstring ).to match( /(^|\s)user='jrandom'/ )
46
+ end
47
+
48
+ it "can create a connection option string from an option string and a hash" do
49
+ optstring = described_class.parse_connect_args( 'dbname=original', :user => 'jrandom' )
50
+
51
+ expect( optstring ).to be_a( String )
52
+ expect( optstring ).to match( /(^|\s)dbname=original/ )
53
+ expect( optstring ).to match( /(^|\s)user='jrandom'/ )
54
+ end
55
+
56
+ it "escapes single quotes and backslashes in connection parameters" do
57
+ expect(
58
+ described_class.parse_connect_args( "DB 'browser' \\" )
59
+ ).to match( /host='DB \\'browser\\' \\\\'/ )
60
+
61
+ end
62
+
63
+ let(:uri) { 'postgresql://user:pass@pgsql.example.com:222/db01?sslmode=require' }
64
+
65
+ it "can connect using a URI" do
66
+ string = described_class.parse_connect_args( uri )
67
+
68
+ expect( string ).to be_a( String )
69
+ expect( string ).to match( %r{^postgresql://user:pass@pgsql.example.com:222/db01\?} )
70
+ expect( string ).to match( %r{\?.*sslmode=require} )
71
+
72
+ string = described_class.parse_connect_args( URI.parse(uri) )
73
+
74
+ expect( string ).to be_a( String )
75
+ expect( string ).to match( %r{^postgresql://user:pass@pgsql.example.com:222/db01\?} )
76
+ expect( string ).to match( %r{\?.*sslmode=require} )
77
+ end
78
+
79
+ it "can create a connection URI from a URI and a hash" do
80
+ string = described_class.parse_connect_args( uri, :connect_timeout => 2 )
81
+
82
+ expect( string ).to be_a( String )
83
+ expect( string ).to match( %r{^postgresql://user:pass@pgsql.example.com:222/db01\?} )
84
+ expect( string ).to match( %r{\?.*sslmode=require} )
85
+ expect( string ).to match( %r{\?.*connect_timeout=2} )
86
+
87
+ string = described_class.parse_connect_args( uri,
88
+ :user => 'a',
89
+ :password => 'b',
90
+ :host => 'localhost',
91
+ :port => 555,
92
+ :dbname => 'x' )
93
+
94
+ expect( string ).to be_a( String )
95
+ expect( string ).to match( %r{^postgresql://\?} )
96
+ expect( string ).to match( %r{\?.*user=a} )
97
+ expect( string ).to match( %r{\?.*password=b} )
98
+ expect( string ).to match( %r{\?.*host=localhost} )
99
+ expect( string ).to match( %r{\?.*port=555} )
100
+ expect( string ).to match( %r{\?.*dbname=x} )
101
+ end
102
+
103
+ it "can create a connection URI with a non-standard domain socket directory" do
104
+ string = described_class.parse_connect_args( 'postgresql://%2Fvar%2Flib%2Fpostgresql/dbname' )
105
+
106
+ expect( string ).to be_a( String )
107
+ expect( string ).to match( %r{^postgresql://%2Fvar%2Flib%2Fpostgresql/dbname} )
108
+
109
+ string = described_class.
110
+ parse_connect_args( 'postgresql:///dbname', :host => '/var/lib/postgresql' )
111
+
112
+ expect( string ).to be_a( String )
113
+ expect( string ).to match( %r{^postgresql:///dbname\?} )
114
+ expect( string ).to match( %r{\?.*host=%2Fvar%2Flib%2Fpostgresql} )
115
+ end
116
+
117
+ it "connects with defaults if no connection parameters are given" do
118
+ expect( described_class.parse_connect_args ).to eq( '' )
119
+ end
120
+
121
+ it "connects successfully with connection string" do
122
+ conninfo_with_colon_in_password = "host=localhost user=a port=555 dbname=test password=a:a"
123
+
124
+ string = described_class.parse_connect_args( conninfo_with_colon_in_password )
125
+
126
+ expect( string ).to be_a( String )
127
+ expect( string ).to match( %r{(^|\s)user=a} )
128
+ expect( string ).to match( %r{(^|\s)password=a:a} )
129
+ expect( string ).to match( %r{(^|\s)host=localhost} )
130
+ expect( string ).to match( %r{(^|\s)port=555} )
131
+ expect( string ).to match( %r{(^|\s)dbname=test} )
132
+ end
133
+
134
+ it "connects successfully with connection string" do
135
+ tmpconn = described_class.connect( @conninfo )
136
+ expect( tmpconn.status ).to eq( PG::CONNECTION_OK )
137
+ tmpconn.finish
138
+ end
139
+
140
+ it "connects using 7 arguments converted to strings" do
141
+ tmpconn = described_class.connect( 'localhost', @port, nil, nil, :test, nil, nil )
142
+ expect( tmpconn.status ).to eq( PG::CONNECTION_OK )
143
+ tmpconn.finish
144
+ end
145
+
146
+ it "connects using a hash of connection parameters" do
147
+ tmpconn = described_class.connect(
148
+ :host => 'localhost',
149
+ :port => @port,
150
+ :dbname => :test)
151
+ expect( tmpconn.status ).to eq( PG::CONNECTION_OK )
152
+ tmpconn.finish
153
+ end
154
+
155
+ it "connects using a hash of optional connection parameters" do
156
+ tmpconn = described_class.connect(
157
+ :host => 'localhost',
158
+ :port => @port,
159
+ :dbname => :test,
160
+ :keepalives => 1)
161
+ expect( tmpconn.status ).to eq( PG::CONNECTION_OK )
162
+ tmpconn.finish
163
+ end
164
+
165
+ it "raises an exception when connecting with an invalid number of arguments" do
166
+ expect {
167
+ described_class.connect( 1, 2, 3, 4, 5, 6, 7, 'the-extra-arg' )
168
+ }.to raise_error do |error|
169
+ expect( error ).to be_an( ArgumentError )
170
+ expect( error.message ).to match( /extra positional parameter/i )
171
+ expect( error.message ).to match( /8/ )
172
+ expect( error.message ).to match( /the-extra-arg/ )
173
+ end
174
+ end
175
+
176
+ it "can connect asynchronously", :socket_io do
177
+ tmpconn = described_class.connect_start( @conninfo )
178
+ expect( tmpconn ).to be_a( described_class )
179
+
180
+ wait_for_polling_ok(tmpconn)
181
+ expect( tmpconn.status ).to eq( PG::CONNECTION_OK )
182
+ tmpconn.finish
183
+ end
184
+
185
+ it "can connect asynchronously for the duration of a block", :socket_io do
186
+ conn = nil
187
+
188
+ described_class.connect_start(@conninfo) do |tmpconn|
189
+ expect( tmpconn ).to be_a( described_class )
190
+ conn = tmpconn
191
+
192
+ wait_for_polling_ok(tmpconn)
193
+ expect( tmpconn.status ).to eq( PG::CONNECTION_OK )
194
+ end
195
+
196
+ expect( conn ).to be_finished()
197
+ end
198
+
199
+ context "with async established connection", :socket_io do
200
+ before :each do
201
+ @conn2 = described_class.connect_start( @conninfo )
202
+ wait_for_polling_ok(@conn2)
203
+ expect( @conn2 ).to still_be_usable
204
+ end
205
+
206
+ after :each do
207
+ expect( @conn2 ).to still_be_usable
208
+ @conn2.close
209
+ end
210
+
211
+ it "conn.send_query and IO.select work" do
212
+ @conn2.send_query("SELECT 1")
213
+ res = wait_for_query_result(@conn2)
214
+ expect( res.values ).to eq([["1"]])
215
+ end
216
+
217
+ it "conn.send_query and conn.block work" do
218
+ @conn2.send_query("SELECT 2")
219
+ @conn2.block
220
+ res = @conn2.get_last_result
221
+ expect( res.values ).to eq([["2"]])
222
+ end
223
+
224
+ it "conn.async_query works" do
225
+ res = @conn2.async_query("SELECT 3")
226
+ expect( res.values ).to eq([["3"]])
227
+ expect( @conn2 ).to still_be_usable
228
+
229
+ res = @conn2.query("SELECT 4")
230
+ end
231
+
232
+ it "can use conn.reset_start to restart the connection" do
233
+ ios = IO.pipe
234
+ conn = described_class.connect_start( @conninfo )
235
+ wait_for_polling_ok(conn)
236
+
237
+ # Close the two pipe file descriptors, so that the file descriptor of
238
+ # newly established connection is probably distinct from the previous one.
239
+ ios.each(&:close)
240
+ conn.reset_start
241
+ wait_for_polling_ok(conn, :reset_poll)
242
+
243
+ # The new connection should work even when the file descriptor has changed.
244
+ conn.send_query("SELECT 1")
245
+ res = wait_for_query_result(conn)
246
+ expect( res.values ).to eq([["1"]])
247
+
248
+ conn.close
249
+ end
250
+
251
+ it "should properly close a socket IO when GC'ed" do
252
+ # This results in
253
+ # Errno::ENOTSOCK: An operation was attempted on something that is not a socket.
254
+ # on Windows when rb_w32_unwrap_io_handle() isn't called in pgconn_gc_free().
255
+ 5.times do
256
+ conn = described_class.connect( @conninfo )
257
+ conn.socket_io.close
258
+ end
259
+ GC.start
260
+ IO.pipe.each(&:close)
261
+ end
262
+ end
263
+
264
+ it "raises proper error when sending fails" do
265
+ conn = described_class.connect_start( '127.0.0.1', 54320, "", "", "me", "xxxx", "somedb" )
266
+ expect{ conn.exec 'SELECT 1' }.to raise_error(PG::UnableToSend, /no connection/)
267
+ end
268
+
269
+ it "doesn't leave stale server connections after finish" do
270
+ described_class.connect(@conninfo).finish
271
+ sleep 0.5
272
+ res = @conn.exec(%[SELECT COUNT(*) AS n FROM pg_stat_activity
273
+ WHERE usename IS NOT NULL AND application_name != ''])
274
+ # there's still the global @conn, but should be no more
275
+ expect( res[0]['n'] ).to eq( '1' )
276
+ end
277
+
278
+ it "can retrieve it's connection parameters for the established connection" do
279
+ expect( @conn.db ).to eq( "test" )
280
+ expect( @conn.user ).to be_a_kind_of( String )
281
+ expect( @conn.pass ).to eq( "" )
282
+ expect( @conn.port ).to eq( @port )
283
+ expect( @conn.tty ).to eq( "" )
284
+ expect( @conn.options ).to eq( "" )
285
+ end
286
+ it "can retrieve it's connection parameters for the established connection",
287
+ skip: RUBY_PLATFORM=~/x64-mingw/ ? "host segfaults on Windows-x64" : false do
288
+ expect( @conn.host ).to eq( "localhost" )
289
+ end
290
+
291
+ EXPECTED_TRACE_OUTPUT = %{
292
+ To backend> Msg Q
293
+ To backend> "SELECT 1 AS one"
294
+ To backend> Msg complete, length 21
295
+ From backend> T
296
+ From backend (#4)> 28
297
+ From backend (#2)> 1
298
+ From backend> "one"
299
+ From backend (#4)> 0
300
+ From backend (#2)> 0
301
+ From backend (#4)> 23
302
+ From backend (#2)> 4
303
+ From backend (#4)> -1
304
+ From backend (#2)> 0
305
+ From backend> D
306
+ From backend (#4)> 11
307
+ From backend (#2)> 1
308
+ From backend (#4)> 1
309
+ From backend (1)> 1
310
+ From backend> C
311
+ From backend (#4)> 13
312
+ From backend> "SELECT 1"
313
+ From backend> Z
314
+ From backend (#4)> 5
315
+ From backend> Z
316
+ From backend (#4)> 5
317
+ From backend> T
318
+ }.gsub( /^\t{2}/, '' ).lstrip
319
+
320
+ it "trace and untrace client-server communication", :unix do
321
+ # be careful to explicitly close files so that the
322
+ # directory can be removed and we don't have to wait for
323
+ # the GC to run.
324
+ trace_file = TEST_DIRECTORY + "test_trace.out"
325
+ trace_io = trace_file.open( 'w', 0600 )
326
+ @conn.trace( trace_io )
327
+ trace_io.close
328
+
329
+ res = @conn.exec("SELECT 1 AS one")
330
+ @conn.untrace
331
+
332
+ res = @conn.exec("SELECT 2 AS two")
333
+
334
+ trace_data = trace_file.read
335
+
336
+ # For async_exec the output will be different:
337
+ # From backend> Z
338
+ # From backend (#4)> 5
339
+ # +From backend> Z
340
+ # +From backend (#4)> 5
341
+ # From backend> T
342
+ trace_data.sub!( /(From backend> Z\nFrom backend \(#4\)> 5\n){3}/m, '\\1\\1' )
343
+
344
+ expect( trace_data ).to eq( EXPECTED_TRACE_OUTPUT )
345
+ end
346
+
347
+ it "allows a query to be cancelled" do
348
+ error = false
349
+ @conn.send_query("SELECT pg_sleep(1000)")
350
+ @conn.cancel
351
+ tmpres = @conn.get_result
352
+ if(tmpres.result_status != PG::PGRES_TUPLES_OK)
353
+ error = true
354
+ end
355
+ expect( error ).to eq( true )
356
+ end
357
+
358
+ it "can stop a thread that runs a blocking query with async_exec" do
359
+ pending "this does not work on Rubinius" if RUBY_ENGINE=='rbx'
360
+
361
+ start = Time.now
362
+ t = Thread.new do
363
+ @conn.async_exec( 'select pg_sleep(10)' )
364
+ end
365
+ sleep 0.1
366
+
367
+ t.kill
368
+ t.join
369
+ expect( (Time.now - start) ).to be < 10
370
+ end
371
+
372
+ it "should work together with signal handlers", :unix do
373
+ signal_received = false
374
+ trap 'USR1' do
375
+ signal_received = true
376
+ end
377
+
378
+ Thread.new do
379
+ sleep 0.1
380
+ Process.kill("USR1", Process.pid)
381
+ end
382
+ @conn.exec("select pg_sleep(0.3)")
383
+ expect( signal_received ).to be_truthy
384
+
385
+ signal_received = false
386
+ Thread.new do
387
+ sleep 0.1
388
+ Process.kill("USR1", Process.pid)
389
+ end
390
+ @conn.async_exec("select pg_sleep(0.3)")
391
+ expect( signal_received ).to be_truthy
392
+ end
393
+
394
+
395
+ it "automatically rolls back a transaction started with Connection#transaction if an exception " +
396
+ "is raised" do
397
+ # abort the per-example transaction so we can test our own
398
+ @conn.exec( 'ROLLBACK' )
399
+
400
+ res = nil
401
+ @conn.exec( "CREATE TABLE pie ( flavor TEXT )" )
402
+
403
+ begin
404
+ expect {
405
+ res = @conn.transaction do
406
+ @conn.exec( "INSERT INTO pie VALUES ('rhubarb'), ('cherry'), ('schizophrenia')" )
407
+ raise "Oh noes! All pie is gone!"
408
+ end
409
+ }.to raise_exception( RuntimeError, /all pie is gone/i )
410
+
411
+ res = @conn.exec( "SELECT * FROM pie" )
412
+ expect( res.ntuples ).to eq( 0 )
413
+ ensure
414
+ @conn.exec( "DROP TABLE pie" )
415
+ end
416
+ end
417
+
418
+ it "returns the block result from Connection#transaction" do
419
+ # abort the per-example transaction so we can test our own
420
+ @conn.exec( 'ROLLBACK' )
421
+
422
+ res = @conn.transaction do
423
+ "transaction result"
424
+ end
425
+ expect( res ).to eq( "transaction result" )
426
+ end
427
+
428
+ it "not read past the end of a large object" do
429
+ @conn.transaction do
430
+ oid = @conn.lo_create( 0 )
431
+ fd = @conn.lo_open( oid, PG::INV_READ|PG::INV_WRITE )
432
+ @conn.lo_write( fd, "foobar" )
433
+ expect( @conn.lo_read( fd, 10 ) ).to be_nil()
434
+ @conn.lo_lseek( fd, 0, PG::SEEK_SET )
435
+ expect( @conn.lo_read( fd, 10 ) ).to eq( 'foobar' )
436
+ end
437
+ end
438
+
439
+ it "supports explicitly calling #exec_params" do
440
+ @conn.exec( "CREATE TABLE students ( name TEXT, age INTEGER )" )
441
+ @conn.exec_params( "INSERT INTO students VALUES( $1, $2 )", ['Wally', 8] )
442
+ @conn.exec_params( "INSERT INTO students VALUES( $1, $2 )", ['Sally', 6] )
443
+ @conn.exec_params( "INSERT INTO students VALUES( $1, $2 )", ['Dorothy', 4] )
444
+
445
+ res = @conn.exec_params( "SELECT name FROM students WHERE age >= $1", [6] )
446
+ expect( res.values ).to eq( [ ['Wally'], ['Sally'] ] )
447
+ end
448
+
449
+ it "supports hash form parameters for #exec_params" do
450
+ hash_param_bin = { value: ["00ff"].pack("H*"), type: 17, format: 1 }
451
+ hash_param_nil = { value: nil, type: 17, format: 1 }
452
+ res = @conn.exec_params( "SELECT $1, $2",
453
+ [ hash_param_bin, hash_param_nil ] )
454
+ expect( res.values ).to eq( [["\\x00ff", nil]] )
455
+ expect( result_typenames(res) ).to eq( ['bytea', 'bytea'] )
456
+ end
457
+
458
+ it "should work with arbitrary number of params" do
459
+ begin
460
+ 3.step( 12, 0.2 ) do |exp|
461
+ num_params = (2 ** exp).to_i
462
+ sql = num_params.times.map{|n| "$#{n+1}::INT" }.join(",")
463
+ params = num_params.times.to_a
464
+ res = @conn.exec_params( "SELECT #{sql}", params )
465
+ expect( res.nfields ).to eq( num_params )
466
+ expect( res.values ).to eq( [num_params.times.map(&:to_s)] )
467
+ end
468
+ rescue PG::ProgramLimitExceeded
469
+ # Stop silently if the server complains about too many params
470
+ end
471
+ end
472
+
473
+ it "can wait for NOTIFY events" do
474
+ @conn.exec( 'ROLLBACK' )
475
+ @conn.exec( 'LISTEN woo' )
476
+
477
+ t = Thread.new do
478
+ begin
479
+ conn = described_class.connect( @conninfo )
480
+ sleep 1
481
+ conn.async_exec( 'NOTIFY woo' )
482
+ ensure
483
+ conn.finish
484
+ end
485
+ end
486
+
487
+ expect( @conn.wait_for_notify( 10 ) ).to eq( 'woo' )
488
+ @conn.exec( 'UNLISTEN woo' )
489
+
490
+ t.join
491
+ end
492
+
493
+ it "calls a block for NOTIFY events if one is given" do
494
+ @conn.exec( 'ROLLBACK' )
495
+ @conn.exec( 'LISTEN woo' )
496
+
497
+ t = Thread.new do
498
+ begin
499
+ conn = described_class.connect( @conninfo )
500
+ sleep 1
501
+ conn.async_exec( 'NOTIFY woo' )
502
+ ensure
503
+ conn.finish
504
+ end
505
+ end
506
+
507
+ eventpid = event = nil
508
+ @conn.wait_for_notify( 10 ) {|*args| event, eventpid = args }
509
+ expect( event ).to eq( 'woo' )
510
+ expect( eventpid ).to be_an( Integer )
511
+
512
+ @conn.exec( 'UNLISTEN woo' )
513
+
514
+ t.join
515
+ end
516
+
517
+ it "doesn't collapse sequential notifications" do
518
+ @conn.exec( 'ROLLBACK' )
519
+ @conn.exec( 'LISTEN woo' )
520
+ @conn.exec( 'LISTEN war' )
521
+ @conn.exec( 'LISTEN woz' )
522
+
523
+ begin
524
+ conn = described_class.connect( @conninfo )
525
+ conn.exec( 'NOTIFY woo' )
526
+ conn.exec( 'NOTIFY war' )
527
+ conn.exec( 'NOTIFY woz' )
528
+ ensure
529
+ conn.finish
530
+ end
531
+
532
+ channels = []
533
+ 3.times do
534
+ channels << @conn.wait_for_notify( 2 )
535
+ end
536
+
537
+ expect( channels.size ).to eq( 3 )
538
+ expect( channels ).to include( 'woo', 'war', 'woz' )
539
+
540
+ @conn.exec( 'UNLISTEN woz' )
541
+ @conn.exec( 'UNLISTEN war' )
542
+ @conn.exec( 'UNLISTEN woo' )
543
+ end
544
+
545
+ it "returns notifications which are already in the queue before wait_for_notify is called " +
546
+ "without waiting for the socket to become readable" do
547
+ @conn.exec( 'ROLLBACK' )
548
+ @conn.exec( 'LISTEN woo' )
549
+
550
+ begin
551
+ conn = described_class.connect( @conninfo )
552
+ conn.exec( 'NOTIFY woo' )
553
+ ensure
554
+ conn.finish
555
+ end
556
+
557
+ # Cause the notification to buffer, but not be read yet
558
+ @conn.exec( 'SELECT 1' )
559
+
560
+ expect( @conn.wait_for_notify( 10 ) ).to eq( 'woo' )
561
+ @conn.exec( 'UNLISTEN woo' )
562
+ end
563
+
564
+ it "can receive notices while waiting for NOTIFY without exceeding the timeout" do
565
+ notices = []
566
+ @conn.set_notice_processor do |msg|
567
+ notices << [msg, Time.now]
568
+ end
569
+ st = Time.now
570
+ @conn.send_query "SELECT pg_sleep(0.5); do $$ BEGIN RAISE NOTICE 'woohoo'; END; $$ LANGUAGE plpgsql;"
571
+ expect( @conn.wait_for_notify( 1 ) ).to be_nil
572
+ expect( notices.first ).to_not be_nil
573
+ et = Time.now
574
+ expect( (et - notices.first[1]) ).to be >= 0.4
575
+ expect( (et - st) ).to be >= 0.9
576
+ expect( (et - st) ).to be < 1.4
577
+ end
578
+
579
+ it "yields the result if block is given to exec" do
580
+ rval = @conn.exec( "select 1234::int as a union select 5678::int as a" ) do |result|
581
+ values = []
582
+ expect( result ).to be_kind_of( PG::Result )
583
+ expect( result.ntuples ).to eq( 2 )
584
+ result.each do |tuple|
585
+ values << tuple['a']
586
+ end
587
+ values
588
+ end
589
+
590
+ expect( rval.size ).to eq( 2 )
591
+ expect( rval ).to include( '5678', '1234' )
592
+ end
593
+
594
+ it "can process #copy_data output queries" do
595
+ rows = []
596
+ res2 = @conn.copy_data( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT" ) do |res|
597
+ expect( res.result_status ).to eq( PG::PGRES_COPY_OUT )
598
+ expect( res.nfields ).to eq( 1 )
599
+ while row=@conn.get_copy_data
600
+ rows << row
601
+ end
602
+ end
603
+ expect( rows ).to eq( ["1\n", "2\n"] )
604
+ expect( res2.result_status ).to eq( PG::PGRES_COMMAND_OK )
605
+ expect( @conn ).to still_be_usable
606
+ end
607
+
608
+ it "can handle incomplete #copy_data output queries" do
609
+ expect {
610
+ @conn.copy_data( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT" ) do |res|
611
+ @conn.get_copy_data
612
+ end
613
+ }.to raise_error(PG::NotAllCopyDataRetrieved, /Not all/)
614
+ expect( @conn ).to still_be_usable
615
+ end
616
+
617
+ it "can handle client errors in #copy_data for output" do
618
+ expect {
619
+ @conn.copy_data( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT" ) do
620
+ raise "boom"
621
+ end
622
+ }.to raise_error(RuntimeError, "boom")
623
+ expect( @conn ).to still_be_usable
624
+ end
625
+
626
+ it "can handle server errors in #copy_data for output" do
627
+ @conn.exec "ROLLBACK"
628
+ @conn.transaction do
629
+ @conn.exec( "CREATE FUNCTION errfunc() RETURNS int AS $$ BEGIN RAISE 'test-error'; END; $$ LANGUAGE plpgsql;" )
630
+ expect {
631
+ @conn.copy_data( "COPY (SELECT errfunc()) TO STDOUT" ) do |res|
632
+ while @conn.get_copy_data
633
+ end
634
+ end
635
+ }.to raise_error(PG::Error, /test-error/)
636
+ end
637
+ expect( @conn ).to still_be_usable
638
+ end
639
+
640
+ it "can process #copy_data input queries" do
641
+ @conn.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" )
642
+ res2 = @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res|
643
+ expect( res.result_status ).to eq( PG::PGRES_COPY_IN )
644
+ expect( res.nfields ).to eq( 1 )
645
+ @conn.put_copy_data "1\n"
646
+ @conn.put_copy_data "2\n"
647
+ end
648
+ expect( res2.result_status ).to eq( PG::PGRES_COMMAND_OK )
649
+
650
+ expect( @conn ).to still_be_usable
651
+
652
+ res = @conn.exec( "SELECT * FROM copytable ORDER BY col1" )
653
+ expect( res.values ).to eq( [["1"], ["2"]] )
654
+ end
655
+
656
+ it "can handle client errors in #copy_data for input" do
657
+ @conn.exec "ROLLBACK"
658
+ @conn.transaction do
659
+ @conn.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" )
660
+ expect {
661
+ @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res|
662
+ raise "boom"
663
+ end
664
+ }.to raise_error(RuntimeError, "boom")
665
+ end
666
+
667
+ expect( @conn ).to still_be_usable
668
+ end
669
+
670
+ it "can handle server errors in #copy_data for input" do
671
+ @conn.exec "ROLLBACK"
672
+ @conn.transaction do
673
+ @conn.exec( "CREATE TEMP TABLE copytable (col1 INT)" )
674
+ expect {
675
+ @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res|
676
+ @conn.put_copy_data "xyz\n"
677
+ end
678
+ }.to raise_error(PG::Error, /invalid input syntax for integer/)
679
+ end
680
+ expect( @conn ).to still_be_usable
681
+ end
682
+
683
+ it "gracefully handle SQL statements while in #copy_data for input" do
684
+ @conn.exec "ROLLBACK"
685
+ @conn.transaction do
686
+ @conn.exec( "CREATE TEMP TABLE copytable (col1 INT)" )
687
+ expect {
688
+ @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res|
689
+ @conn.exec "SELECT 1"
690
+ end
691
+ }.to raise_error(PG::Error, /no COPY in progress/)
692
+ end
693
+ expect( @conn ).to still_be_usable
694
+ end
695
+
696
+ it "gracefully handle SQL statements while in #copy_data for output" do
697
+ @conn.exec "ROLLBACK"
698
+ @conn.transaction do
699
+ expect {
700
+ @conn.copy_data( "COPY (VALUES(1), (2)) TO STDOUT" ) do |res|
701
+ @conn.exec "SELECT 3"
702
+ end
703
+ }.to raise_error(PG::Error, /no COPY in progress/)
704
+ end
705
+ expect( @conn ).to still_be_usable
706
+ end
707
+
708
+ it "should raise an error for non copy statements in #copy_data" do
709
+ expect {
710
+ @conn.copy_data( "SELECT 1" ){}
711
+ }.to raise_error(ArgumentError, /no COPY/)
712
+
713
+ expect( @conn ).to still_be_usable
714
+ end
715
+
716
+ it "correctly finishes COPY queries passed to #async_exec" do
717
+ @conn.async_exec( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT" )
718
+
719
+ results = []
720
+ begin
721
+ data = @conn.get_copy_data( true )
722
+ if false == data
723
+ @conn.block( 2.0 )
724
+ data = @conn.get_copy_data( true )
725
+ end
726
+ results << data if data
727
+ end until data.nil?
728
+
729
+ expect( results.size ).to eq( 2 )
730
+ expect( results ).to include( "1\n", "2\n" )
731
+ end
732
+
733
+
734
+ it "described_class#block shouldn't block a second thread" do
735
+ start = Time.now
736
+ t = Thread.new do
737
+ @conn.send_query( "select pg_sleep(3)" )
738
+ @conn.block
739
+ end
740
+
741
+ sleep 0.5
742
+ expect( t ).to be_alive()
743
+ @conn.cancel
744
+ t.join
745
+ expect( (Time.now - start) ).to be < 3
746
+ end
747
+
748
+ it "described_class#block should allow a timeout" do
749
+ @conn.send_query( "select pg_sleep(1)" )
750
+
751
+ start = Time.now
752
+ @conn.block( 0.3 )
753
+ finish = Time.now
754
+
755
+ expect( (finish - start) ).to be_within( 0.2 ).of( 0.3 )
756
+ end
757
+
758
+ it "can return the default connection options" do
759
+ expect( described_class.conndefaults ).to be_a( Array )
760
+ expect( described_class.conndefaults ).to all( be_a(Hash) )
761
+ expect( described_class.conndefaults[0] ).to include( :keyword, :label, :dispchar, :dispsize )
762
+ expect( @conn.conndefaults ).to eq( described_class.conndefaults )
763
+ end
764
+
765
+ it "can return the default connection options as a Hash" do
766
+ expect( described_class.conndefaults_hash ).to be_a( Hash )
767
+ expect( described_class.conndefaults_hash ).to include( :user, :password, :dbname, :host, :port )
768
+ expect( ['5432', '54321', @port.to_s] ).to include( described_class.conndefaults_hash[:port] )
769
+ expect( @conn.conndefaults_hash ).to eq( described_class.conndefaults_hash )
770
+ end
771
+
772
+ it "can return the connection's connection options", :postgresql_93 do
773
+ expect( @conn.conninfo ).to be_a( Array )
774
+ expect( @conn.conninfo ).to all( be_a(Hash) )
775
+ expect( @conn.conninfo[0] ).to include( :keyword, :label, :dispchar, :dispsize )
776
+ end
777
+
778
+
779
+ it "can return the connection's connection options as a Hash", :postgresql_93 do
780
+ expect( @conn.conninfo_hash ).to be_a( Hash )
781
+ expect( @conn.conninfo_hash ).to include( :user, :password, :connect_timeout, :dbname, :host )
782
+ expect( @conn.conninfo_hash[:dbname] ).to eq( 'test' )
783
+ end
784
+
785
+ describe "connection information related to SSL" do
786
+
787
+ it "can retrieve connection's ssl state", :postgresql_95 do
788
+ expect( @conn.ssl_in_use? ).to be false
789
+ end
790
+
791
+ it "can retrieve connection's ssl attribute_names", :postgresql_95 do
792
+ expect( @conn.ssl_attribute_names ).to be_a(Array)
793
+ end
794
+
795
+ it "can retrieve a single ssl connection attribute", :postgresql_95 do
796
+ expect( @conn.ssl_attribute('dbname') ).to eq( nil )
797
+ end
798
+
799
+ it "can retrieve all connection's ssl attributes", :postgresql_95 do
800
+ expect( @conn.ssl_attributes ).to be_a_kind_of( Hash )
801
+ end
802
+ end
803
+
804
+
805
+ it "honors the connect_timeout connection parameter", :postgresql_93 do
806
+ conn = PG.connect( port: @port, dbname: 'test', connect_timeout: 11 )
807
+ begin
808
+ expect( conn.conninfo_hash[:connect_timeout] ).to eq( "11" )
809
+ ensure
810
+ conn.finish
811
+ end
812
+ end
813
+
814
+ describe "deprecated password encryption method" do
815
+ it "can encrypt password for a given user" do
816
+ expect( described_class.encrypt_password("postgres", "postgres") ).to match( /\S+/ )
817
+ end
818
+
819
+ it "raises an appropriate error if either of the required arguments is not valid" do
820
+ expect {
821
+ described_class.encrypt_password( nil, nil )
822
+ }.to raise_error( TypeError )
823
+ expect {
824
+ described_class.encrypt_password( "postgres", nil )
825
+ }.to raise_error( TypeError )
826
+ expect {
827
+ described_class.encrypt_password( nil, "postgres" )
828
+ }.to raise_error( TypeError )
829
+ end
830
+ end
831
+
832
+ describe "password encryption method", :postgresql_10 do
833
+ it "can encrypt without algorithm" do
834
+ expect( @conn.encrypt_password("postgres", "postgres") ).to match( /\S+/ )
835
+ expect( @conn.encrypt_password("postgres", "postgres", nil) ).to match( /\S+/ )
836
+ end
837
+
838
+ it "can encrypt with algorithm" do
839
+ expect( @conn.encrypt_password("postgres", "postgres", "md5") ).to match( /md5\S+/i )
840
+ expect( @conn.encrypt_password("postgres", "postgres", "scram-sha-256") ).to match( /SCRAM-SHA-256\S+/i )
841
+ end
842
+
843
+ it "raises an appropriate error if either of the required arguments is not valid" do
844
+ expect {
845
+ @conn.encrypt_password( nil, nil )
846
+ }.to raise_error( TypeError )
847
+ expect {
848
+ @conn.encrypt_password( "postgres", nil )
849
+ }.to raise_error( TypeError )
850
+ expect {
851
+ @conn.encrypt_password( nil, "postgres" )
852
+ }.to raise_error( TypeError )
853
+ expect {
854
+ @conn.encrypt_password( "postgres", "postgres", :invalid )
855
+ }.to raise_error( TypeError )
856
+ expect {
857
+ @conn.encrypt_password( "postgres", "postgres", "invalid" )
858
+ }.to raise_error( PG::Error, /unrecognized/ )
859
+ end
860
+ end
861
+
862
+
863
+ it "allows fetching a column of values from a result by column number" do
864
+ res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' )
865
+ expect( res.column_values( 0 ) ).to eq( %w[1 2 3] )
866
+ expect( res.column_values( 1 ) ).to eq( %w[2 3 4] )
867
+ end
868
+
869
+
870
+ it "allows fetching a column of values from a result by field name" do
871
+ res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' )
872
+ expect( res.field_values( 'column1' ) ).to eq( %w[1 2 3] )
873
+ expect( res.field_values( 'column2' ) ).to eq( %w[2 3 4] )
874
+ end
875
+
876
+
877
+ it "raises an error if selecting an invalid column index" do
878
+ res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' )
879
+ expect {
880
+ res.column_values( 20 )
881
+ }.to raise_error( IndexError )
882
+ end
883
+
884
+
885
+ it "raises an error if selecting an invalid field name" do
886
+ res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' )
887
+ expect {
888
+ res.field_values( 'hUUuurrg' )
889
+ }.to raise_error( IndexError )
890
+ end
891
+
892
+
893
+ it "raises an error if column index is not a number" do
894
+ res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' )
895
+ expect {
896
+ res.column_values( 'hUUuurrg' )
897
+ }.to raise_error( TypeError )
898
+ end
899
+
900
+
901
+ it "handles server close while asynchronous connect", :socket_io do
902
+ serv = TCPServer.new( '127.0.0.1', 54320 )
903
+ conn = described_class.connect_start( '127.0.0.1', 54320, "", "", "me", "xxxx", "somedb" )
904
+ expect( [PG::PGRES_POLLING_WRITING, PG::CONNECTION_OK] ).to include conn.connect_poll
905
+ select( nil, [conn.socket_io], nil, 0.2 )
906
+ serv.close
907
+ if conn.connect_poll == PG::PGRES_POLLING_READING
908
+ select( [conn.socket_io], nil, nil, 0.2 )
909
+ end
910
+ expect( conn.connect_poll ).to eq( PG::PGRES_POLLING_FAILED )
911
+ end
912
+
913
+ it "discards previous results at #discard_results" do
914
+ @conn.send_query( "select 1" )
915
+ @conn.discard_results
916
+ @conn.send_query( "select 41 as one" )
917
+ res = @conn.get_last_result
918
+ expect( res.to_a ).to eq( [{ 'one' => '41' }] )
919
+ end
920
+
921
+ it "discards previous results (if any) before waiting on #exec" do
922
+ @conn.send_query( "select 1" )
923
+ res = @conn.exec( "select 42 as one" )
924
+ expect( res.to_a ).to eq( [{ 'one' => '42' }] )
925
+ end
926
+
927
+ it "discards previous errors before waiting on #exec", :without_transaction do
928
+ @conn.send_query( "ERROR" )
929
+ res = @conn.exec( "select 43 as one" )
930
+ expect( res.to_a ).to eq( [{ 'one' => '43' }] )
931
+ end
932
+
933
+ it "calls the block if one is provided to #exec" do
934
+ result = nil
935
+ @conn.exec( "select 47 as one" ) do |pg_res|
936
+ result = pg_res[0]
937
+ end
938
+ expect( result ).to eq( { 'one' => '47' } )
939
+ end
940
+
941
+ it "raises a rescue-able error if #finish is called twice", :without_transaction do
942
+ conn = PG.connect( @conninfo )
943
+
944
+ conn.finish
945
+ expect { conn.finish }.to raise_error( PG::ConnectionBad, /connection is closed/i )
946
+ end
947
+
948
+ it "can use conn.reset to restart the connection" do
949
+ ios = IO.pipe
950
+ conn = PG.connect( @conninfo )
951
+
952
+ # Close the two pipe file descriptors, so that the file descriptor of
953
+ # newly established connection is probably distinct from the previous one.
954
+ ios.each(&:close)
955
+ conn.reset
956
+
957
+ # The new connection should work even when the file descriptor has changed.
958
+ expect( conn.exec("SELECT 1").values ).to eq([["1"]])
959
+ conn.close
960
+ end
961
+
962
+ it "closes the IO fetched from #socket_io when the connection is closed", :without_transaction, :socket_io do
963
+ conn = PG.connect( @conninfo )
964
+ io = conn.socket_io
965
+ conn.finish
966
+ expect( io ).to be_closed()
967
+ expect { conn.socket_io }.to raise_error( PG::ConnectionBad, /connection is closed/i )
968
+ end
969
+
970
+ it "closes the IO fetched from #socket_io when the connection is reset", :without_transaction, :socket_io do
971
+ conn = PG.connect( @conninfo )
972
+ io = conn.socket_io
973
+ conn.reset
974
+ expect( io ).to be_closed()
975
+ expect( conn.socket_io ).to_not equal( io )
976
+ conn.finish
977
+ end
978
+
979
+ it "block should raise ConnectionBad for a closed connection" do
980
+ serv = TCPServer.new( '127.0.0.1', 54320 )
981
+ conn = described_class.connect_start( '127.0.0.1', 54320, "", "", "me", "xxxx", "somedb" )
982
+ while [PG::CONNECTION_STARTED, PG::CONNECTION_MADE].include?(conn.connect_poll)
983
+ sleep 0.1
984
+ end
985
+ serv.close
986
+ expect{ conn.block }.to raise_error(PG::ConnectionBad, /server closed the connection unexpectedly/)
987
+ expect{ conn.block }.to raise_error(PG::ConnectionBad, /can't get socket descriptor/)
988
+ end
989
+
990
+ it "sets the fallback_application_name on new connections" do
991
+ conn_string = PG::Connection.parse_connect_args( 'dbname=test' )
992
+
993
+ conn_name = conn_string[ /application_name='(.*?)'/, 1 ]
994
+ expect( conn_name ).to include( $0[0..10] )
995
+ expect( conn_name ).to include( $0[-10..-1] )
996
+ expect( conn_name.length ).to be <= 64
997
+ end
998
+
999
+ it "sets a shortened fallback_application_name on new connections" do
1000
+ old_0 = $0
1001
+ begin
1002
+ $0 = "/this/is/a/very/long/path/with/many/directories/to/our/beloved/ruby"
1003
+ conn_string = PG::Connection.parse_connect_args( 'dbname=test' )
1004
+ conn_name = conn_string[ /application_name='(.*?)'/, 1 ]
1005
+ expect( conn_name ).to include( $0[0..10] )
1006
+ expect( conn_name ).to include( $0[-10..-1] )
1007
+ expect( conn_name.length ).to be <= 64
1008
+ ensure
1009
+ $0 = old_0
1010
+ end
1011
+ end
1012
+
1013
+ it "calls the block supplied to wait_for_notify with the notify payload if it accepts " +
1014
+ "any number of arguments" do
1015
+
1016
+ @conn.exec( 'ROLLBACK' )
1017
+ @conn.exec( 'LISTEN knees' )
1018
+
1019
+ conn = described_class.connect( @conninfo )
1020
+ conn.exec( %Q{NOTIFY knees, 'skirt and boots'} )
1021
+ conn.finish
1022
+
1023
+ event, pid, msg = nil
1024
+ @conn.wait_for_notify( 10 ) do |*args|
1025
+ event, pid, msg = *args
1026
+ end
1027
+ @conn.exec( 'UNLISTEN knees' )
1028
+
1029
+ expect( event ).to eq( 'knees' )
1030
+ expect( pid ).to be_a_kind_of( Integer )
1031
+ expect( msg ).to eq( 'skirt and boots' )
1032
+ end
1033
+
1034
+ it "accepts nil as the timeout in #wait_for_notify " do
1035
+ @conn.exec( 'ROLLBACK' )
1036
+ @conn.exec( 'LISTEN knees' )
1037
+
1038
+ conn = described_class.connect( @conninfo )
1039
+ conn.exec( %Q{NOTIFY knees} )
1040
+ conn.finish
1041
+
1042
+ event, pid = nil
1043
+ @conn.wait_for_notify( nil ) do |*args|
1044
+ event, pid = *args
1045
+ end
1046
+ @conn.exec( 'UNLISTEN knees' )
1047
+
1048
+ expect( event ).to eq( 'knees' )
1049
+ expect( pid ).to be_a_kind_of( Integer )
1050
+ end
1051
+
1052
+ it "sends nil as the payload if the notification wasn't given one" do
1053
+ @conn.exec( 'ROLLBACK' )
1054
+ @conn.exec( 'LISTEN knees' )
1055
+
1056
+ conn = described_class.connect( @conninfo )
1057
+ conn.exec( %Q{NOTIFY knees} )
1058
+ conn.finish
1059
+
1060
+ payload = :notnil
1061
+ @conn.wait_for_notify( nil ) do |*args|
1062
+ payload = args[ 2 ]
1063
+ end
1064
+ @conn.exec( 'UNLISTEN knees' )
1065
+
1066
+ expect( payload ).to be_nil()
1067
+ end
1068
+
1069
+ it "calls the block supplied to wait_for_notify with the notify payload if it accepts " +
1070
+ "two arguments" do
1071
+
1072
+ @conn.exec( 'ROLLBACK' )
1073
+ @conn.exec( 'LISTEN knees' )
1074
+
1075
+ conn = described_class.connect( @conninfo )
1076
+ conn.exec( %Q{NOTIFY knees, 'skirt and boots'} )
1077
+ conn.finish
1078
+
1079
+ event, pid, msg = nil
1080
+ @conn.wait_for_notify( 10 ) do |arg1, arg2|
1081
+ event, pid, msg = arg1, arg2
1082
+ end
1083
+ @conn.exec( 'UNLISTEN knees' )
1084
+
1085
+ expect( event ).to eq( 'knees' )
1086
+ expect( pid ).to be_a_kind_of( Integer )
1087
+ expect( msg ).to be_nil()
1088
+ end
1089
+
1090
+ it "calls the block supplied to wait_for_notify with the notify payload if it " +
1091
+ "doesn't accept arguments" do
1092
+
1093
+ @conn.exec( 'ROLLBACK' )
1094
+ @conn.exec( 'LISTEN knees' )
1095
+
1096
+ conn = described_class.connect( @conninfo )
1097
+ conn.exec( %Q{NOTIFY knees, 'skirt and boots'} )
1098
+ conn.finish
1099
+
1100
+ notification_received = false
1101
+ @conn.wait_for_notify( 10 ) do
1102
+ notification_received = true
1103
+ end
1104
+ @conn.exec( 'UNLISTEN knees' )
1105
+
1106
+ expect( notification_received ).to be_truthy()
1107
+ end
1108
+
1109
+ it "calls the block supplied to wait_for_notify with the notify payload if it accepts " +
1110
+ "three arguments" do
1111
+
1112
+ @conn.exec( 'ROLLBACK' )
1113
+ @conn.exec( 'LISTEN knees' )
1114
+
1115
+ conn = described_class.connect( @conninfo )
1116
+ conn.exec( %Q{NOTIFY knees, 'skirt and boots'} )
1117
+ conn.finish
1118
+
1119
+ event, pid, msg = nil
1120
+ @conn.wait_for_notify( 10 ) do |arg1, arg2, arg3|
1121
+ event, pid, msg = arg1, arg2, arg3
1122
+ end
1123
+ @conn.exec( 'UNLISTEN knees' )
1124
+
1125
+ expect( event ).to eq( 'knees' )
1126
+ expect( pid ).to be_a_kind_of( Integer )
1127
+ expect( msg ).to eq( 'skirt and boots' )
1128
+ end
1129
+
1130
+ context "server ping", :without_transaction do
1131
+
1132
+ it "pings successfully with connection string" do
1133
+ ping = described_class.ping(@conninfo)
1134
+ expect( ping ).to eq( PG::PQPING_OK )
1135
+ end
1136
+
1137
+ it "pings using 7 arguments converted to strings" do
1138
+ ping = described_class.ping('localhost', @port, nil, nil, :test, nil, nil)
1139
+ expect( ping ).to eq( PG::PQPING_OK )
1140
+ end
1141
+
1142
+ it "pings using a hash of connection parameters" do
1143
+ ping = described_class.ping(
1144
+ :host => 'localhost',
1145
+ :port => @port,
1146
+ :dbname => :test)
1147
+ expect( ping ).to eq( PG::PQPING_OK )
1148
+ end
1149
+
1150
+ it "returns correct response when ping connection cannot be established" do
1151
+ ping = described_class.ping(
1152
+ :host => 'localhost',
1153
+ :port => 9999,
1154
+ :dbname => :test)
1155
+ expect( ping ).to eq( PG::PQPING_NO_RESPONSE )
1156
+ end
1157
+
1158
+ it "returns error when ping connection arguments are wrong" do
1159
+ ping = described_class.ping('localhost', 'localhost', nil, nil, :test, nil, nil)
1160
+ expect( ping ).to_not eq( PG::PQPING_OK )
1161
+ end
1162
+
1163
+ it "returns correct response when ping connection arguments are wrong" do
1164
+ ping = described_class.ping(
1165
+ :host => 'localhost',
1166
+ :invalid_option => 9999,
1167
+ :dbname => :test)
1168
+ expect( ping ).to eq( PG::PQPING_NO_ATTEMPT )
1169
+ end
1170
+
1171
+ end
1172
+
1173
+ describe "set_single_row_mode" do
1174
+
1175
+ it "raises an error when called at the wrong time" do
1176
+ expect {
1177
+ @conn.set_single_row_mode
1178
+ }.to raise_error(PG::Error)
1179
+ end
1180
+
1181
+ it "should work in single row mode" do
1182
+ @conn.send_query( "SELECT generate_series(1,10)" )
1183
+ @conn.set_single_row_mode
1184
+
1185
+ results = []
1186
+ loop do
1187
+ @conn.block
1188
+ res = @conn.get_result or break
1189
+ results << res
1190
+ end
1191
+ expect( results.length ).to eq( 11 )
1192
+ results[0..-2].each do |res|
1193
+ expect( res.result_status ).to eq( PG::PGRES_SINGLE_TUPLE )
1194
+ values = res.field_values('generate_series')
1195
+ expect( values.length ).to eq( 1 )
1196
+ expect( values.first.to_i ).to be > 0
1197
+ end
1198
+ expect( results.last.result_status ).to eq( PG::PGRES_TUPLES_OK )
1199
+ expect( results.last.ntuples ).to eq( 0 )
1200
+ end
1201
+
1202
+ it "should receive rows before entire query is finished" do
1203
+ @conn.send_query( "SELECT generate_series(0,999), NULL UNION ALL SELECT 1000, pg_sleep(1);" )
1204
+ @conn.set_single_row_mode
1205
+
1206
+ start_time = Time.now
1207
+ first_row_time = nil
1208
+ loop do
1209
+ res = @conn.get_result or break
1210
+ res.check
1211
+ first_row_time = Time.now unless first_row_time
1212
+ end
1213
+ expect( (Time.now - start_time) ).to be >= 0.9
1214
+ expect( (first_row_time - start_time) ).to be < 0.9
1215
+ end
1216
+
1217
+ it "should receive rows before entire query fails" do
1218
+ @conn.exec( "CREATE FUNCTION errfunc() RETURNS int AS $$ BEGIN RAISE 'test-error'; END; $$ LANGUAGE plpgsql;" )
1219
+ @conn.send_query( "SELECT generate_series(0,999), NULL UNION ALL SELECT 1000, errfunc();" )
1220
+ @conn.set_single_row_mode
1221
+
1222
+ first_result = nil
1223
+ expect do
1224
+ loop do
1225
+ res = @conn.get_result or break
1226
+ res.check
1227
+ first_result ||= res
1228
+ end
1229
+ end.to raise_error(PG::Error)
1230
+ expect( first_result.kind_of?(PG::Result) ).to be_truthy
1231
+ expect( first_result.result_status ).to eq( PG::PGRES_SINGLE_TUPLE )
1232
+ end
1233
+
1234
+ end
1235
+
1236
+ context "multinationalization support", :ruby_19 do
1237
+
1238
+ describe "rubyforge #22925: m17n support" do
1239
+ it "should return results in the same encoding as the client (iso-8859-1)" do
1240
+ out_string = nil
1241
+ @conn.transaction do |conn|
1242
+ conn.internal_encoding = 'iso8859-1'
1243
+ res = conn.exec_params("VALUES ('fantasia')", [], 0)
1244
+ out_string = res[0]['column1']
1245
+ end
1246
+ expect( out_string ).to eq( 'fantasia' )
1247
+ expect( out_string.encoding ).to eq( Encoding::ISO8859_1 )
1248
+ end
1249
+
1250
+ it "should return results in the same encoding as the client (utf-8)" do
1251
+ out_string = nil
1252
+ @conn.transaction do |conn|
1253
+ conn.internal_encoding = 'utf-8'
1254
+ res = conn.exec_params("VALUES ('世界線航跡蔵')", [], 0)
1255
+ out_string = res[0]['column1']
1256
+ end
1257
+ expect( out_string ).to eq( '世界線航跡蔵' )
1258
+ expect( out_string.encoding ).to eq( Encoding::UTF_8 )
1259
+ end
1260
+
1261
+ it "should return results in the same encoding as the client (EUC-JP)" do
1262
+ out_string = nil
1263
+ @conn.transaction do |conn|
1264
+ conn.internal_encoding = 'EUC-JP'
1265
+ stmt = "VALUES ('世界線航跡蔵')".encode('EUC-JP')
1266
+ res = conn.exec_params(stmt, [], 0)
1267
+ out_string = res[0]['column1']
1268
+ end
1269
+ expect( out_string ).to eq( '世界線航跡蔵'.encode('EUC-JP') )
1270
+ expect( out_string.encoding ).to eq( Encoding::EUC_JP )
1271
+ end
1272
+
1273
+ it "returns the results in the correct encoding even if the client_encoding has " +
1274
+ "changed since the results were fetched" do
1275
+ out_string = nil
1276
+ @conn.transaction do |conn|
1277
+ conn.internal_encoding = 'EUC-JP'
1278
+ stmt = "VALUES ('世界線航跡蔵')".encode('EUC-JP')
1279
+ res = conn.exec_params(stmt, [], 0)
1280
+ conn.internal_encoding = 'utf-8'
1281
+ out_string = res[0]['column1']
1282
+ end
1283
+ expect( out_string ).to eq( '世界線航跡蔵'.encode('EUC-JP') )
1284
+ expect( out_string.encoding ).to eq( Encoding::EUC_JP )
1285
+ end
1286
+
1287
+ it "the connection should return ASCII-8BIT when it's set to SQL_ASCII" do
1288
+ @conn.exec "SET client_encoding TO SQL_ASCII"
1289
+ expect( @conn.internal_encoding ).to eq( Encoding::ASCII_8BIT )
1290
+ end
1291
+
1292
+ it "the connection should use JOHAB dummy encoding when it's set to JOHAB" do
1293
+ @conn.set_client_encoding "JOHAB"
1294
+ val = @conn.exec("SELECT chr(x'3391'::int)").values[0][0]
1295
+ expect( val.encoding.name ).to eq( "JOHAB" )
1296
+ expect( val.unpack("H*")[0] ).to eq( "dc65" )
1297
+ end
1298
+
1299
+ it "can retrieve server encoding as text" do
1300
+ enc = @conn.parameter_status "server_encoding"
1301
+ expect( enc ).to eq( "UTF8" )
1302
+ end
1303
+
1304
+ it "can retrieve server encoding as ruby encoding" do
1305
+ expect( @conn.external_encoding ).to eq( Encoding::UTF_8 )
1306
+ end
1307
+
1308
+ it "uses the client encoding for escaped string" do
1309
+ original = "Möhre to 'scape".encode( "utf-16be" )
1310
+ @conn.set_client_encoding( "euc_jp" )
1311
+ escaped = @conn.escape( original )
1312
+ expect( escaped.encoding ).to eq( Encoding::EUC_JP )
1313
+ expect( escaped ).to eq( "Möhre to ''scape".encode(Encoding::EUC_JP) )
1314
+ end
1315
+
1316
+ it "uses the client encoding for escaped literal" do
1317
+ original = "Möhre to 'scape".encode( "utf-16be" )
1318
+ @conn.set_client_encoding( "euc_jp" )
1319
+ escaped = @conn.escape_literal( original )
1320
+ expect( escaped.encoding ).to eq( Encoding::EUC_JP )
1321
+ expect( escaped ).to eq( "'Möhre to ''scape'".encode(Encoding::EUC_JP) )
1322
+ end
1323
+
1324
+ it "uses the client encoding for escaped identifier" do
1325
+ original = "Möhre to 'scape".encode( "utf-16le" )
1326
+ @conn.set_client_encoding( "euc_jp" )
1327
+ escaped = @conn.escape_identifier( original )
1328
+ expect( escaped.encoding ).to eq( Encoding::EUC_JP )
1329
+ expect( escaped ).to eq( "\"Möhre to 'scape\"".encode(Encoding::EUC_JP) )
1330
+ end
1331
+
1332
+ it "uses the client encoding for quote_ident" do
1333
+ original = "Möhre to 'scape".encode( "utf-16le" )
1334
+ @conn.set_client_encoding( "euc_jp" )
1335
+ escaped = @conn.quote_ident( original )
1336
+ expect( escaped.encoding ).to eq( Encoding::EUC_JP )
1337
+ expect( escaped ).to eq( "\"Möhre to 'scape\"".encode(Encoding::EUC_JP) )
1338
+ end
1339
+
1340
+ it "uses the previous string encoding for escaped string" do
1341
+ original = "Möhre to 'scape".encode( "iso-8859-1" )
1342
+ @conn.set_client_encoding( "euc_jp" )
1343
+ escaped = described_class.escape( original )
1344
+ expect( escaped.encoding ).to eq( Encoding::ISO8859_1 )
1345
+ expect( escaped ).to eq( "Möhre to ''scape".encode(Encoding::ISO8859_1) )
1346
+ end
1347
+
1348
+ it "uses the previous string encoding for quote_ident" do
1349
+ original = "Möhre to 'scape".encode( "iso-8859-1" )
1350
+ @conn.set_client_encoding( "euc_jp" )
1351
+ escaped = described_class.quote_ident( original )
1352
+ expect( escaped.encoding ).to eq( Encoding::ISO8859_1 )
1353
+ expect( escaped.encode ).to eq( "\"Möhre to 'scape\"".encode(Encoding::ISO8859_1) )
1354
+ end
1355
+
1356
+ it "raises appropriate error if set_client_encoding is called with invalid arguments" do
1357
+ expect { @conn.set_client_encoding( "invalid" ) }.to raise_error(PG::Error, /invalid value/)
1358
+ expect { @conn.set_client_encoding( :invalid ) }.to raise_error(TypeError)
1359
+ expect { @conn.set_client_encoding( nil ) }.to raise_error(TypeError)
1360
+ end
1361
+ end
1362
+
1363
+ describe "respect and convert character encoding of input strings" do
1364
+ before :each do
1365
+ @conn.internal_encoding = __ENCODING__
1366
+ end
1367
+
1368
+ it "should convert query string and parameters to #exec_params" do
1369
+ r = @conn.exec_params("VALUES( $1, $2, $1=$2, 'grün')".encode("utf-16le"),
1370
+ ['grün'.encode('utf-16be'), 'grün'.encode('iso-8859-1')])
1371
+ expect( r.values ).to eq( [['grün', 'grün', 't', 'grün']] )
1372
+ end
1373
+
1374
+ it "should convert query string to #exec" do
1375
+ r = @conn.exec("SELECT 'grün'".encode("utf-16be"))
1376
+ expect( r.values ).to eq( [['grün']] )
1377
+ end
1378
+
1379
+ it "should convert strings and parameters to #prepare and #exec_prepared" do
1380
+ @conn.prepare("weiß1".encode("utf-16be"), "VALUES( $1, $2, $1=$2, 'grün')".encode("cp850"))
1381
+ r = @conn.exec_prepared("weiß1".encode("utf-32le"),
1382
+ ['grün'.encode('cp936'), 'grün'.encode('utf-16le')])
1383
+ expect( r.values ).to eq( [['grün', 'grün', 't', 'grün']] )
1384
+ end
1385
+
1386
+ it "should convert strings to #describe_prepared" do
1387
+ @conn.prepare("weiß2", "VALUES(123)")
1388
+ r = @conn.describe_prepared("weiß2".encode("utf-16be"))
1389
+ expect( r.nfields ).to eq( 1 )
1390
+ end
1391
+
1392
+ it "should convert strings to #describe_portal" do
1393
+ @conn.exec "DECLARE cörsör CURSOR FOR VALUES(1,2,3)"
1394
+ r = @conn.describe_portal("cörsör".encode("utf-16le"))
1395
+ expect( r.nfields ).to eq( 3 )
1396
+ end
1397
+
1398
+ it "should convert query string to #send_query" do
1399
+ @conn.send_query("VALUES('grün')".encode("utf-16be"))
1400
+ expect( @conn.get_last_result.values ).to eq( [['grün']] )
1401
+ end
1402
+
1403
+ it "should convert query string and parameters to #send_query_params" do
1404
+ @conn.send_query_params("VALUES( $1, $2, $1=$2, 'grün')".encode("utf-16le"),
1405
+ ['grün'.encode('utf-32be'), 'grün'.encode('iso-8859-1')])
1406
+ expect( @conn.get_last_result.values ).to eq( [['grün', 'grün', 't', 'grün']] )
1407
+ end
1408
+
1409
+ it "should convert strings and parameters to #send_prepare and #send_query_prepared" do
1410
+ @conn.send_prepare("weiß3".encode("iso-8859-1"), "VALUES( $1, $2, $1=$2, 'grün')".encode("utf-16be"))
1411
+ @conn.get_last_result
1412
+ @conn.send_query_prepared("weiß3".encode("utf-32le"),
1413
+ ['grün'.encode('utf-16le'), 'grün'.encode('iso-8859-1')])
1414
+ expect( @conn.get_last_result.values ).to eq( [['grün', 'grün', 't', 'grün']] )
1415
+ end
1416
+
1417
+ it "should convert strings to #send_describe_prepared" do
1418
+ @conn.prepare("weiß4", "VALUES(123)")
1419
+ @conn.send_describe_prepared("weiß4".encode("utf-16be"))
1420
+ expect( @conn.get_last_result.nfields ).to eq( 1 )
1421
+ end
1422
+
1423
+ it "should convert strings to #send_describe_portal" do
1424
+ @conn.exec "DECLARE cörsör CURSOR FOR VALUES(1,2,3)"
1425
+ @conn.send_describe_portal("cörsör".encode("utf-16le"))
1426
+ expect( @conn.get_last_result.nfields ).to eq( 3 )
1427
+ end
1428
+
1429
+ it "should convert error string to #put_copy_end" do
1430
+ @conn.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" )
1431
+ @conn.exec( "COPY copytable FROM STDIN" )
1432
+ @conn.put_copy_end("grün".encode("utf-16be"))
1433
+ expect( @conn.get_result.error_message ).to match(/grün/)
1434
+ @conn.get_result
1435
+ end
1436
+ end
1437
+
1438
+ it "rejects command strings with zero bytes" do
1439
+ expect{ @conn.exec( "SELECT 1;\x00" ) }.to raise_error(ArgumentError, /null byte/)
1440
+ expect{ @conn.exec_params( "SELECT 1;\x00", [] ) }.to raise_error(ArgumentError, /null byte/)
1441
+ expect{ @conn.prepare( "abc\x00", "SELECT 1;" ) }.to raise_error(ArgumentError, /null byte/)
1442
+ expect{ @conn.prepare( "abc", "SELECT 1;\x00" ) }.to raise_error(ArgumentError, /null byte/)
1443
+ expect{ @conn.exec_prepared( "abc\x00", [] ) }.to raise_error(ArgumentError, /null byte/)
1444
+ expect{ @conn.describe_prepared( "abc\x00" ) }.to raise_error(ArgumentError, /null byte/)
1445
+ expect{ @conn.describe_portal( "abc\x00" ) }.to raise_error(ArgumentError, /null byte/)
1446
+ expect{ @conn.send_query( "SELECT 1;\x00" ) }.to raise_error(ArgumentError, /null byte/)
1447
+ expect{ @conn.send_query_params( "SELECT 1;\x00", [] ) }.to raise_error(ArgumentError, /null byte/)
1448
+ expect{ @conn.send_prepare( "abc\x00", "SELECT 1;" ) }.to raise_error(ArgumentError, /null byte/)
1449
+ expect{ @conn.send_prepare( "abc", "SELECT 1;\x00" ) }.to raise_error(ArgumentError, /null byte/)
1450
+ expect{ @conn.send_query_prepared( "abc\x00", [] ) }.to raise_error(ArgumentError, /null byte/)
1451
+ expect{ @conn.send_describe_prepared( "abc\x00" ) }.to raise_error(ArgumentError, /null byte/)
1452
+ expect{ @conn.send_describe_portal( "abc\x00" ) }.to raise_error(ArgumentError, /null byte/)
1453
+ end
1454
+
1455
+ it "rejects query params with zero bytes" do
1456
+ expect{ @conn.exec_params( "SELECT 1;\x00", ["ab\x00"] ) }.to raise_error(ArgumentError, /null byte/)
1457
+ expect{ @conn.exec_prepared( "abc\x00", ["ab\x00"] ) }.to raise_error(ArgumentError, /null byte/)
1458
+ expect{ @conn.send_query_params( "SELECT 1;\x00", ["ab\x00"] ) }.to raise_error(ArgumentError, /null byte/)
1459
+ expect{ @conn.send_query_prepared( "abc\x00", ["ab\x00"] ) }.to raise_error(ArgumentError, /null byte/)
1460
+ end
1461
+
1462
+ it "rejects string with zero bytes in escape" do
1463
+ expect{ @conn.escape( "ab\x00cd" ) }.to raise_error(ArgumentError, /null byte/)
1464
+ end
1465
+
1466
+ it "rejects string with zero bytes in escape_literal" do
1467
+ expect{ @conn.escape_literal( "ab\x00cd" ) }.to raise_error(ArgumentError, /null byte/)
1468
+ end
1469
+
1470
+ it "rejects string with zero bytes in escape_identifier" do
1471
+ expect{ @conn.escape_identifier( "ab\x00cd" ) }.to raise_error(ArgumentError, /null byte/)
1472
+ end
1473
+
1474
+ it "rejects string with zero bytes in quote_ident" do
1475
+ expect{ described_class.quote_ident( "ab\x00cd" ) }.to raise_error(ArgumentError, /null byte/)
1476
+ end
1477
+
1478
+ it "rejects Array with string with zero bytes" do
1479
+ original = ["xyz", "2\x00"]
1480
+ expect{ described_class.quote_ident( original ) }.to raise_error(ArgumentError, /null byte/)
1481
+ end
1482
+
1483
+ it "can quote bigger strings with quote_ident" do
1484
+ original = "'01234567\"" * 100
1485
+ escaped = described_class.quote_ident( original )
1486
+ expect( escaped ).to eq( "\"" + original.gsub("\"", "\"\"") + "\"" )
1487
+ end
1488
+
1489
+ it "can quote Arrays with quote_ident" do
1490
+ original = "'01234567\""
1491
+ escaped = described_class.quote_ident( [original]*3 )
1492
+ expected = ["\"" + original.gsub("\"", "\"\"") + "\""] * 3
1493
+ expect( escaped ).to eq( expected.join(".") )
1494
+ end
1495
+
1496
+ it "will raise a TypeError for invalid arguments to quote_ident" do
1497
+ expect{ described_class.quote_ident( nil ) }.to raise_error(TypeError)
1498
+ expect{ described_class.quote_ident( [nil] ) }.to raise_error(TypeError)
1499
+ expect{ described_class.quote_ident( [['a']] ) }.to raise_error(TypeError)
1500
+ end
1501
+
1502
+ describe "Ruby 1.9.x default_internal encoding" do
1503
+
1504
+ it "honors the Encoding.default_internal if it's set and the synchronous interface is used" do
1505
+ @conn.transaction do |txn_conn|
1506
+ txn_conn.internal_encoding = Encoding::ISO8859_1
1507
+ txn_conn.exec( "CREATE TABLE defaultinternaltest ( foo text )" )
1508
+ txn_conn.exec( "INSERT INTO defaultinternaltest VALUES ('Grün und Weiß')" )
1509
+ end
1510
+
1511
+ begin
1512
+ prev_encoding = Encoding.default_internal
1513
+ Encoding.default_internal = Encoding::ISO8859_2
1514
+
1515
+ conn = PG.connect( @conninfo )
1516
+ expect( conn.internal_encoding ).to eq( Encoding::ISO8859_2 )
1517
+ res = conn.exec( "SELECT foo FROM defaultinternaltest" )
1518
+ expect( res[0]['foo'].encoding ).to eq( Encoding::ISO8859_2 )
1519
+ ensure
1520
+ conn.exec( "DROP TABLE defaultinternaltest" )
1521
+ conn.finish if conn
1522
+ Encoding.default_internal = prev_encoding
1523
+ end
1524
+ end
1525
+
1526
+ it "allows users of the async interface to set the client_encoding to the default_internal" do
1527
+ begin
1528
+ prev_encoding = Encoding.default_internal
1529
+ Encoding.default_internal = Encoding::KOI8_R
1530
+
1531
+ @conn.set_default_encoding
1532
+
1533
+ expect( @conn.internal_encoding ).to eq( Encoding::KOI8_R )
1534
+ ensure
1535
+ Encoding.default_internal = prev_encoding
1536
+ end
1537
+ end
1538
+
1539
+ end
1540
+
1541
+
1542
+ it "encodes exception messages with the connection's encoding (#96)", :without_transaction do
1543
+ # Use a new connection so the client_encoding isn't set outside of this example
1544
+ conn = PG.connect( @conninfo )
1545
+ conn.client_encoding = 'iso-8859-15'
1546
+
1547
+ conn.transaction do
1548
+ conn.exec "CREATE TABLE foo (bar TEXT)"
1549
+
1550
+ begin
1551
+ query = "INSERT INTO foo VALUES ('Côte d'Ivoire')".encode( 'iso-8859-15' )
1552
+ conn.exec( query )
1553
+ rescue => err
1554
+ expect( err.message.encoding ).to eq( Encoding::ISO8859_15 )
1555
+ else
1556
+ fail "No exception raised?!"
1557
+ end
1558
+ end
1559
+
1560
+ conn.finish if conn
1561
+ end
1562
+
1563
+ it "handles clearing result in or after set_notice_receiver" do
1564
+ r = nil
1565
+ @conn.set_notice_receiver do |result|
1566
+ r = result
1567
+ expect( r.cleared? ).to eq(false)
1568
+ end
1569
+ @conn.exec "do $$ BEGIN RAISE NOTICE 'foo'; END; $$ LANGUAGE plpgsql;"
1570
+ sleep 0.2
1571
+ expect( r ).to be_a( PG::Result )
1572
+ expect( r.cleared? ).to eq(true)
1573
+ expect( r.autoclear? ).to eq(true)
1574
+ r.clear
1575
+ @conn.set_notice_receiver
1576
+ end
1577
+
1578
+ it "receives properly encoded messages in the notice callbacks" do
1579
+ [:receiver, :processor].each do |kind|
1580
+ notices = []
1581
+ @conn.internal_encoding = 'utf-8'
1582
+ if kind == :processor
1583
+ @conn.set_notice_processor do |msg|
1584
+ notices << msg
1585
+ end
1586
+ else
1587
+ @conn.set_notice_receiver do |result|
1588
+ notices << result.error_message
1589
+ end
1590
+ end
1591
+
1592
+ 3.times do
1593
+ @conn.exec "do $$ BEGIN RAISE NOTICE '世界線航跡蔵'; END; $$ LANGUAGE plpgsql;"
1594
+ end
1595
+
1596
+ expect( notices.length ).to eq( 3 )
1597
+ notices.each do |notice|
1598
+ expect( notice ).to match( /^NOTICE:.*世界線航跡蔵/ )
1599
+ expect( notice.encoding ).to eq( Encoding::UTF_8 )
1600
+ end
1601
+ @conn.set_notice_receiver
1602
+ @conn.set_notice_processor
1603
+ end
1604
+ end
1605
+
1606
+ it "receives properly encoded text from wait_for_notify" do
1607
+ @conn.internal_encoding = 'utf-8'
1608
+ @conn.exec( 'ROLLBACK' )
1609
+ @conn.exec( 'LISTEN "Möhre"' )
1610
+ @conn.exec( %Q{NOTIFY "Möhre", '世界線航跡蔵'} )
1611
+ event, pid, msg = nil
1612
+ @conn.wait_for_notify( 10 ) do |*args|
1613
+ event, pid, msg = *args
1614
+ end
1615
+ @conn.exec( 'UNLISTEN "Möhre"' )
1616
+
1617
+ expect( event ).to eq( "Möhre" )
1618
+ expect( event.encoding ).to eq( Encoding::UTF_8 )
1619
+ expect( msg ).to eq( '世界線航跡蔵' )
1620
+ expect( msg.encoding ).to eq( Encoding::UTF_8 )
1621
+ end
1622
+
1623
+ it "returns properly encoded text from notifies" do
1624
+ @conn.internal_encoding = 'utf-8'
1625
+ @conn.exec( 'ROLLBACK' )
1626
+ @conn.exec( 'LISTEN "Möhre"' )
1627
+ @conn.exec( %Q{NOTIFY "Möhre", '世界線航跡蔵'} )
1628
+ @conn.exec( 'UNLISTEN "Möhre"' )
1629
+
1630
+ notification = @conn.notifies
1631
+ expect( notification[:relname] ).to eq( "Möhre" )
1632
+ expect( notification[:relname].encoding ).to eq( Encoding::UTF_8 )
1633
+ expect( notification[:extra] ).to eq( '世界線航跡蔵' )
1634
+ expect( notification[:extra].encoding ).to eq( Encoding::UTF_8 )
1635
+ expect( notification[:be_pid] ).to be > 0
1636
+ end
1637
+ end
1638
+
1639
+ context "OS thread support", :ruby_19 do
1640
+ it "Connection#exec shouldn't block a second thread" do
1641
+ t = Thread.new do
1642
+ @conn.exec( "select pg_sleep(1)" )
1643
+ end
1644
+
1645
+ sleep 0.5
1646
+ expect( t ).to be_alive()
1647
+ t.join
1648
+ end
1649
+
1650
+ it "Connection.new shouldn't block a second thread" do
1651
+ serv = nil
1652
+ t = Thread.new do
1653
+ serv = TCPServer.new( '127.0.0.1', 54320 )
1654
+ expect {
1655
+ described_class.new( '127.0.0.1', 54320, "", "", "me", "xxxx", "somedb" )
1656
+ }.to raise_error(PG::ConnectionBad, /server closed the connection unexpectedly/)
1657
+ end
1658
+
1659
+ sleep 0.5
1660
+ expect( t ).to be_alive()
1661
+ serv.close
1662
+ t.join
1663
+ end
1664
+ end
1665
+
1666
+ describe "type casting" do
1667
+ it "should raise an error on invalid param mapping" do
1668
+ expect{
1669
+ @conn.exec_params( "SELECT 1", [], nil, :invalid )
1670
+ }.to raise_error(TypeError)
1671
+ end
1672
+
1673
+ it "should return nil if no type mapping is set" do
1674
+ expect( @conn.type_map_for_queries ).to be_kind_of(PG::TypeMapAllStrings)
1675
+ expect( @conn.type_map_for_results ).to be_kind_of(PG::TypeMapAllStrings)
1676
+ end
1677
+
1678
+ it "shouldn't type map params unless requested" do
1679
+ if @conn.server_version < 100000
1680
+ expect{
1681
+ @conn.exec_params( "SELECT $1", [5] )
1682
+ }.to raise_error(PG::IndeterminateDatatype)
1683
+ else
1684
+ # PostgreSQL-10 maps to TEXT type (OID 25)
1685
+ expect( @conn.exec_params( "SELECT $1", [5] ).ftype(0)).to eq(25)
1686
+ end
1687
+ end
1688
+
1689
+ it "should raise an error on invalid encoder to put_copy_data" do
1690
+ expect{
1691
+ @conn.put_copy_data [1], :invalid
1692
+ }.to raise_error(TypeError)
1693
+ end
1694
+
1695
+ it "can type cast parameters to put_copy_data with explicit encoder" do
1696
+ tm = PG::TypeMapByColumn.new [nil]
1697
+ row_encoder = PG::TextEncoder::CopyRow.new type_map: tm
1698
+
1699
+ @conn.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" )
1700
+ res2 = @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res|
1701
+ @conn.put_copy_data [1], row_encoder
1702
+ @conn.put_copy_data ["2"], row_encoder
1703
+ end
1704
+
1705
+ res2 = @conn.copy_data( "COPY copytable FROM STDOUT", row_encoder ) do |res|
1706
+ @conn.put_copy_data [3]
1707
+ @conn.put_copy_data ["4"]
1708
+ end
1709
+
1710
+ res = @conn.exec( "SELECT * FROM copytable ORDER BY col1" )
1711
+ expect( res.values ).to eq( [["1"], ["2"], ["3"], ["4"]] )
1712
+ end
1713
+
1714
+ context "with default query type map" do
1715
+ before :each do
1716
+ @conn2 = described_class.new(@conninfo)
1717
+ tm = PG::TypeMapByClass.new
1718
+ tm[Integer] = PG::TextEncoder::Integer.new oid: 20
1719
+ @conn2.type_map_for_queries = tm
1720
+
1721
+ row_encoder = PG::TextEncoder::CopyRow.new type_map: tm
1722
+ @conn2.encoder_for_put_copy_data = row_encoder
1723
+ end
1724
+ after :each do
1725
+ @conn2.close
1726
+ end
1727
+
1728
+ it "should respect a type mapping for params and it's OID and format code" do
1729
+ res = @conn2.exec_params( "SELECT $1", [5] )
1730
+ expect( res.values ).to eq( [["5"]] )
1731
+ expect( res.ftype(0) ).to eq( 20 )
1732
+ end
1733
+
1734
+ it "should return the current type mapping" do
1735
+ expect( @conn2.type_map_for_queries ).to be_kind_of(PG::TypeMapByClass)
1736
+ end
1737
+
1738
+ it "should work with arbitrary number of params in conjunction with type casting" do
1739
+ begin
1740
+ 3.step( 12, 0.2 ) do |exp|
1741
+ num_params = (2 ** exp).to_i
1742
+ sql = num_params.times.map{|n| "$#{n+1}" }.join(",")
1743
+ params = num_params.times.to_a
1744
+ res = @conn2.exec_params( "SELECT #{sql}", params )
1745
+ expect( res.nfields ).to eq( num_params )
1746
+ expect( res.values ).to eq( [num_params.times.map(&:to_s)] )
1747
+ end
1748
+ rescue PG::ProgramLimitExceeded
1749
+ # Stop silently as soon the server complains about too many params
1750
+ end
1751
+ end
1752
+
1753
+ it "can process #copy_data input queries with row encoder and respects character encoding" do
1754
+ @conn2.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" )
1755
+ res2 = @conn2.copy_data( "COPY copytable FROM STDOUT" ) do |res|
1756
+ @conn2.put_copy_data [1]
1757
+ @conn2.put_copy_data ["Möhre".encode("utf-16le")]
1758
+ end
1759
+
1760
+ res = @conn2.exec( "SELECT * FROM copytable ORDER BY col1" )
1761
+ expect( res.values ).to eq( [["1"], ["Möhre"]] )
1762
+ end
1763
+ end
1764
+
1765
+ context "with default result type map" do
1766
+ before :each do
1767
+ @conn2 = described_class.new(@conninfo)
1768
+ tm = PG::TypeMapByOid.new
1769
+ tm.add_coder PG::TextDecoder::Integer.new oid: 23, format: 0
1770
+ @conn2.type_map_for_results = tm
1771
+
1772
+ row_decoder = PG::TextDecoder::CopyRow.new
1773
+ @conn2.decoder_for_get_copy_data = row_decoder
1774
+ end
1775
+ after :each do
1776
+ @conn2.close
1777
+ end
1778
+
1779
+ it "should respect a type mapping for result" do
1780
+ res = @conn2.exec_params( "SELECT $1::INT", ["5"] )
1781
+ expect( res.values ).to eq( [[5]] )
1782
+ end
1783
+
1784
+ it "should return the current type mapping" do
1785
+ expect( @conn2.type_map_for_results ).to be_kind_of(PG::TypeMapByOid)
1786
+ end
1787
+
1788
+ it "should work with arbitrary number of params in conjunction with type casting" do
1789
+ begin
1790
+ 3.step( 12, 0.2 ) do |exp|
1791
+ num_params = (2 ** exp).to_i
1792
+ sql = num_params.times.map{|n| "$#{n+1}::INT" }.join(",")
1793
+ params = num_params.times.to_a
1794
+ res = @conn2.exec_params( "SELECT #{sql}", params )
1795
+ expect( res.nfields ).to eq( num_params )
1796
+ expect( res.values ).to eq( [num_params.times.to_a] )
1797
+ end
1798
+ rescue PG::ProgramLimitExceeded
1799
+ # Stop silently as soon the server complains about too many params
1800
+ end
1801
+ end
1802
+
1803
+ it "can process #copy_data output with row decoder and respects character encoding" do
1804
+ @conn2.internal_encoding = Encoding::ISO8859_1
1805
+ rows = []
1806
+ res2 = @conn2.copy_data( "COPY (VALUES('1'), ('Möhre')) TO STDOUT".encode("utf-16le") ) do |res|
1807
+ while row=@conn2.get_copy_data
1808
+ rows << row
1809
+ end
1810
+ end
1811
+ expect( rows.last.last.encoding ).to eq( Encoding::ISO8859_1 )
1812
+ expect( rows ).to eq( [["1"], ["Möhre".encode("iso-8859-1")]] )
1813
+ end
1814
+
1815
+ it "can type cast #copy_data output with explicit decoder" do
1816
+ tm = PG::TypeMapByColumn.new [PG::TextDecoder::Integer.new]
1817
+ row_decoder = PG::TextDecoder::CopyRow.new type_map: tm
1818
+ rows = []
1819
+ @conn.copy_data( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT", row_decoder ) do |res|
1820
+ while row=@conn.get_copy_data
1821
+ rows << row
1822
+ end
1823
+ end
1824
+ @conn.copy_data( "COPY (SELECT 3 UNION ALL SELECT 4) TO STDOUT" ) do |res|
1825
+ while row=@conn.get_copy_data( false, row_decoder )
1826
+ rows << row
1827
+ end
1828
+ end
1829
+ expect( rows ).to eq( [[1], [2], [3], [4]] )
1830
+ end
1831
+ end
1832
+ end
1833
+
1834
+ describe "deprecated forms of methods" do
1835
+ it "should forward exec to exec_params" do
1836
+ res = @conn.exec("VALUES($1::INT)", [7]).values
1837
+ expect(res).to eq( [["7"]] )
1838
+ res = @conn.exec("VALUES($1::INT)", [7], 1).values
1839
+ expect(res).to eq( [[[7].pack("N")]] )
1840
+ res = @conn.exec("VALUES(8)", [], 1).values
1841
+ expect(res).to eq( [[[8].pack("N")]] )
1842
+ end
1843
+
1844
+ it "should forward exec_params to exec" do
1845
+ res = @conn.exec_params("VALUES(3); VALUES(4)").values
1846
+ expect(res).to eq( [["4"]] )
1847
+ res = @conn.exec_params("VALUES(3); VALUES(4)", nil).values
1848
+ expect(res).to eq( [["4"]] )
1849
+ res = @conn.exec_params("VALUES(3); VALUES(4)", nil, nil).values
1850
+ expect(res).to eq( [["4"]] )
1851
+ res = @conn.exec_params("VALUES(3); VALUES(4)", nil, 1).values
1852
+ expect(res).to eq( [["4"]] )
1853
+ res = @conn.exec_params("VALUES(3); VALUES(4)", nil, nil, nil).values
1854
+ expect(res).to eq( [["4"]] )
1855
+ expect{
1856
+ @conn.exec_params("VALUES(3); VALUES(4)", nil, nil, nil, nil).values
1857
+ }.to raise_error(ArgumentError)
1858
+ end
1859
+
1860
+ it "should forward send_query to send_query_params" do
1861
+ @conn.send_query("VALUES($1)", [5])
1862
+ expect(@conn.get_last_result.values).to eq( [["5"]] )
1863
+ end
1864
+
1865
+ it "shouldn't forward send_query_params to send_query" do
1866
+ expect{ @conn.send_query_params("VALUES(4)").values }
1867
+ .to raise_error(ArgumentError)
1868
+ expect{ @conn.send_query_params("VALUES(4)", nil).values }
1869
+ .to raise_error(TypeError)
1870
+ end
1871
+ end
1872
+ end