pg 0.17.1 → 0.18.4

Sign up to get free protection for your applications and to get access to all the features.
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