Skip to content

Commit

Permalink
Allow URI connection string
Browse files Browse the repository at this point in the history
(Imported from #3)
  • Loading branch information
cbandy committed Aug 22, 2014
1 parent a1681e4 commit 244a701
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 29 deletions.
64 changes: 37 additions & 27 deletions lib/pg/connection.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env ruby

require 'pg' unless defined?( PG )
require 'uri'

# The PostgreSQL connection class. The interface for this class is based on
# {libpq}[http://www.postgresql.org/docs/9.2/interactive/libpq.html], the C
Expand Down Expand Up @@ -34,46 +35,55 @@ def self::quote_connstr( value )
def self::parse_connect_args( *args )
return '' if args.empty?

# This will be swapped soon for code that makes options like those required for
# PQconnectdbParams()/PQconnectStartParams(). For now, stick to an options string for
# PQconnectdb()/PQconnectStart().
hash_arg = args.last.is_a?( Hash ) ? args.pop : {}
option_string = ''
options = {}

# Parameter 'fallback_application_name' was introduced in PostgreSQL 9.0
# together with PQescapeLiteral().
if PG::Connection.instance_methods.find{|m| m.to_sym == :escape_literal }
appname = $0.sub(/^(.{30}).{4,}(.{30})$/){ $1+"..."+$2 }
appname = PG::Connection.quote_connstr( appname )
connopts = ["fallback_application_name=#{appname}"]
else
connopts = []
if PG::Connection.instance_methods.find {|m| m.to_sym == :escape_literal }
options[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
end

# Handle an options hash first
if args.last.is_a?( Hash )
opthash = args.pop
opthash.each do |key, val|
connopts.push( "%s=%s" % [key, PG::Connection.quote_connstr(val)] )
if args.length == 1
case args.first
when URI, URI.regexp
uri = URI(args.first)
options.merge!( Hash[URI.decode_www_form( uri.query )] ) if uri.query
when /=/
# Option string style
option_string = args.first.to_s
else
# Positional parameters
options[CONNECT_ARGUMENT_ORDER.first.to_sym] = args.first
end
end

# Option string style
if args.length == 1 && args.first.to_s.index( '=' )
connopts.unshift( args.first )

# Append positional parameters
else
args.each_with_index do |val, i|
next unless val # Skip nil placeholders
max = CONNECT_ARGUMENT_ORDER.length
raise ArgumentError,
"Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max

key = CONNECT_ARGUMENT_ORDER[ i ] or
raise ArgumentError, "Extra positional parameter %d: %p" % [ i+1, val ]
connopts.push( "%s=%s" % [key, PG::Connection.quote_connstr(val.to_s)] )
CONNECT_ARGUMENT_ORDER.zip( args ) do |(k,v)|
options[ k.to_sym ] = v if v
end
end

return connopts.join(' ')
options.merge!( hash_arg )

if uri
uri.host = nil if options[:host]
uri.port = nil if options[:port]
uri.user = nil if options[:user]
uri.password = nil if options[:password]
uri.path = '' if options[:dbname]
uri.query = URI.encode_www_form( options )
return uri.to_s.sub( /^#{uri.scheme}:(?!\/\/)/, "#{uri.scheme}://" )
else
option_string += ' ' unless option_string.empty? && options.empty?
return option_string + options.map { |k,v| "#{k}=#{quote_connstr(v)}" }.join( ' ' )
end
end


# call-seq:
# conn.copy_data( sql ) {|sql_result| ... } -> PG::Result
#
Expand Down
65 changes: 63 additions & 2 deletions spec/pg/connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,69 @@
expect( optstring ).to match( /(^|\s)user='jrandom'/ )
end

it "can create a connection option string from an option string and a hash" do
optstring = described_class.parse_connect_args( 'dbname=original', :user => 'jrandom' )

expect( optstring ).to be_a( String )
expect( optstring ).to match( /(^|\s)dbname=original/ )
expect( optstring ).to match( /(^|\s)user='jrandom'/ )
end

it "escapes single quotes and backslashes in connection parameters" do
expect(
described_class.parse_connect_args( "DB 'browser' \\" )
).to match( /host='DB \\'browser\\' \\\\'/ )

end

let(:uri) { 'postgresql://user:pass@pgsql.example.com:222/db01?sslmode=require' }

it "can connect using a URI" do
string = described_class.parse_connect_args(uri)

expect( string ).to be_a( String )
expect( string ).to match( %r{^postgresql://user:pass@pgsql.example.com:222/db01\?} )
expect( string ).to match( %r{\?.*sslmode=require} )

string = described_class.parse_connect_args(URI.parse(uri))

expect( string ).to be_a( String )
expect( string ).to match( %r{^postgresql://user:pass@pgsql.example.com:222/db01\?} )
expect( string ).to match( %r{\?.*sslmode=require} )
end

it "can create a connection URI from a URI and a hash" do
string = described_class.parse_connect_args(uri, :connect_timeout => 2)

expect( string ).to be_a( String )
expect( string ).to match( %r{^postgresql://user:pass@pgsql.example.com:222/db01\?} )
expect( string ).to match( %r{\?.*sslmode=require} )
expect( string ).to match( %r{\?.*connect_timeout=2} )

string = described_class.parse_connect_args(uri, :user => 'a', :password => 'b', :host => 'localhost', :port => 555, :dbname => 'x')

expect( string ).to be_a( String )
expect( string ).to match( %r{^postgresql://\?} )
expect( string ).to match( %r{\?.*user=a} )
expect( string ).to match( %r{\?.*password=b} )
expect( string ).to match( %r{\?.*host=localhost} )
expect( string ).to match( %r{\?.*port=555} )
expect( string ).to match( %r{\?.*dbname=x} )
end

it "can create a connection URI with a non-standard domain socket directory" do
string = described_class.parse_connect_args('postgresql://%2Fvar%2Flib%2Fpostgresql/dbname')

expect( string ).to be_a( String )
expect( string ).to match( %r{^postgresql://%2Fvar%2Flib%2Fpostgresql/dbname} )

string = described_class.parse_connect_args('postgresql:///dbname', :host => '/var/lib/postgresql')

expect( string ).to be_a( String )
expect( string ).to match( %r{^postgresql:///dbname\?} )
expect( string ).to match( %r{\?.*host=%2Fvar%2Flib%2Fpostgresql} )
end

it "connects with defaults if no connection parameters are given" do
expect( described_class.parse_connect_args ).to eq( '' )
end
Expand Down Expand Up @@ -89,8 +145,13 @@

it "raises an exception when connecting with an invalid number of arguments" do
expect {
described_class.connect( 1, 2, 3, 4, 5, 6, 7, 'extra' )
}.to raise_error( ArgumentError, /extra positional parameter/i )
described_class.connect( 1, 2, 3, 4, 5, 6, 7, 'the-extra-arg' )
}.to raise_error do |error|
expect( error ).to be_an( ArgumentError )
expect( error.message ).to match( /extra positional parameter/i )
expect( error.message ).to match( /8/ )
expect( error.message ).to match( /the-extra-arg/ )
end
end

it "can connect asynchronously", :socket_io do
Expand Down

0 comments on commit 244a701

Please sign in to comment.