pg 0.17.1 → 0.18.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 (53) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/ChangeLog +2407 -2
  4. data/History.rdoc +68 -0
  5. data/Manifest.txt +29 -1
  6. data/README-Windows.rdoc +15 -26
  7. data/README.rdoc +52 -2
  8. data/Rakefile +56 -18
  9. data/Rakefile.cross +77 -49
  10. data/ext/extconf.rb +33 -26
  11. data/ext/pg.c +142 -21
  12. data/ext/pg.h +242 -6
  13. data/ext/pg_binary_decoder.c +162 -0
  14. data/ext/pg_binary_encoder.c +162 -0
  15. data/ext/pg_coder.c +479 -0
  16. data/ext/pg_connection.c +858 -553
  17. data/ext/pg_copy_coder.c +561 -0
  18. data/ext/pg_errors.c +6 -0
  19. data/ext/pg_result.c +479 -128
  20. data/ext/pg_text_decoder.c +421 -0
  21. data/ext/pg_text_encoder.c +663 -0
  22. data/ext/pg_type_map.c +159 -0
  23. data/ext/pg_type_map_all_strings.c +116 -0
  24. data/ext/pg_type_map_by_class.c +239 -0
  25. data/ext/pg_type_map_by_column.c +312 -0
  26. data/ext/pg_type_map_by_mri_type.c +284 -0
  27. data/ext/pg_type_map_by_oid.c +355 -0
  28. data/ext/pg_type_map_in_ruby.c +299 -0
  29. data/ext/util.c +149 -0
  30. data/ext/util.h +65 -0
  31. data/lib/pg/basic_type_mapping.rb +399 -0
  32. data/lib/pg/coder.rb +83 -0
  33. data/lib/pg/connection.rb +81 -29
  34. data/lib/pg/result.rb +13 -3
  35. data/lib/pg/text_decoder.rb +44 -0
  36. data/lib/pg/text_encoder.rb +27 -0
  37. data/lib/pg/type_map_by_column.rb +15 -0
  38. data/lib/pg.rb +12 -2
  39. data/spec/{lib/helpers.rb → helpers.rb} +101 -39
  40. data/spec/pg/basic_type_mapping_spec.rb +251 -0
  41. data/spec/pg/connection_spec.rb +516 -218
  42. data/spec/pg/result_spec.rb +216 -112
  43. data/spec/pg/type_map_by_class_spec.rb +138 -0
  44. data/spec/pg/type_map_by_column_spec.rb +222 -0
  45. data/spec/pg/type_map_by_mri_type_spec.rb +136 -0
  46. data/spec/pg/type_map_by_oid_spec.rb +149 -0
  47. data/spec/pg/type_map_in_ruby_spec.rb +164 -0
  48. data/spec/pg/type_map_spec.rb +22 -0
  49. data/spec/pg/type_spec.rb +697 -0
  50. data/spec/pg_spec.rb +24 -18
  51. data.tar.gz.sig +0 -0
  52. metadata +111 -45
  53. metadata.gz.sig +0 -0
data/lib/pg/connection.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'pg' unless defined?( PG )
4
+ require 'uri'
4
5
 
5
6
  # The PostgreSQL connection class. The interface for this class is based on
6
7
  # {libpq}[http://www.postgresql.org/docs/9.2/interactive/libpq.html], the C
@@ -34,46 +35,55 @@ class PG::Connection
34
35
  def self::parse_connect_args( *args )
35
36
  return '' if args.empty?
36
37
 
37
- # This will be swapped soon for code that makes options like those required for
38
- # PQconnectdbParams()/PQconnectStartParams(). For now, stick to an options string for
39
- # PQconnectdb()/PQconnectStart().
38
+ hash_arg = args.last.is_a?( Hash ) ? args.pop : {}
39
+ option_string = ''
40
+ options = {}
40
41
 
41
42
  # Parameter 'fallback_application_name' was introduced in PostgreSQL 9.0
42
43
  # together with PQescapeLiteral().
43
- if PG::Connection.instance_methods.find{|m| m.to_sym == :escape_literal }
44
- appname = $0.sub(/^(.{30}).{4,}(.{30})$/){ $1+"..."+$2 }
45
- appname = PG::Connection.quote_connstr( appname )
46
- connopts = ["fallback_application_name=#{appname}"]
47
- else
48
- connopts = []
44
+ if PG::Connection.instance_methods.find {|m| m.to_sym == :escape_literal }
45
+ options[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
49
46
  end
50
47
 
51
- # Handle an options hash first
52
- if args.last.is_a?( Hash )
53
- opthash = args.pop
54
- opthash.each do |key, val|
55
- connopts.push( "%s=%s" % [key, PG::Connection.quote_connstr(val)] )
48
+ if args.length == 1
49
+ case args.first
50
+ when URI, URI.regexp
51
+ uri = URI(args.first)
52
+ options.merge!( Hash[URI.decode_www_form( uri.query )] ) if uri.query
53
+ when /=/
54
+ # Option string style
55
+ option_string = args.first.to_s
56
+ else
57
+ # Positional parameters
58
+ options[CONNECT_ARGUMENT_ORDER.first.to_sym] = args.first
56
59
  end
57
- end
58
-
59
- # Option string style
60
- if args.length == 1 && args.first.to_s.index( '=' )
61
- connopts.unshift( args.first )
62
-
63
- # Append positional parameters
64
60
  else
65
- args.each_with_index do |val, i|
66
- next unless val # Skip nil placeholders
61
+ max = CONNECT_ARGUMENT_ORDER.length
62
+ raise ArgumentError,
63
+ "Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
67
64
 
68
- key = CONNECT_ARGUMENT_ORDER[ i ] or
69
- raise ArgumentError, "Extra positional parameter %d: %p" % [ i+1, val ]
70
- connopts.push( "%s=%s" % [key, PG::Connection.quote_connstr(val.to_s)] )
65
+ CONNECT_ARGUMENT_ORDER.zip( args ) do |(k,v)|
66
+ options[ k.to_sym ] = v if v
71
67
  end
72
68
  end
73
69
 
74
- return connopts.join(' ')
70
+ options.merge!( hash_arg )
71
+
72
+ if uri
73
+ uri.host = nil if options[:host]
74
+ uri.port = nil if options[:port]
75
+ uri.user = nil if options[:user]
76
+ uri.password = nil if options[:password]
77
+ uri.path = '' if options[:dbname]
78
+ uri.query = URI.encode_www_form( options )
79
+ return uri.to_s.sub( /^#{uri.scheme}:(?!\/\/)/, "#{uri.scheme}://" )
80
+ else
81
+ option_string += ' ' unless option_string.empty? && options.empty?
82
+ return option_string + options.map { |k,v| "#{k}=#{quote_connstr(v)}" }.join( ' ' )
83
+ end
75
84
  end
76
85
 
86
+
77
87
  # call-seq:
78
88
  # conn.copy_data( sql ) {|sql_result| ... } -> PG::Result
79
89
  #
@@ -101,7 +111,7 @@ class PG::Connection
101
111
  #
102
112
  # Example with CSV input format:
103
113
  # conn.exec "create table my_table (a text,b text,c text,d text,e text)"
104
- # conn.copy_data "COPY my_table FROM STDOUT CSV" do
114
+ # conn.copy_data "COPY my_table FROM STDIN CSV" do
105
115
  # conn.put_copy_data "some,csv,data,to,copy\n"
106
116
  # conn.put_copy_data "more,csv,data,to,copy\n"
107
117
  # end
@@ -116,12 +126,16 @@ class PG::Connection
116
126
  # This prints all rows of +my_table+ to stdout:
117
127
  # "some,csv,data,to,copy\n"
118
128
  # "more,csv,data,to,copy\n"
119
- def copy_data( sql )
129
+ def copy_data( sql, coder=nil )
120
130
  res = exec( sql )
121
131
 
122
132
  case res.result_status
123
133
  when PGRES_COPY_IN
124
134
  begin
135
+ if coder
136
+ old_coder = self.encoder_for_put_copy_data
137
+ self.encoder_for_put_copy_data = coder
138
+ end
125
139
  yield res
126
140
  rescue Exception => err
127
141
  errmsg = "%s while copy data: %s" % [ err.class.name, err.message ]
@@ -131,10 +145,16 @@ class PG::Connection
131
145
  else
132
146
  put_copy_end
133
147
  get_last_result
148
+ ensure
149
+ self.encoder_for_put_copy_data = old_coder if coder
134
150
  end
135
151
 
136
152
  when PGRES_COPY_OUT
137
153
  begin
154
+ if coder
155
+ old_coder = self.decoder_for_get_copy_data
156
+ self.decoder_for_get_copy_data = coder
157
+ end
138
158
  yield res
139
159
  rescue Exception => err
140
160
  cancel
@@ -153,6 +173,8 @@ class PG::Connection
153
173
  raise PG::NotAllCopyDataRetrieved, "Not all COPY data retrieved"
154
174
  end
155
175
  res
176
+ ensure
177
+ self.decoder_for_get_copy_data = old_coder if coder
156
178
  end
157
179
 
158
180
  else
@@ -172,6 +194,36 @@ class PG::Connection
172
194
  return self.class.conndefaults
173
195
  end
174
196
 
197
+ ### Return the Postgres connection defaults structure as a Hash keyed by option
198
+ ### keyword (as a Symbol).
199
+ ###
200
+ ### See also #conndefaults
201
+ def self.conndefaults_hash
202
+ return self.conndefaults.each_with_object({}) do |info, hash|
203
+ hash[ info[:keyword].to_sym ] = info[:val]
204
+ end
205
+ end
206
+
207
+ ### Returns a Hash with connection defaults. See ::conndefaults_hash
208
+ ### for details.
209
+ def conndefaults_hash
210
+ return self.class.conndefaults_hash
211
+ end
212
+
213
+ # Method 'conninfo' was introduced in PostgreSQL 9.3.
214
+ if self.instance_methods.find{|m| m.to_sym == :conninfo }
215
+
216
+ ### Return the Postgres connection info structure as a Hash keyed by option
217
+ ### keyword (as a Symbol).
218
+ ###
219
+ ### See also #conninfo
220
+ def conninfo_hash
221
+ return self.conninfo.each_with_object({}) do |info, hash|
222
+ hash[ info[:keyword].to_sym ] = info[:val]
223
+ end
224
+ end
225
+ end
226
+
175
227
  end # class PG::Connection
176
228
 
177
229
  # Backward-compatible alias
data/lib/pg/result.rb CHANGED
@@ -5,11 +5,21 @@ require 'pg' unless defined?( PG )
5
5
 
6
6
  class PG::Result
7
7
 
8
- ### Returns all tuples as an array of arrays
9
- def values
10
- return enum_for(:each_row).to_a
8
+ # Apply a type map for all value retrieving methods.
9
+ #
10
+ # +type_map+: a PG::TypeMap instance.
11
+ #
12
+ # See PG::BasicTypeMapForResults
13
+ def map_types!(type_map)
14
+ self.type_map = type_map
15
+ self
11
16
  end
12
17
 
18
+ def inspect
19
+ str = self.to_s
20
+ str[-1,0] = " status=#{res_status(result_status)} ntuples=#{ntuples} nfields=#{nfields} cmd_tuples=#{cmd_tuples}"
21
+ str
22
+ end
13
23
  end # class PG::Result
14
24
 
15
25
  # Backward-compatible alias
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'date'
4
+
5
+ module PG
6
+ module TextDecoder
7
+ class Date < SimpleDecoder
8
+ ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
9
+
10
+ def decode(string, tuple=nil, field=nil)
11
+ if string =~ ISO_DATE
12
+ ::Date.new $1.to_i, $2.to_i, $3.to_i
13
+ else
14
+ string
15
+ end
16
+ end
17
+ end
18
+
19
+ class TimestampWithoutTimeZone < SimpleDecoder
20
+ ISO_DATETIME_WITHOUT_TIMEZONE = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
21
+
22
+ def decode(string, tuple=nil, field=nil)
23
+ if string =~ ISO_DATETIME_WITHOUT_TIMEZONE
24
+ Time.new $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, "#{$6}#{$7}".to_r
25
+ else
26
+ string
27
+ end
28
+ end
29
+ end
30
+
31
+ class TimestampWithTimeZone < SimpleDecoder
32
+ ISO_DATETIME_WITH_TIMEZONE = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?([-\+]\d\d):?(\d\d)?:?(\d\d)?\z/
33
+
34
+ def decode(string, tuple=nil, field=nil)
35
+ if string =~ ISO_DATETIME_WITH_TIMEZONE
36
+ Time.new $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, "#{$6}#{$7}".to_r, "#{$8}:#{$9 || '00'}:#{$10 || '00'}"
37
+ else
38
+ string
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end # module PG
44
+
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module PG
4
+ module TextEncoder
5
+ class Date < SimpleEncoder
6
+ STRFTIME_ISO_DATE = "%Y-%m-%d".freeze
7
+ def encode(value)
8
+ value.respond_to?(:strftime) ? value.strftime(STRFTIME_ISO_DATE) : value
9
+ end
10
+ end
11
+
12
+ class TimestampWithoutTimeZone < SimpleEncoder
13
+ STRFTIME_ISO_DATETIME_WITHOUT_TIMEZONE = "%Y-%m-%d %H:%M:%S.%N".freeze
14
+ def encode(value)
15
+ value.respond_to?(:strftime) ? value.strftime(STRFTIME_ISO_DATETIME_WITHOUT_TIMEZONE) : value
16
+ end
17
+ end
18
+
19
+ class TimestampWithTimeZone < SimpleEncoder
20
+ STRFTIME_ISO_DATETIME_WITH_TIMEZONE = "%Y-%m-%d %H:%M:%S.%N %:z".freeze
21
+ def encode(value)
22
+ value.respond_to?(:strftime) ? value.strftime(STRFTIME_ISO_DATETIME_WITH_TIMEZONE) : value
23
+ end
24
+ end
25
+ end
26
+ end # module PG
27
+
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pg' unless defined?( PG )
4
+
5
+ class PG::TypeMapByColumn
6
+ # Returns the type oids of the assigned coders.
7
+ def oids
8
+ coders.map{|c| c.oid if c }
9
+ end
10
+
11
+ def inspect
12
+ type_strings = coders.map{|c| c ? "#{c.name}:#{c.format}" : 'nil' }
13
+ "#<#{self.class} #{type_strings.join(' ')}>"
14
+ end
15
+ end
data/lib/pg.rb CHANGED
@@ -7,7 +7,12 @@ rescue LoadError
7
7
  if RUBY_PLATFORM =~/(mswin|mingw)/i
8
8
  major_minor = RUBY_VERSION[ /^(\d+\.\d+)/ ] or
9
9
  raise "Oops, can't extract the major/minor version from #{RUBY_VERSION.dump}"
10
+
11
+ # Set the PATH environment variable, so that libpq.dll can be found.
12
+ old_path = ENV['PATH']
13
+ ENV['PATH'] = "#{File.expand_path("../#{RUBY_PLATFORM}", __FILE__)};#{old_path}"
10
14
  require "#{major_minor}/pg_ext"
15
+ ENV['PATH'] = old_path
11
16
  else
12
17
  raise
13
18
  end
@@ -19,10 +24,10 @@ end
19
24
  module PG
20
25
 
21
26
  # Library version
22
- VERSION = '0.17.1'
27
+ VERSION = '0.18.4'
23
28
 
24
29
  # VCS revision
25
- REVISION = %q$Revision: 22d57e3a2b37 $
30
+ REVISION = %q$Revision: da42b972b5ab $
26
31
 
27
32
  class NotAllCopyDataRetrieved < PG::Error
28
33
  end
@@ -43,6 +48,11 @@ module PG
43
48
 
44
49
  require 'pg/exceptions'
45
50
  require 'pg/constants'
51
+ require 'pg/coder'
52
+ require 'pg/text_encoder'
53
+ require 'pg/text_decoder'
54
+ require 'pg/basic_type_mapping'
55
+ require 'pg/type_map_by_column'
46
56
  require 'pg/connection'
47
57
  require 'pg/result'
48
58
 
@@ -9,6 +9,38 @@ TEST_DIRECTORY = Pathname.getwd + "tmp_test_specs"
9
9
 
10
10
  module PG::TestingHelpers
11
11
 
12
+ ### Automatically set up the database when it's used, and wrap a transaction around
13
+ ### examples that don't disable it.
14
+ def self::included( mod )
15
+ super
16
+
17
+ if mod.respond_to?( :around )
18
+
19
+ mod.before( :all ) { @conn = setup_testing_db(described_class ? described_class.name : mod.description) }
20
+
21
+ mod.around( :each ) do |example|
22
+ begin
23
+ @conn.exec( 'BEGIN' ) unless example.metadata[:without_transaction]
24
+ if PG.respond_to?( :library_version )
25
+ desc = example.source_location.join(':')
26
+ @conn.exec_params %Q{SET application_name TO '%s'} %
27
+ [@conn.escape_string(desc.slice(-60))]
28
+ end
29
+ example.run
30
+ ensure
31
+ @conn.exec( 'ROLLBACK' ) unless example.metadata[:without_transaction]
32
+ end
33
+ end
34
+
35
+ mod.after( :all ) { teardown_testing_db(@conn) }
36
+ end
37
+
38
+ end
39
+
40
+
41
+ #
42
+ # Examples
43
+ #
12
44
 
13
45
  # Set some ANSI escape code constants (Shamelessly stolen from Perl's
14
46
  # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
@@ -121,28 +153,9 @@ module PG::TestingHelpers
121
153
  end
122
154
 
123
155
  # Eliminate the noise of creating/tearing down the database by
124
- # redirecting STDERR/STDOUT to a logfile if the Ruby interpreter
125
- # supports fork()
156
+ # redirecting STDERR/STDOUT to a logfile
126
157
  logfh = File.open( logpath, File::WRONLY|File::CREAT|File::APPEND )
127
- begin
128
- pid = fork
129
- rescue NotImplementedError
130
- logfh.close
131
- system( *cmd )
132
- else
133
- if pid
134
- logfh.close
135
- else
136
- $stdout.reopen( logfh )
137
- $stderr.reopen( $stdout )
138
- $stderr.puts( ">>> " + cmd.shelljoin )
139
- exec( *cmd )
140
- $stderr.puts "After the exec()?!??!"
141
- exit!
142
- end
143
-
144
- Process.wait( pid )
145
- end
158
+ system( *cmd, [STDOUT, STDERR] => logfh )
146
159
 
147
160
  raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success?
148
161
  end
@@ -242,35 +255,82 @@ module PG::TestingHelpers
242
255
  unless conns.empty?
243
256
  puts "Lingering connections remain:"
244
257
  conns.each do |row|
245
- puts " [%d] {%s} %s -- %s" % row.values_at( 'pid', 'state', 'application_name', 'query' )
258
+ puts " [%s] {%s} %s -- %s" % row.values_at( 'pid', 'state', 'application_name', 'query' )
246
259
  end
247
260
  end
248
261
  end
249
262
  end
250
263
 
251
- def connection_string_should_contain_application_name(conn_args, app_name)
252
- conn_name = conn_args.match(/application_name='(.*)'/)[1]
253
- conn_name.should include(app_name[0..10])
254
- conn_name.should include(app_name[-10..-1])
255
- conn_name.length.should <= 64
264
+
265
+ # Retrieve the names of the column types of a given result set.
266
+ def result_typenames(res)
267
+ @conn.exec( "SELECT " + res.nfields.times.map{|i| "format_type($#{i*2+1},$#{i*2+2})"}.join(","),
268
+ res.nfields.times.map{|i| [res.ftype(i), res.fmod(i)] }.flatten ).
269
+ values[0]
270
+ end
271
+
272
+
273
+ # A matcher for checking the status of a PG::Connection to ensure it's still
274
+ # usable.
275
+ class ConnStillUsableMatcher
276
+
277
+ def initialize
278
+ @conn = nil
279
+ @problem = nil
280
+ end
281
+
282
+ def matches?( conn )
283
+ @conn = conn
284
+ @problem = self.check_for_problems
285
+ return @problem.nil?
286
+ end
287
+
288
+ def check_for_problems
289
+ return "is finished" if @conn.finished?
290
+ return "has bad status" unless @conn.status == PG::CONNECTION_OK
291
+ return "has bad transaction status (%d)" % [ @conn.transaction_status ] unless
292
+ @conn.transaction_status.between?( PG::PQTRANS_IDLE, PG::PQTRANS_INTRANS )
293
+ return "is not usable." unless self.can_exec_query?
294
+ return nil
295
+ end
296
+
297
+ def can_exec_query?
298
+ @conn.send_query( "VALUES (1)" )
299
+ @conn.get_last_result.values == [["1"]]
300
+ end
301
+
302
+ def failure_message
303
+ return "expected %p to be usable, but it %s" % [ @conn, @problem ]
304
+ end
305
+
306
+ def failure_message_when_negated
307
+ "expected %p not to be usable, but it still is" % [ @conn ]
308
+ end
309
+
256
310
  end
257
311
 
258
- # Ensure the connection is in a clean execution status.
259
- def verify_clean_exec_status
260
- @conn.send_query( "VALUES (1)" )
261
- @conn.get_last_result.values.should == [["1"]]
312
+
313
+ ### Return a ConnStillUsableMatcher to be used like:
314
+ ###
315
+ ### expect( pg_conn ).to still_be_usable
316
+ ###
317
+ def still_be_usable
318
+ return ConnStillUsableMatcher.new
262
319
  end
320
+
263
321
  end
264
322
 
265
323
 
266
324
  RSpec.configure do |config|
267
- ruby_version_vec = RUBY_VERSION.split('.').map {|c| c.to_i }.pack( "N*" )
268
-
269
325
  config.include( PG::TestingHelpers )
270
- config.treat_symbols_as_metadata_keys_with_true_values = true
271
326
 
272
- config.mock_with :rspec
273
- config.filter_run_excluding :ruby_19 if ruby_version_vec <= [1,9,1].pack( "N*" )
327
+ config.run_all_when_everything_filtered = true
328
+ config.filter_run :focus
329
+ config.order = 'random'
330
+ config.mock_with( :rspec ) do |mock|
331
+ mock.syntax = :expect
332
+ end
333
+
274
334
  if RUBY_PLATFORM =~ /mingw|mswin/
275
335
  config.filter_run_excluding :unix
276
336
  else
@@ -283,11 +343,13 @@ RSpec.configure do |config|
283
343
  PG::Connection.instance_methods.map( &:to_sym ).include?( :escape_literal )
284
344
 
285
345
  if !PG.respond_to?( :library_version )
286
- config.filter_run_excluding( :postgresql_91, :postgresql_92, :postgresql_93 )
346
+ config.filter_run_excluding( :postgresql_91, :postgresql_92, :postgresql_93, :postgresql_94 )
287
347
  elsif PG.library_version < 90200
288
- config.filter_run_excluding( :postgresql_92, :postgresql_93 )
348
+ config.filter_run_excluding( :postgresql_92, :postgresql_93, :postgresql_94 )
289
349
  elsif PG.library_version < 90300
290
- config.filter_run_excluding( :postgresql_93 )
350
+ config.filter_run_excluding( :postgresql_93, :postgresql_94 )
351
+ elsif PG.library_version < 90400
352
+ config.filter_run_excluding( :postgresql_94 )
291
353
  end
292
354
  end
293
355