pg-1.2.3/0000755000004100000410000000000013704151215012152 5ustar www-datawww-datapg-1.2.3/POSTGRES0000644000004100000410000000225013704151215013342 0ustar www-datawww-dataPostgreSQL Database Management System (formerly known as Postgres, then as Postgres95) Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group Portions Copyright (c) 1994, The Regents of the University of California Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. pg-1.2.3/pg.gemspec0000644000004100000410000002006513704151215014130 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: pg 1.2.3 ruby lib # stub: ext/extconf.rb Gem::Specification.new do |s| s.name = "pg".freeze s.version = "1.2.3" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "homepage_uri" => "https://github.com/ged/ruby-pg" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Michael Granger".freeze, "Lars Kanis".freeze] s.cert_chain = ["-----BEGIN CERTIFICATE-----\nMIID+DCCAmCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdnZWQv\nREM9RmFlcmllTVVEL0RDPW9yZzAeFw0xOTEyMjQyMDE5NTFaFw0yMDEyMjMyMDE5\nNTFaMCIxIDAeBgNVBAMMF2dlZC9EQz1GYWVyaWVNVUQvREM9b3JnMIIBojANBgkq\nhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvyVhkRzvlEs0fe7145BYLfN6njX9ih5H\nL60U0p0euIurpv84op9CNKF9tx+1WKwyQvQP7qFGuZxkSUuWcP/sFhDXL1lWUuIl\nM4uHbGCRmOshDrF4dgnBeOvkHr1fIhPlJm5FO+Vew8tSQmlDsosxLUx+VB7DrVFO\n5PU2AEbf04GGSrmqADGWXeaslaoRdb1fu/0M5qfPTRn5V39sWD9umuDAF9qqil/x\nSl6phTvgBrG8GExHbNZpLARd3xrBYLEFsX7RvBn2UPfgsrtvpdXjsHGfpT3IPN+B\nvQ66lts4alKC69TE5cuKasWBm+16A4aEe3XdZBRNmtOu/g81gvwA7fkJHKllJuaI\ndXzdHqq+zbGZVSQ7pRYHYomD0IiDe1DbIouFnPWmagaBnGHwXkDT2bKKP+s2v21m\nozilJg4aar2okb/RA6VS87o+d7g6LpDDMMQjH4G9OPnJENLdhu8KnPw/ivSVvQw7\nN2I4L/ZOIe2DIVuYH7aLHfjZDQv/mNgpAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYD\nVR0PBAQDAgSwMB0GA1UdDgQWBBRyjf55EbrHagiRLqt5YAd3yb8k4DANBgkqhkiG\n9w0BAQsFAAOCAYEAifxlz7x0EfT3fjhM520ZEIrWa+tLMuLKNefkY18u8tZnx4EX\nXxwh3tna3fvNfrOrdY5leIj1dbv4FTRg+gIBnIxAySqvpGvI/Axg5EdYbwninCLL\nLAKCmRo+5QwaPMYN2zdHIjGrp8jg1neCo5zy6tVvyTv0DMI6FLrydVJYduMMDFSy\ngQKR1rVOcCJtnBnLCF9+kKEUKohAHOmGsE7OBZFnjMIpH5yUDUVJKByv0gIipFt0\n1T6zff6oVU0w8WBiNKR381+6sF3wIZVnVY0XeJg6hNL+YecE8ILxLhHTmtT/BO0S\n3xPze9uXDR+iD6HYl8KU5QEg/dXFPhfQb512vVkTJDZvMcwu6PxDUjHFChLjAji/\nAZXjg1C5E9znTkeUR8ieU9F1MOKoiH57a5lYSTI8Ga8PpsNXTxNeXc16Ob26CqrJ\n83uuAYSy65yXDGXXPVBeKPVnYrqp91pqpS5Nh7wfuiCrE8lgU8PATh7K4BV1UhAT\n0MHbAT42wTYkfUj3\n-----END CERTIFICATE-----\n".freeze] s.date = "2020-03-18" s.description = "Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/].\n\nIt works with {PostgreSQL 9.2 and later}[http://www.postgresql.org/support/versioning/].\n\nA small example usage:\n\n #!/usr/bin/env ruby\n\n require 'pg'\n\n # Output a table of current connections to the DB\n conn = PG.connect( dbname: 'sales' )\n conn.exec( \"SELECT * FROM pg_stat_activity\" ) do |result|\n puts \" PID | User | Query\"\n result.each do |row|\n puts \" %7d | %-16s | %s \" %\n row.values_at('procpid', 'usename', 'current_query')\n end\n end".freeze s.email = ["ged@FaerieMUD.org".freeze, "lars@greiz-reinsdorf.de".freeze] s.extensions = ["ext/extconf.rb".freeze] s.extra_rdoc_files = ["Contributors.rdoc".freeze, "History.rdoc".freeze, "LICENSE".freeze, "Manifest.txt".freeze, "POSTGRES".freeze, "README-OS_X.rdoc".freeze, "README-Windows.rdoc".freeze, "README.ja.rdoc".freeze, "README.rdoc".freeze, "ext/errorcodes.txt".freeze, "ext/gvl_wrappers.c".freeze, "ext/pg.c".freeze, "ext/pg_binary_decoder.c".freeze, "ext/pg_binary_encoder.c".freeze, "ext/pg_coder.c".freeze, "ext/pg_connection.c".freeze, "ext/pg_copy_coder.c".freeze, "ext/pg_errors.c".freeze, "ext/pg_record_coder.c".freeze, "ext/pg_result.c".freeze, "ext/pg_text_decoder.c".freeze, "ext/pg_text_encoder.c".freeze, "ext/pg_tuple.c".freeze, "ext/pg_type_map.c".freeze, "ext/pg_type_map_all_strings.c".freeze, "ext/pg_type_map_by_class.c".freeze, "ext/pg_type_map_by_column.c".freeze, "ext/pg_type_map_by_mri_type.c".freeze, "ext/pg_type_map_by_oid.c".freeze, "ext/pg_type_map_in_ruby.c".freeze, "ext/pg_util.c".freeze] s.files = [".gemtest".freeze, "BSDL".freeze, "ChangeLog".freeze, "Contributors.rdoc".freeze, "History.rdoc".freeze, "LICENSE".freeze, "Manifest.txt".freeze, "POSTGRES".freeze, "README-OS_X.rdoc".freeze, "README-Windows.rdoc".freeze, "README.ja.rdoc".freeze, "README.rdoc".freeze, "Rakefile".freeze, "Rakefile.cross".freeze, "ext/errorcodes.def".freeze, "ext/errorcodes.rb".freeze, "ext/errorcodes.txt".freeze, "ext/extconf.rb".freeze, "ext/gvl_wrappers.c".freeze, "ext/gvl_wrappers.h".freeze, "ext/pg.c".freeze, "ext/pg.h".freeze, "ext/pg_binary_decoder.c".freeze, "ext/pg_binary_encoder.c".freeze, "ext/pg_coder.c".freeze, "ext/pg_connection.c".freeze, "ext/pg_copy_coder.c".freeze, "ext/pg_errors.c".freeze, "ext/pg_record_coder.c".freeze, "ext/pg_result.c".freeze, "ext/pg_text_decoder.c".freeze, "ext/pg_text_encoder.c".freeze, "ext/pg_tuple.c".freeze, "ext/pg_type_map.c".freeze, "ext/pg_type_map_all_strings.c".freeze, "ext/pg_type_map_by_class.c".freeze, "ext/pg_type_map_by_column.c".freeze, "ext/pg_type_map_by_mri_type.c".freeze, "ext/pg_type_map_by_oid.c".freeze, "ext/pg_type_map_in_ruby.c".freeze, "ext/pg_util.c".freeze, "ext/pg_util.h".freeze, "ext/vc/pg.sln".freeze, "ext/vc/pg_18/pg.vcproj".freeze, "ext/vc/pg_19/pg_19.vcproj".freeze, "lib/pg.rb".freeze, "lib/pg/basic_type_mapping.rb".freeze, "lib/pg/binary_decoder.rb".freeze, "lib/pg/coder.rb".freeze, "lib/pg/connection.rb".freeze, "lib/pg/constants.rb".freeze, "lib/pg/exceptions.rb".freeze, "lib/pg/result.rb".freeze, "lib/pg/text_decoder.rb".freeze, "lib/pg/text_encoder.rb".freeze, "lib/pg/tuple.rb".freeze, "lib/pg/type_map_by_column.rb".freeze, "spec/data/expected_trace.out".freeze, "spec/data/random_binary_data".freeze, "spec/helpers.rb".freeze, "spec/pg/basic_type_mapping_spec.rb".freeze, "spec/pg/connection_spec.rb".freeze, "spec/pg/connection_sync_spec.rb".freeze, "spec/pg/result_spec.rb".freeze, "spec/pg/tuple_spec.rb".freeze, "spec/pg/type_map_by_class_spec.rb".freeze, "spec/pg/type_map_by_column_spec.rb".freeze, "spec/pg/type_map_by_mri_type_spec.rb".freeze, "spec/pg/type_map_by_oid_spec.rb".freeze, "spec/pg/type_map_in_ruby_spec.rb".freeze, "spec/pg/type_map_spec.rb".freeze, "spec/pg/type_spec.rb".freeze, "spec/pg_spec.rb".freeze] s.homepage = "https://github.com/ged/ruby-pg".freeze s.licenses = ["BSD-2-Clause".freeze] s.rdoc_options = ["--main".freeze, "README.rdoc".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.2".freeze) s.rubygems_version = "2.5.2.1".freeze s.summary = "Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]".freeze if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q.freeze, ["~> 3.20"]) s.add_development_dependency(%q.freeze, ["~> 1.0"]) s.add_development_dependency(%q.freeze, ["~> 0.10"]) s.add_development_dependency(%q.freeze, ["~> 0.2"]) s.add_development_dependency(%q.freeze, ["~> 1.4"]) s.add_development_dependency(%q.freeze, ["~> 1.0"]) s.add_development_dependency(%q.freeze, ["~> 1.0"]) s.add_development_dependency(%q.freeze, ["~> 5.1"]) s.add_development_dependency(%q.freeze, ["~> 3.5"]) else s.add_dependency(%q.freeze, ["~> 3.20"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 0.10"]) s.add_dependency(%q.freeze, ["~> 0.2"]) s.add_dependency(%q.freeze, ["~> 1.4"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 5.1"]) s.add_dependency(%q.freeze, ["~> 3.5"]) end else s.add_dependency(%q.freeze, ["~> 3.20"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 0.10"]) s.add_dependency(%q.freeze, ["~> 0.2"]) s.add_dependency(%q.freeze, ["~> 1.4"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 5.1"]) s.add_dependency(%q.freeze, ["~> 3.5"]) end end pg-1.2.3/data.tar.gz.sig0000444000004100000410000000060013704151215014765 0ustar www-datawww-dataP8]Qbwʦv2:qXwFG#KoV=ECVxuFD.a]sjnڲLUD EdFƨDF.#XRܰ҅0({ ڇ~K_@EX I&6RQlҢ4YkbwO= Ƿ5[=O|%݄S.nEˈ"$?_L1L{JK>1F*^9E3s2]$ b-XQxpg-1.2.3/README.ja.rdoc0000644000004100000410000000043713704151215014355 0ustar www-datawww-data= pg home :: https://github.com/ged/ruby-pg docs :: http://deveiate.org/code/pg == Description This file needs a translation of the English README. Pull requests, patches, or volunteers gladly accepted. Until such time, please accept my sincere apologies for not knowing Japanese. pg-1.2.3/README-Windows.rdoc0000644000004100000410000000426013704151215015412 0ustar www-datawww-data= Compiling 'pg' on MS Windows In order to build this extension on MS Windows you will need a couple things. First, a compiler. For the one click installer this means you should use the DevKit or the compiler that comes with cygwin if you're building on that platform. If you've built Ruby yourself, you should use the same compiler to build this library that you used to build Ruby. Second, PostgreSQL. Be sure you installed it with the development header files if you installed it using the standard PostgreSQL installer for Windows. If you didn't, you can run the installer again, select "modify", and then select the 'development headers' option to install them. I recommend making sure that 'pg_config.exe' is in your PATH. The PostgreSQL installer for Windows does not necessarily update your PATH when it installs itself, so you may need to do this manually. This isn't strictly necessary, however. In order to build ruby-pg, just run 'rake'. If the pg_config.exe executable is not in your PATH, you'll need to explicitly point ruby-pg to where your PostgreSQL headers and libraries are with something like this: rake --with-pg-dir=c:/progra~1/postgr~1/8.3 Adjust your path accordingly. BE SURE TO USE THE SHORT PATH NAMES! If you try to use a path with spaces in it, the nmake.exe program will choke. == Building binary 'pg' gems for MS Windows Binary gems for windows can be built on Linux, OS-X and even on Windows with the help of docker. This is how regular windows gems are built for rubygems.org . To do this, install boot2docker {on Windows}[https://github.com/boot2docker/windows-installer/releases] or {on OS X}[https://github.com/boot2docker/osx-installer/releases] and make sure it is started. A native Docker installation is best on Linux. Then run: rake gem:windows This will download a docker image suited for building windows gems, and it will download and build OpenSSL and PostgreSQL. Finally the gem is built containing binaries for all supported ruby versions. == Reporting Problems If you have any problems you can submit them via {the project's issue-tracker}[https://github.com/ged/ruby-pg/issues]. And submit questions, problems, or solutions, so that it can be improved. pg-1.2.3/spec/0000755000004100000410000000000013704151215013104 5ustar www-datawww-datapg-1.2.3/spec/pg/0000755000004100000410000000000013704151215013512 5ustar www-datawww-datapg-1.2.3/spec/pg/type_map_by_column_spec.rb0000644000004100000410000002020513704151215020735 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative '../helpers' require 'pg' describe PG::TypeMapByColumn do let!(:textenc_int){ PG::TextEncoder::Integer.new name: 'INT4', oid: 23 } let!(:textdec_int){ PG::TextDecoder::Integer.new name: 'INT4', oid: 23 } let!(:textenc_float){ PG::TextEncoder::Float.new name: 'FLOAT4', oid: 700 } let!(:textdec_float){ PG::TextDecoder::Float.new name: 'FLOAT4', oid: 700 } let!(:textenc_string){ PG::TextEncoder::String.new name: 'TEXT', oid: 25 } let!(:textdec_string){ PG::TextDecoder::String.new name: 'TEXT', oid: 25 } let!(:textdec_bytea){ PG::TextDecoder::Bytea.new name: 'BYTEA', oid: 17 } let!(:binaryenc_bytea){ PG::BinaryEncoder::Bytea.new name: 'BYTEA', oid: 17, format: 1 } let!(:binarydec_bytea){ PG::BinaryDecoder::Bytea.new name: 'BYTEA', oid: 17, format: 1 } let!(:pass_through_type) do type = Class.new(PG::SimpleDecoder) do def decode(*v) v end end.new type.oid = 123456 type.format = 1 type.name = 'pass_through' type end it "should retrieve it's conversions" do cm = PG::TypeMapByColumn.new( [textdec_int, textenc_string, textdec_float, pass_through_type, nil] ) expect( cm.coders ).to eq( [ textdec_int, textenc_string, textdec_float, pass_through_type, nil ] ) end it "should respond to inspect" do cm = PG::TypeMapByColumn.new( [textdec_int, textenc_string, textdec_float, pass_through_type, PG::TextEncoder::Float.new, nil] ) expect( cm.inspect ).to eq( "#" ) end it "should retrieve it's oids" do cm = PG::TypeMapByColumn.new( [textdec_int, textdec_string, textdec_float, pass_through_type, nil] ) expect( cm.oids ).to eq( [23, 25, 700, 123456, nil] ) end it "should gracefully handle not initialized state" do # PG::TypeMapByColumn is not initialized in allocate function, like other # type maps, but in #initialize. So it might be not called by derived classes. not_init = Class.new(PG::TypeMapByColumn) do def initialize # no super call end end.new expect{ @conn.exec_params( "SELECT $1", [ 0 ], 0, not_init ) }.to raise_error(NotImplementedError) res = @conn.exec( "SELECT 1" ) expect{ res.type_map = not_init }.to raise_error(NotImplementedError) @conn.copy_data("COPY (SELECT 1) TO STDOUT") do decoder = PG::TextDecoder::CopyRow.new(type_map: not_init) expect{ @conn.get_copy_data(false, decoder) }.to raise_error(NotImplementedError) @conn.get_copy_data end end # # Encoding Examples # it "should encode integer params" do col_map = PG::TypeMapByColumn.new( [textenc_int]*3 ) res = @conn.exec_params( "SELECT $1, $2, $3", [ 0, nil, "-999" ], 0, col_map ) expect( res.values ).to eq( [ [ "0", nil, "-999" ], ] ) end it "should encode bytea params" do data = "'\u001F\\" col_map = PG::TypeMapByColumn.new( [binaryenc_bytea]*2 ) res = @conn.exec_params( "SELECT $1, $2", [ data, nil ], 0, col_map ) res.type_map = PG::TypeMapByColumn.new( [textdec_bytea]*2 ) expect( res.values ).to eq( [ [ data, nil ], ] ) end it "should allow hash form parameters for default encoder" do col_map = PG::TypeMapByColumn.new( [nil, nil] ) hash_param_bin = { value: ["00ff"].pack("H*"), type: 17, format: 1 } hash_param_nil = { value: nil, type: 17, format: 1 } res = @conn.exec_params( "SELECT $1, $2", [ hash_param_bin, hash_param_nil ], 0, col_map ) expect( res.values ).to eq( [["\\x00ff", nil]] ) expect( result_typenames(res) ).to eq( ['bytea', 'bytea'] ) end it "should convert hash form parameters to string when using string encoders" do col_map = PG::TypeMapByColumn.new( [textenc_string, textenc_string] ) hash_param_bin = { value: ["00ff"].pack("H*"), type: 17, format: 1 } hash_param_nil = { value: nil, type: 17, format: 1 } res = @conn.exec_params( "SELECT $1::text, $2::text", [ hash_param_bin, hash_param_nil ], 0, col_map ) expect( res.values ).to eq( [["{:value=>\"\\x00\\xFF\", :type=>17, :format=>1}", "{:value=>nil, :type=>17, :format=>1}"]] ) end it "shouldn't allow param mappings with different number of fields" do expect{ @conn.exec_params( "SELECT $1", [ 123 ], 0, PG::TypeMapByColumn.new([]) ) }.to raise_error(ArgumentError, /mapped columns/) end it "should verify the default type map for query params as well" do tm1 = PG::TypeMapByColumn.new([]) expect{ @conn.exec_params( "SELECT $1", [ 123 ], 0, PG::TypeMapByColumn.new([nil]).with_default_type_map(tm1) ) }.to raise_error(ArgumentError, /mapped columns/) end it "forwards query param conversions to the #default_type_map" do tm1 = PG::TypeMapByClass.new tm1[Integer] = PG::TextEncoder::Integer.new name: 'INT2', oid: 21 tm2 = PG::TypeMapByColumn.new( [textenc_int, nil, nil] ).with_default_type_map( tm1 ) res = @conn.exec_params( "SELECT $1, $2, $3::TEXT", [1, 2, :abc], 0, tm2 ) expect( res.ftype(0) ).to eq( 23 ) # tm2 expect( res.ftype(1) ).to eq( 21 ) # tm1 expect( res.getvalue(0,2) ).to eq( "abc" ) # TypeMapAllStrings end # # Decoding Examples # class Exception_in_decode < PG::SimpleDecoder def decode(res, tuple, field) raise "no type decoder defined for tuple #{tuple} field #{field}" end end it "should raise an error from decode method of type converter" do res = @conn.exec( "SELECT now()" ) types = Array.new( res.nfields, Exception_in_decode.new ) res.type_map = PG::TypeMapByColumn.new( types ) expect{ res.values }.to raise_error(/no type decoder defined/) end it "should raise an error for invalid params" do expect{ PG::TypeMapByColumn.new( :WrongType ) }.to raise_error(TypeError, /wrong argument type/) expect{ PG::TypeMapByColumn.new( [123] ) }.to raise_error(ArgumentError, /invalid/) end it "shouldn't allow result mappings with different number of fields" do res = @conn.exec( "SELECT 1" ) expect{ res.type_map = PG::TypeMapByColumn.new([]) }.to raise_error(ArgumentError, /mapped columns/) end it "should verify the default type map for result values as well" do res = @conn.exec( "SELECT 1" ) tm1 = PG::TypeMapByColumn.new([]) expect{ res.type_map = PG::TypeMapByColumn.new([nil]).with_default_type_map(tm1) }.to raise_error(ArgumentError, /mapped columns/) end it "forwards result value conversions to a TypeMapByOid as #default_type_map" do # One run with implicit built TypeMapByColumn and another with online lookup [0, 10].each do |max_rows| tm1 = PG::TypeMapByOid.new tm1.add_coder PG::TextDecoder::Integer.new name: 'INT2', oid: 21 tm1.max_rows_for_online_lookup = max_rows tm2 = PG::TypeMapByColumn.new( [textdec_int, nil, nil] ).with_default_type_map( tm1 ) res = @conn.exec( "SELECT '1'::INT4, '2'::INT2, '3'::INT8" ).map_types!( tm2 ) expect( res.getvalue(0,0) ).to eq( 1 ) # tm2 expect( res.getvalue(0,1) ).to eq( 2 ) # tm1 expect( res.getvalue(0,2) ).to eq( "3" ) # TypeMapAllStrings end end it "forwards get_copy_data conversions to another TypeMapByColumn as #default_type_map" do tm1 = PG::TypeMapByColumn.new( [textdec_int, nil, nil] ) tm2 = PG::TypeMapByColumn.new( [nil, textdec_int, nil] ).with_default_type_map( tm1 ) decoder = PG::TextDecoder::CopyRow.new(type_map: tm2) @conn.copy_data("COPY (SELECT 1, 2, 3) TO STDOUT", decoder) do expect( @conn.get_copy_data ).to eq( [1, 2, '3'] ) @conn.get_copy_data end end it "will deny copy queries with different column count" do [[2, 2], [2, 3], [3, 2]].each do |cols1, cols2| tm1 = PG::TypeMapByColumn.new( [textdec_int, nil, nil][0, cols1] ) tm2 = PG::TypeMapByColumn.new( [nil, textdec_int, nil][0, cols2] ).with_default_type_map( tm1 ) decoder = PG::TextDecoder::CopyRow.new(type_map: tm2) @conn.copy_data("COPY (SELECT 1, 2, 3) TO STDOUT", decoder) do expect{ @conn.get_copy_data }.to raise_error(ArgumentError, /number of copy fields/) @conn.get_copy_data end end end # # Decoding Examples text format # it "should allow mixed type conversions" do res = @conn.exec( "SELECT 1, 'a', 2.0::FLOAT, '2013-06-30'::DATE, 3" ) res.type_map = PG::TypeMapByColumn.new( [textdec_int, textdec_string, textdec_float, pass_through_type, nil] ) expect( res.values ).to eq( [[1, 'a', 2.0, ['2013-06-30', 0, 3], '3' ]] ) end end pg-1.2.3/spec/pg/type_map_by_mri_type_spec.rb0000644000004100000410000000731013704151215021272 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative '../helpers' require 'pg' describe PG::TypeMapByMriType do let!(:textenc_int){ PG::TextEncoder::Integer.new name: 'INT4', oid: 23 } let!(:textenc_float){ PG::TextEncoder::Float.new name: 'FLOAT8', oid: 701 } let!(:textenc_string){ PG::TextEncoder::String.new name: 'TEXT', oid: 25 } let!(:binaryenc_int){ PG::BinaryEncoder::Int8.new name: 'INT8', oid: 20, format: 1 } let!(:pass_through_type) do type = Class.new(PG::SimpleEncoder) do def encode(*v) v.inspect end end.new type.oid = 25 type.format = 0 type.name = 'pass_through' type end let!(:tm) do tm = PG::TypeMapByMriType.new tm['T_FIXNUM'] = binaryenc_int tm['T_FLOAT'] = textenc_float tm['T_SYMBOL'] = pass_through_type tm end let!(:derived_tm) do tm = Class.new(PG::TypeMapByMriType) do def array_type_map_for(value) PG::TextEncoder::Array.new name: '_INT4', oid: 1007, elements_type: PG::TextEncoder::Integer.new end end.new tm['T_FIXNUM'] = proc{|value| textenc_int } tm['T_REGEXP'] = proc{|value| :invalid } tm['T_ARRAY'] = :array_type_map_for tm end it "should retrieve all conversions" do expect( tm.coders ).to eq( { "T_FIXNUM" => binaryenc_int, "T_FLOAT" => textenc_float, "T_SYMBOL" => pass_through_type, "T_HASH" => nil, "T_ARRAY" => nil, "T_BIGNUM" => nil, "T_CLASS" => nil, "T_COMPLEX" => nil, "T_DATA" => nil, "T_FALSE" => nil, "T_FILE" => nil, "T_MODULE" => nil, "T_OBJECT" => nil, "T_RATIONAL" => nil, "T_REGEXP" => nil, "T_STRING" => nil, "T_STRUCT" => nil, "T_TRUE" => nil, } ) end it "should retrieve particular conversions" do expect( tm['T_FIXNUM'] ).to eq(binaryenc_int) expect( tm['T_FLOAT'] ).to eq(textenc_float) expect( tm['T_BIGNUM'] ).to be_nil expect( derived_tm['T_REGEXP'] ).to be_kind_of(Proc) expect( derived_tm['T_ARRAY'] ).to eq(:array_type_map_for) end it "should allow deletion of coders" do tm['T_FIXNUM'] = nil expect( tm['T_FIXNUM'] ).to be_nil end it "should check MRI type key" do expect{ tm['NO_TYPE'] }.to raise_error(ArgumentError) expect{ tm[123] }.to raise_error(TypeError) expect{ tm['NO_TYPE'] = textenc_float }.to raise_error(ArgumentError) expect{ tm[123] = textenc_float }.to raise_error(TypeError) end it "forwards query param conversions to the #default_type_map" do tm1 = PG::TypeMapByColumn.new( [textenc_int, nil, nil] ) tm2 = PG::TypeMapByMriType.new tm2['T_FIXNUM'] = PG::TextEncoder::Integer.new name: 'INT2', oid: 21 tm2.default_type_map = tm1 res = @conn.exec_params( "SELECT $1, $2, $3::TEXT", ['1', 2, 3], 0, tm2 ) expect( res.ftype(0) ).to eq( 23 ) # tm1 expect( res.ftype(1) ).to eq( 21 ) # tm2 expect( res.getvalue(0,2) ).to eq( "3" ) # TypeMapAllStrings end # # Decoding Examples # it "should raise an error when used for results" do res = @conn.exec_params( "SELECT 1", [], 1 ) expect{ res.type_map = tm }.to raise_error(NotImplementedError, /not suitable to map result values/) end # # Encoding Examples # it "should allow mixed type conversions" do res = @conn.exec_params( "SELECT $1, $2, $3", [5, 1.23, :TestSymbol], 0, tm ) expect( res.values ).to eq([['5', '1.23', "[:TestSymbol, #{@conn.internal_encoding.inspect}]"]]) expect( res.ftype(0) ).to eq(20) end it "should allow mixed type conversions with derived type map" do res = @conn.exec_params( "SELECT $1, $2", [6, [7]], 0, derived_tm ) expect( res.values ).to eq([['6', '{7}']]) expect( res.ftype(0) ).to eq(23) expect( res.ftype(1) ).to eq(1007) end it "should raise TypeError with derived type map" do expect{ @conn.exec_params( "SELECT $1", [//], 0, derived_tm ) }.to raise_error(TypeError, /argument 1/) end end pg-1.2.3/spec/pg/connection_sync_spec.rb0000644000004100000410000000132313704151215020243 0ustar www-datawww-data# -*- rspec -*- #encoding: utf-8 require_relative '../helpers' context "running with sync_* methods" do before :each do PG::Connection.async_api = false end after :each do PG::Connection.async_api = true end fname = File.expand_path("../connection_spec.rb", __FILE__) eval File.read(fname, encoding: __ENCODING__), binding, fname it "enables/disables async/sync methods by #async_api" do [true, false].each do |async| PG::Connection.async_api = async start = Time.now t = Thread.new do @conn.exec( 'select pg_sleep(1)' ) end sleep 0.1 t.kill t.join dt = Time.now - start if async expect( dt ).to be < 1.0 else expect( dt ).to be >= 1.0 end end end end pg-1.2.3/spec/pg/connection_spec.rb0000644000004100000410000017324613704151215017225 0ustar www-datawww-data# -*- rspec -*- #encoding: utf-8 require_relative '../helpers' require 'timeout' require 'socket' require 'pg' describe PG::Connection do it "can create a connection option string from a Hash of options" do optstring = described_class.parse_connect_args( :host => 'pgsql.example.com', :dbname => 'db01', 'sslmode' => 'require' ) expect( optstring ).to be_a( String ) expect( optstring ).to match( /(^|\s)host='pgsql.example.com'/ ) expect( optstring ).to match( /(^|\s)dbname='db01'/ ) expect( optstring ).to match( /(^|\s)sslmode='require'/ ) end it "can create a connection option string from positional parameters" do optstring = described_class.parse_connect_args( 'pgsql.example.com', nil, '-c geqo=off', nil, 'sales' ) expect( optstring ).to be_a( String ) expect( optstring ).to match( /(^|\s)host='pgsql.example.com'/ ) expect( optstring ).to match( /(^|\s)dbname='sales'/ ) expect( optstring ).to match( /(^|\s)options='-c geqo=off'/ ) expect( optstring ).to_not match( /port=/ ) expect( optstring ).to_not match( /tty=/ ) end it "can create a connection option string from a mix of positional and hash parameters" do optstring = described_class.parse_connect_args( 'pgsql.example.com', :dbname => 'licensing', :user => 'jrandom' ) expect( optstring ).to be_a( String ) expect( optstring ).to match( /(^|\s)host='pgsql.example.com'/ ) expect( optstring ).to match( /(^|\s)dbname='licensing'/ ) 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 it "connects successfully with connection string" do conninfo_with_colon_in_password = "host=localhost user=a port=555 dbname=test password=a:a" string = described_class.parse_connect_args( conninfo_with_colon_in_password ) expect( string ).to be_a( String ) expect( string ).to match( %r{(^|\s)user=a} ) expect( string ).to match( %r{(^|\s)password=a:a} ) expect( string ).to match( %r{(^|\s)host=localhost} ) expect( string ).to match( %r{(^|\s)port=555} ) expect( string ).to match( %r{(^|\s)dbname=test} ) end it "connects successfully with connection string" do tmpconn = described_class.connect( @conninfo ) expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) tmpconn.finish end it "connects using 7 arguments converted to strings" do tmpconn = described_class.connect( 'localhost', @port, nil, nil, :test, nil, nil ) expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) tmpconn.finish end it "connects using a hash of connection parameters" do tmpconn = described_class.connect( :host => 'localhost', :port => @port, :dbname => :test) expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) tmpconn.finish end it "connects using a hash of optional connection parameters" do tmpconn = described_class.connect( :host => 'localhost', :port => @port, :dbname => :test, :keepalives => 1) expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) tmpconn.finish end it "raises an exception when connecting with an invalid number of arguments" do expect { 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" do tmpconn = described_class.connect_start( @conninfo ) expect( tmpconn ).to be_a( described_class ) wait_for_polling_ok(tmpconn) expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) tmpconn.finish end it "can connect asynchronously for the duration of a block" do conn = nil described_class.connect_start(@conninfo) do |tmpconn| expect( tmpconn ).to be_a( described_class ) conn = tmpconn wait_for_polling_ok(tmpconn) expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) end expect( conn ).to be_finished() end context "with async established connection" do before :each do @conn2 = described_class.connect_start( @conninfo ) wait_for_polling_ok(@conn2) expect( @conn2 ).to still_be_usable end after :each do expect( @conn2 ).to still_be_usable @conn2.close end it "conn.send_query and IO.select work" do @conn2.send_query("SELECT 1") res = wait_for_query_result(@conn2) expect( res.values ).to eq([["1"]]) end it "conn.send_query and conn.block work" do @conn2.send_query("SELECT 2") @conn2.block res = @conn2.get_last_result expect( res.values ).to eq([["2"]]) end it "conn.async_query works" do res = @conn2.async_query("SELECT 3") expect( res.values ).to eq([["3"]]) expect( @conn2 ).to still_be_usable res = @conn2.query("SELECT 4") end it "can use conn.reset_start to restart the connection" do ios = IO.pipe conn = described_class.connect_start( @conninfo ) wait_for_polling_ok(conn) # Close the two pipe file descriptors, so that the file descriptor of # newly established connection is probably distinct from the previous one. ios.each(&:close) conn.reset_start wait_for_polling_ok(conn, :reset_poll) # The new connection should work even when the file descriptor has changed. conn.send_query("SELECT 1") res = wait_for_query_result(conn) expect( res.values ).to eq([["1"]]) conn.close end it "should properly close a socket IO when GC'ed" do # This results in # Errno::ENOTSOCK: An operation was attempted on something that is not a socket. # on Windows when rb_w32_unwrap_io_handle() isn't called in pgconn_gc_free(). 5.times do conn = described_class.connect( @conninfo ) conn.socket_io.close end GC.start IO.pipe.each(&:close) end end it "raises proper error when sending fails" do conn = described_class.connect_start( '127.0.0.1', 54320, "", "", "me", "xxxx", "somedb" ) expect{ conn.exec 'SELECT 1' }.to raise_error(PG::UnableToSend, /no connection/) end it "doesn't leave stale server connections after finish" do described_class.connect(@conninfo).finish sleep 0.5 res = @conn.exec(%[SELECT COUNT(*) AS n FROM pg_stat_activity WHERE usename IS NOT NULL AND application_name != '']) # there's still the global @conn, but should be no more expect( res[0]['n'] ).to eq( '1' ) end it "can retrieve it's connection parameters for the established connection" do expect( @conn.db ).to eq( "test" ) expect( @conn.user ).to be_a_kind_of( String ) expect( @conn.pass ).to eq( "" ) expect( @conn.port ).to eq( @port ) expect( @conn.tty ).to eq( "" ) expect( @conn.options ).to eq( "" ) end it "can retrieve it's connection parameters for the established connection", skip: RUBY_PLATFORM=~/x64-mingw/ ? "host segfaults on Windows-x64" : false do expect( @conn.host ).to eq( "localhost" ) end it "can set error verbosity" do old = @conn.set_error_verbosity( PG::PQERRORS_TERSE ) new = @conn.set_error_verbosity( old ) expect( new ).to eq( PG::PQERRORS_TERSE ) end it "can set error context visibility", :postgresql_96 do old = @conn.set_error_context_visibility( PG::PQSHOW_CONTEXT_NEVER ) new = @conn.set_error_context_visibility( old ) expect( new ).to eq( PG::PQSHOW_CONTEXT_NEVER ) end let(:expected_trace_output) do %{ To backend> Msg Q To backend> "SELECT 1 AS one" To backend> Msg complete, length 21 From backend> T From backend (#4)> 28 From backend (#2)> 1 From backend> "one" From backend (#4)> 0 From backend (#2)> 0 From backend (#4)> 23 From backend (#2)> 4 From backend (#4)> -1 From backend (#2)> 0 From backend> D From backend (#4)> 11 From backend (#2)> 1 From backend (#4)> 1 From backend (1)> 1 From backend> C From backend (#4)> 13 From backend> "SELECT 1" From backend> Z From backend (#4)> 5 From backend> Z From backend (#4)> 5 From backend> T }.gsub( /^\t{2}/, '' ).lstrip end it "trace and untrace client-server communication", :unix do # be careful to explicitly close files so that the # directory can be removed and we don't have to wait for # the GC to run. trace_file = TEST_DIRECTORY + "test_trace.out" trace_io = trace_file.open( 'w', 0600 ) @conn.trace( trace_io ) trace_io.close @conn.exec("SELECT 1 AS one") @conn.untrace @conn.exec("SELECT 2 AS two") trace_data = trace_file.read # For async_exec the output will be different: # From backend> Z # From backend (#4)> 5 # +From backend> Z # +From backend (#4)> 5 # From backend> T trace_data.sub!( /(From backend> Z\nFrom backend \(#4\)> 5\n){3}/m, '\\1\\1' ) expect( trace_data ).to eq( expected_trace_output ) end it "allows a query to be cancelled" do error = false @conn.send_query("SELECT pg_sleep(1000)") @conn.cancel tmpres = @conn.get_result if(tmpres.result_status != PG::PGRES_TUPLES_OK) error = true end expect( error ).to eq( true ) end it "can stop a thread that runs a blocking query with async_exec" do start = Time.now t = Thread.new do @conn.async_exec( 'select pg_sleep(10)' ) end sleep 0.1 t.kill t.join expect( (Time.now - start) ).to be < 10 end it "should work together with signal handlers", :unix do signal_received = false trap 'USR2' do signal_received = true end Thread.new do sleep 0.1 Process.kill("USR2", Process.pid) end @conn.exec("select pg_sleep(0.3)") expect( signal_received ).to be_truthy end it "automatically rolls back a transaction started with Connection#transaction if an exception " + "is raised" do # abort the per-example transaction so we can test our own @conn.exec( 'ROLLBACK' ) res = nil @conn.exec( "CREATE TABLE pie ( flavor TEXT )" ) begin expect { res = @conn.transaction do @conn.exec( "INSERT INTO pie VALUES ('rhubarb'), ('cherry'), ('schizophrenia')" ) raise "Oh noes! All pie is gone!" end }.to raise_exception( RuntimeError, /all pie is gone/i ) res = @conn.exec( "SELECT * FROM pie" ) expect( res.ntuples ).to eq( 0 ) ensure @conn.exec( "DROP TABLE pie" ) end end it "returns the block result from Connection#transaction" do # abort the per-example transaction so we can test our own @conn.exec( 'ROLLBACK' ) res = @conn.transaction do "transaction result" end expect( res ).to eq( "transaction result" ) end it "not read past the end of a large object" do @conn.transaction do oid = @conn.lo_create( 0 ) fd = @conn.lo_open( oid, PG::INV_READ|PG::INV_WRITE ) @conn.lo_write( fd, "foobar" ) expect( @conn.lo_read( fd, 10 ) ).to be_nil() @conn.lo_lseek( fd, 0, PG::SEEK_SET ) expect( @conn.lo_read( fd, 10 ) ).to eq( 'foobar' ) end end it "supports explicitly calling #exec_params" do @conn.exec( "CREATE TABLE students ( name TEXT, age INTEGER )" ) @conn.exec_params( "INSERT INTO students VALUES( $1, $2 )", ['Wally', 8] ) @conn.exec_params( "INSERT INTO students VALUES( $1, $2 )", ['Sally', 6] ) @conn.exec_params( "INSERT INTO students VALUES( $1, $2 )", ['Dorothy', 4] ) res = @conn.exec_params( "SELECT name FROM students WHERE age >= $1", [6] ) expect( res.values ).to eq( [ ['Wally'], ['Sally'] ] ) end it "supports hash form parameters for #exec_params" do hash_param_bin = { value: ["00ff"].pack("H*"), type: 17, format: 1 } hash_param_nil = { value: nil, type: 17, format: 1 } res = @conn.exec_params( "SELECT $1, $2", [ hash_param_bin, hash_param_nil ] ) expect( res.values ).to eq( [["\\x00ff", nil]] ) expect( result_typenames(res) ).to eq( ['bytea', 'bytea'] ) end it "should work with arbitrary number of params" do begin 3.step( 12, 0.2 ) do |exp| num_params = (2 ** exp).to_i sql = num_params.times.map{|n| "$#{n+1}::INT" }.join(",") params = num_params.times.to_a res = @conn.exec_params( "SELECT #{sql}", params ) expect( res.nfields ).to eq( num_params ) expect( res.values ).to eq( [num_params.times.map(&:to_s)] ) end rescue PG::ProgramLimitExceeded # Stop silently if the server complains about too many params end end it "can wait for NOTIFY events" do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN woo' ) t = Thread.new do begin conn = described_class.connect( @conninfo ) sleep 1 conn.async_exec( 'NOTIFY woo' ) ensure conn.finish end end expect( @conn.wait_for_notify( 10 ) ).to eq( 'woo' ) @conn.exec( 'UNLISTEN woo' ) t.join end it "calls a block for NOTIFY events if one is given" do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN woo' ) t = Thread.new do begin conn = described_class.connect( @conninfo ) sleep 1 conn.async_exec( 'NOTIFY woo' ) ensure conn.finish end end eventpid = event = nil @conn.wait_for_notify( 10 ) {|*args| event, eventpid = args } expect( event ).to eq( 'woo' ) expect( eventpid ).to be_an( Integer ) @conn.exec( 'UNLISTEN woo' ) t.join end it "doesn't collapse sequential notifications" do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN woo' ) @conn.exec( 'LISTEN war' ) @conn.exec( 'LISTEN woz' ) begin conn = described_class.connect( @conninfo ) conn.exec( 'NOTIFY woo' ) conn.exec( 'NOTIFY war' ) conn.exec( 'NOTIFY woz' ) ensure conn.finish end channels = [] 3.times do channels << @conn.wait_for_notify( 2 ) end expect( channels.size ).to eq( 3 ) expect( channels ).to include( 'woo', 'war', 'woz' ) @conn.exec( 'UNLISTEN woz' ) @conn.exec( 'UNLISTEN war' ) @conn.exec( 'UNLISTEN woo' ) end it "returns notifications which are already in the queue before wait_for_notify is called " + "without waiting for the socket to become readable" do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN woo' ) begin conn = described_class.connect( @conninfo ) conn.exec( 'NOTIFY woo' ) ensure conn.finish end # Cause the notification to buffer, but not be read yet @conn.exec( 'SELECT 1' ) expect( @conn.wait_for_notify( 10 ) ).to eq( 'woo' ) @conn.exec( 'UNLISTEN woo' ) end it "can receive notices while waiting for NOTIFY without exceeding the timeout" do notices = [] @conn.set_notice_processor do |msg| notices << [msg, Time.now] end st = Time.now @conn.send_query "SELECT pg_sleep(0.5); do $$ BEGIN RAISE NOTICE 'woohoo'; END; $$ LANGUAGE plpgsql;" expect( @conn.wait_for_notify( 1 ) ).to be_nil expect( notices.first ).to_not be_nil et = Time.now expect( (et - notices.first[1]) ).to be >= 0.3 expect( (et - st) ).to be >= 0.9 expect( (et - st) ).to be < 1.4 end it "yields the result if block is given to exec" do rval = @conn.exec( "select 1234::int as a union select 5678::int as a" ) do |result| values = [] expect( result ).to be_kind_of( PG::Result ) expect( result.ntuples ).to eq( 2 ) result.each do |tuple| values << tuple['a'] end values end expect( rval.size ).to eq( 2 ) expect( rval ).to include( '5678', '1234' ) end it "can process #copy_data output queries" do rows = [] res2 = @conn.copy_data( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT" ) do |res| expect( res.result_status ).to eq( PG::PGRES_COPY_OUT ) expect( res.nfields ).to eq( 1 ) while row=@conn.get_copy_data rows << row end end expect( rows ).to eq( ["1\n", "2\n"] ) expect( res2.result_status ).to eq( PG::PGRES_COMMAND_OK ) expect( @conn ).to still_be_usable end it "can handle incomplete #copy_data output queries" do expect { @conn.copy_data( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT" ) do |res| @conn.get_copy_data end }.to raise_error(PG::NotAllCopyDataRetrieved, /Not all/) expect( @conn ).to still_be_usable end it "can handle client errors in #copy_data for output" do expect { @conn.copy_data( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT" ) do raise "boom" end }.to raise_error(RuntimeError, "boom") expect( @conn ).to still_be_usable end it "can handle server errors in #copy_data for output" do @conn.exec "ROLLBACK" @conn.transaction do @conn.exec( "CREATE FUNCTION errfunc() RETURNS int AS $$ BEGIN RAISE 'test-error'; END; $$ LANGUAGE plpgsql;" ) expect { @conn.copy_data( "COPY (SELECT errfunc()) TO STDOUT" ) do |res| while @conn.get_copy_data end end }.to raise_error(PG::Error, /test-error/) end expect( @conn ).to still_be_usable end it "can process #copy_data input queries" do @conn.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" ) res2 = @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res| expect( res.result_status ).to eq( PG::PGRES_COPY_IN ) expect( res.nfields ).to eq( 1 ) @conn.put_copy_data "1\n" @conn.put_copy_data "2\n" end expect( res2.result_status ).to eq( PG::PGRES_COMMAND_OK ) expect( @conn ).to still_be_usable res = @conn.exec( "SELECT * FROM copytable ORDER BY col1" ) expect( res.values ).to eq( [["1"], ["2"]] ) end it "can handle client errors in #copy_data for input" do @conn.exec "ROLLBACK" @conn.transaction do @conn.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" ) expect { @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res| raise "boom" end }.to raise_error(RuntimeError, "boom") end expect( @conn ).to still_be_usable end it "can handle server errors in #copy_data for input" do @conn.exec "ROLLBACK" @conn.transaction do @conn.exec( "CREATE TEMP TABLE copytable (col1 INT)" ) expect { @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res| @conn.put_copy_data "xyz\n" end }.to raise_error(PG::Error, /invalid input syntax for .*integer/) end expect( @conn ).to still_be_usable end it "gracefully handle SQL statements while in #copy_data for input" do @conn.exec "ROLLBACK" @conn.transaction do @conn.exec( "CREATE TEMP TABLE copytable (col1 INT)" ) expect { @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res| @conn.exec "SELECT 1" end }.to raise_error(PG::Error, /no COPY in progress/) end expect( @conn ).to still_be_usable end it "gracefully handle SQL statements while in #copy_data for output" do @conn.exec "ROLLBACK" @conn.transaction do expect { @conn.copy_data( "COPY (VALUES(1), (2)) TO STDOUT" ) do |res| @conn.exec "SELECT 3" end }.to raise_error(PG::Error, /no COPY in progress/) end expect( @conn ).to still_be_usable end it "should raise an error for non copy statements in #copy_data" do expect { @conn.copy_data( "SELECT 1" ){} }.to raise_error(ArgumentError, /no COPY/) expect( @conn ).to still_be_usable end it "correctly finishes COPY queries passed to #async_exec" do @conn.async_exec( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT" ) results = [] begin data = @conn.get_copy_data( true ) if false == data @conn.block( 2.0 ) data = @conn.get_copy_data( true ) end results << data if data end until data.nil? expect( results.size ).to eq( 2 ) expect( results ).to include( "1\n", "2\n" ) end it "described_class#block shouldn't block a second thread" do start = Time.now t = Thread.new do @conn.send_query( "select pg_sleep(3)" ) @conn.block end sleep 0.5 expect( t ).to be_alive() @conn.cancel t.join expect( (Time.now - start) ).to be < 3 end it "described_class#block should allow a timeout" do @conn.send_query( "select pg_sleep(1)" ) start = Time.now @conn.block( 0.3 ) finish = Time.now expect( (finish - start) ).to be_within( 0.2 ).of( 0.3 ) end it "can return the default connection options" do expect( described_class.conndefaults ).to be_a( Array ) expect( described_class.conndefaults ).to all( be_a(Hash) ) expect( described_class.conndefaults[0] ).to include( :keyword, :label, :dispchar, :dispsize ) expect( @conn.conndefaults ).to eq( described_class.conndefaults ) end it "can return the default connection options as a Hash" do expect( described_class.conndefaults_hash ).to be_a( Hash ) expect( described_class.conndefaults_hash ).to include( :user, :password, :dbname, :host, :port ) expect( ['5432', '54321', @port.to_s] ).to include( described_class.conndefaults_hash[:port] ) expect( @conn.conndefaults_hash ).to eq( described_class.conndefaults_hash ) end it "can return the connection's connection options", :postgresql_93 do expect( @conn.conninfo ).to be_a( Array ) expect( @conn.conninfo ).to all( be_a(Hash) ) expect( @conn.conninfo[0] ).to include( :keyword, :label, :dispchar, :dispsize ) end it "can return the connection's connection options as a Hash", :postgresql_93 do expect( @conn.conninfo_hash ).to be_a( Hash ) expect( @conn.conninfo_hash ).to include( :user, :password, :connect_timeout, :dbname, :host ) expect( @conn.conninfo_hash[:dbname] ).to eq( 'test' ) end describe "connection information related to SSL" do it "can retrieve connection's ssl state", :postgresql_95 do expect( @conn.ssl_in_use? ).to be false end it "can retrieve connection's ssl attribute_names", :postgresql_95 do expect( @conn.ssl_attribute_names ).to be_a(Array) end it "can retrieve a single ssl connection attribute", :postgresql_95 do expect( @conn.ssl_attribute('dbname') ).to eq( nil ) end it "can retrieve all connection's ssl attributes", :postgresql_95 do expect( @conn.ssl_attributes ).to be_a_kind_of( Hash ) end end it "honors the connect_timeout connection parameter", :postgresql_93 do conn = PG.connect( port: @port, dbname: 'test', connect_timeout: 11 ) begin expect( conn.conninfo_hash[:connect_timeout] ).to eq( "11" ) ensure conn.finish end end describe "deprecated password encryption method" do it "can encrypt password for a given user" do expect( described_class.encrypt_password("postgres", "postgres") ).to match( /\S+/ ) end it "raises an appropriate error if either of the required arguments is not valid" do expect { described_class.encrypt_password( nil, nil ) }.to raise_error( TypeError ) expect { described_class.encrypt_password( "postgres", nil ) }.to raise_error( TypeError ) expect { described_class.encrypt_password( nil, "postgres" ) }.to raise_error( TypeError ) end end describe "password encryption method", :postgresql_10 do it "can encrypt without algorithm" do expect( @conn.encrypt_password("postgres", "postgres") ).to match( /\S+/ ) expect( @conn.encrypt_password("postgres", "postgres", nil) ).to match( /\S+/ ) end it "can encrypt with algorithm" do expect( @conn.encrypt_password("postgres", "postgres", "md5") ).to match( /md5\S+/i ) expect( @conn.encrypt_password("postgres", "postgres", "scram-sha-256") ).to match( /SCRAM-SHA-256\S+/i ) end it "raises an appropriate error if either of the required arguments is not valid" do expect { @conn.encrypt_password( nil, nil ) }.to raise_error( TypeError ) expect { @conn.encrypt_password( "postgres", nil ) }.to raise_error( TypeError ) expect { @conn.encrypt_password( nil, "postgres" ) }.to raise_error( TypeError ) expect { @conn.encrypt_password( "postgres", "postgres", :invalid ) }.to raise_error( TypeError ) expect { @conn.encrypt_password( "postgres", "postgres", "invalid" ) }.to raise_error( PG::Error, /unrecognized/ ) end end it "allows fetching a column of values from a result by column number" do res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' ) expect( res.column_values( 0 ) ).to eq( %w[1 2 3] ) expect( res.column_values( 1 ) ).to eq( %w[2 3 4] ) end it "allows fetching a column of values from a result by field name" do res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' ) expect( res.field_values( 'column1' ) ).to eq( %w[1 2 3] ) expect( res.field_values( 'column2' ) ).to eq( %w[2 3 4] ) end it "raises an error if selecting an invalid column index" do res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' ) expect { res.column_values( 20 ) }.to raise_error( IndexError ) end it "raises an error if selecting an invalid field name" do res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' ) expect { res.field_values( 'hUUuurrg' ) }.to raise_error( IndexError ) end it "raises an error if column index is not a number" do res = @conn.exec( 'VALUES (1,2),(2,3),(3,4)' ) expect { res.column_values( 'hUUuurrg' ) }.to raise_error( TypeError ) end it "handles server close while asynchronous connect" do serv = TCPServer.new( '127.0.0.1', 54320 ) conn = described_class.connect_start( '127.0.0.1', 54320, "", "", "me", "xxxx", "somedb" ) expect( [PG::PGRES_POLLING_WRITING, PG::CONNECTION_OK] ).to include conn.connect_poll select( nil, [conn.socket_io], nil, 0.2 ) serv.close if conn.connect_poll == PG::PGRES_POLLING_READING select( [conn.socket_io], nil, nil, 0.2 ) end expect( conn.connect_poll ).to eq( PG::PGRES_POLLING_FAILED ) end it "discards previous results at #discard_results" do @conn.send_query( "select 1" ) @conn.discard_results @conn.send_query( "select 41 as one" ) res = @conn.get_last_result expect( res.to_a ).to eq( [{ 'one' => '41' }] ) end it "discards previous results (if any) before waiting on #exec" do @conn.send_query( "select 1" ) res = @conn.exec( "select 42 as one" ) expect( res.to_a ).to eq( [{ 'one' => '42' }] ) end it "discards previous errors before waiting on #exec", :without_transaction do @conn.send_query( "ERROR" ) res = @conn.exec( "select 43 as one" ) expect( res.to_a ).to eq( [{ 'one' => '43' }] ) end it "calls the block if one is provided to #exec" do result = nil @conn.exec( "select 47 as one" ) do |pg_res| result = pg_res[0] end expect( result ).to eq( { 'one' => '47' } ) end it "raises a rescue-able error if #finish is called twice", :without_transaction do conn = PG.connect( @conninfo ) conn.finish expect { conn.finish }.to raise_error( PG::ConnectionBad, /connection is closed/i ) end it "can use conn.reset to restart the connection" do ios = IO.pipe conn = PG.connect( @conninfo ) # Close the two pipe file descriptors, so that the file descriptor of # newly established connection is probably distinct from the previous one. ios.each(&:close) conn.reset # The new connection should work even when the file descriptor has changed. expect( conn.exec("SELECT 1").values ).to eq([["1"]]) conn.close end it "closes the IO fetched from #socket_io when the connection is closed", :without_transaction do conn = PG.connect( @conninfo ) io = conn.socket_io conn.finish expect( io ).to be_closed() expect { conn.socket_io }.to raise_error( PG::ConnectionBad, /connection is closed/i ) end it "closes the IO fetched from #socket_io when the connection is reset", :without_transaction do conn = PG.connect( @conninfo ) io = conn.socket_io conn.reset expect( io ).to be_closed() expect( conn.socket_io ).to_not equal( io ) conn.finish end it "block should raise ConnectionBad for a closed connection" do serv = TCPServer.new( '127.0.0.1', 54320 ) conn = described_class.connect_start( '127.0.0.1', 54320, "", "", "me", "xxxx", "somedb" ) while [PG::CONNECTION_STARTED, PG::CONNECTION_MADE].include?(conn.connect_poll) sleep 0.1 end serv.close expect{ conn.block }.to raise_error(PG::ConnectionBad, /server closed the connection unexpectedly/) expect{ conn.block }.to raise_error(PG::ConnectionBad, /can't get socket descriptor/) end it "sets the fallback_application_name on new connections" do conn_string = PG::Connection.parse_connect_args( 'dbname=test' ) conn_name = conn_string[ /application_name='(.*?)'/, 1 ] expect( conn_name ).to include( $0[0..10] ) expect( conn_name ).to include( $0[-10..-1] ) expect( conn_name.length ).to be <= 64 end it "sets a shortened fallback_application_name on new connections" do old_0 = $0 begin $0 = "/this/is/a/very/long/path/with/many/directories/to/our/beloved/ruby" conn_string = PG::Connection.parse_connect_args( 'dbname=test' ) conn_name = conn_string[ /application_name='(.*?)'/, 1 ] expect( conn_name ).to include( $0[0..10] ) expect( conn_name ).to include( $0[-10..-1] ) expect( conn_name.length ).to be <= 64 ensure $0 = old_0 end end it "calls the block supplied to wait_for_notify with the notify payload if it accepts " + "any number of arguments" do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN knees' ) conn = described_class.connect( @conninfo ) conn.exec( %Q{NOTIFY knees, 'skirt and boots'} ) conn.finish event, pid, msg = nil @conn.wait_for_notify( 10 ) do |*args| event, pid, msg = *args end @conn.exec( 'UNLISTEN knees' ) expect( event ).to eq( 'knees' ) expect( pid ).to be_a_kind_of( Integer ) expect( msg ).to eq( 'skirt and boots' ) end it "accepts nil as the timeout in #wait_for_notify " do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN knees' ) conn = described_class.connect( @conninfo ) conn.exec( %Q{NOTIFY knees} ) conn.finish event, pid = nil @conn.wait_for_notify( nil ) do |*args| event, pid = *args end @conn.exec( 'UNLISTEN knees' ) expect( event ).to eq( 'knees' ) expect( pid ).to be_a_kind_of( Integer ) end it "sends nil as the payload if the notification wasn't given one" do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN knees' ) conn = described_class.connect( @conninfo ) conn.exec( %Q{NOTIFY knees} ) conn.finish payload = :notnil @conn.wait_for_notify( nil ) do |*args| payload = args[ 2 ] end @conn.exec( 'UNLISTEN knees' ) expect( payload ).to be_nil() end it "calls the block supplied to wait_for_notify with the notify payload if it accepts " + "two arguments" do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN knees' ) conn = described_class.connect( @conninfo ) conn.exec( %Q{NOTIFY knees, 'skirt and boots'} ) conn.finish event, pid, msg = nil @conn.wait_for_notify( 10 ) do |arg1, arg2| event, pid, msg = arg1, arg2 end @conn.exec( 'UNLISTEN knees' ) expect( event ).to eq( 'knees' ) expect( pid ).to be_a_kind_of( Integer ) expect( msg ).to be_nil() end it "calls the block supplied to wait_for_notify with the notify payload if it " + "doesn't accept arguments" do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN knees' ) conn = described_class.connect( @conninfo ) conn.exec( %Q{NOTIFY knees, 'skirt and boots'} ) conn.finish notification_received = false @conn.wait_for_notify( 10 ) do notification_received = true end @conn.exec( 'UNLISTEN knees' ) expect( notification_received ).to be_truthy() end it "calls the block supplied to wait_for_notify with the notify payload if it accepts " + "three arguments" do @conn.exec( 'ROLLBACK' ) @conn.exec( 'LISTEN knees' ) conn = described_class.connect( @conninfo ) conn.exec( %Q{NOTIFY knees, 'skirt and boots'} ) conn.finish event, pid, msg = nil @conn.wait_for_notify( 10 ) do |arg1, arg2, arg3| event, pid, msg = arg1, arg2, arg3 end @conn.exec( 'UNLISTEN knees' ) expect( event ).to eq( 'knees' ) expect( pid ).to be_a_kind_of( Integer ) expect( msg ).to eq( 'skirt and boots' ) end context "server ping", :without_transaction do it "pings successfully with connection string" do ping = described_class.ping(@conninfo) expect( ping ).to eq( PG::PQPING_OK ) end it "pings using 7 arguments converted to strings" do ping = described_class.ping('localhost', @port, nil, nil, :test, nil, nil) expect( ping ).to eq( PG::PQPING_OK ) end it "pings using a hash of connection parameters" do ping = described_class.ping( :host => 'localhost', :port => @port, :dbname => :test) expect( ping ).to eq( PG::PQPING_OK ) end it "returns correct response when ping connection cannot be established" do ping = described_class.ping( :host => 'localhost', :port => 9999, :dbname => :test) expect( ping ).to eq( PG::PQPING_NO_RESPONSE ) end it "returns error when ping connection arguments are wrong" do ping = described_class.ping('localhost', 'localhost', nil, nil, :test, nil, nil) expect( ping ).to_not eq( PG::PQPING_OK ) end it "returns correct response when ping connection arguments are wrong" do ping = described_class.ping( :host => 'localhost', :invalid_option => 9999, :dbname => :test) expect( ping ).to eq( PG::PQPING_NO_ATTEMPT ) end end describe "set_single_row_mode" do it "raises an error when called at the wrong time" do expect { @conn.set_single_row_mode }.to raise_error(PG::Error) end it "should work in single row mode" do @conn.send_query( "SELECT generate_series(1,10)" ) @conn.set_single_row_mode results = [] loop do @conn.block res = @conn.get_result or break results << res end expect( results.length ).to eq( 11 ) results[0..-2].each do |res| expect( res.result_status ).to eq( PG::PGRES_SINGLE_TUPLE ) values = res.field_values('generate_series') expect( values.length ).to eq( 1 ) expect( values.first.to_i ).to be > 0 end expect( results.last.result_status ).to eq( PG::PGRES_TUPLES_OK ) expect( results.last.ntuples ).to eq( 0 ) end it "should receive rows before entire query is finished" do @conn.send_query( "SELECT generate_series(0,999), NULL UNION ALL SELECT 1000, pg_sleep(1);" ) @conn.set_single_row_mode start_time = Time.now first_row_time = nil loop do res = @conn.get_result or break res.check first_row_time = Time.now unless first_row_time end expect( (Time.now - start_time) ).to be >= 0.9 expect( (first_row_time - start_time) ).to be < 0.9 end it "should receive rows before entire query fails" do @conn.exec( "CREATE FUNCTION errfunc() RETURNS int AS $$ BEGIN RAISE 'test-error'; END; $$ LANGUAGE plpgsql;" ) @conn.send_query( "SELECT generate_series(0,999), NULL UNION ALL SELECT 1000, errfunc();" ) @conn.set_single_row_mode first_result = nil expect do loop do res = @conn.get_result or break res.check first_result ||= res end end.to raise_error(PG::Error) expect( first_result.kind_of?(PG::Result) ).to be_truthy expect( first_result.result_status ).to eq( PG::PGRES_SINGLE_TUPLE ) end end context "multinationalization support" do describe "rubyforge #22925: m17n support" do it "should return results in the same encoding as the client (iso-8859-1)" do @conn.internal_encoding = 'iso8859-1' res = @conn.exec_params("VALUES ('fantasia')", [], 0) out_string = res[0]['column1'] expect( out_string ).to eq( 'fantasia' ) expect( out_string.encoding ).to eq( Encoding::ISO8859_1 ) end it "should return results in the same encoding as the client (utf-8)" do @conn.internal_encoding = 'utf-8' res = @conn.exec_params("VALUES ('世界線航跡蔵')", [], 0) out_string = res[0]['column1'] expect( out_string ).to eq( '世界線航跡蔵' ) expect( out_string.encoding ).to eq( Encoding::UTF_8 ) end it "should return results in the same encoding as the client (EUC-JP)" do @conn.internal_encoding = 'EUC-JP' stmt = "VALUES ('世界線航跡蔵')".encode('EUC-JP') res = @conn.exec_params(stmt, [], 0) out_string = res[0]['column1'] expect( out_string ).to eq( '世界線航跡蔵'.encode('EUC-JP') ) expect( out_string.encoding ).to eq( Encoding::EUC_JP ) end it "returns the results in the correct encoding even if the client_encoding has " + "changed since the results were fetched" do @conn.internal_encoding = 'EUC-JP' stmt = "VALUES ('世界線航跡蔵')".encode('EUC-JP') res = @conn.exec_params(stmt, [], 0) @conn.internal_encoding = 'utf-8' out_string = res[0]['column1'] expect( out_string ).to eq( '世界線航跡蔵'.encode('EUC-JP') ) expect( out_string.encoding ).to eq( Encoding::EUC_JP ) end it "the connection should return ASCII-8BIT when it's set to SQL_ASCII" do @conn.exec "SET client_encoding TO SQL_ASCII" expect( @conn.internal_encoding ).to eq( Encoding::ASCII_8BIT ) end it "the connection should use JOHAB dummy encoding when it's set to JOHAB" do @conn.set_client_encoding "JOHAB" val = @conn.exec("SELECT chr(x'3391'::int)").values[0][0] expect( val.encoding.name ).to eq( "JOHAB" ) expect( val.unpack("H*")[0] ).to eq( "dc65" ) end it "can retrieve server encoding as text" do enc = @conn.parameter_status "server_encoding" expect( enc ).to eq( "UTF8" ) end it "can retrieve server encoding as ruby encoding" do expect( @conn.external_encoding ).to eq( Encoding::UTF_8 ) end it "uses the client encoding for escaped string" do original = "Möhre to 'scape".encode( "utf-16be" ) @conn.set_client_encoding( "euc_jp" ) escaped = @conn.escape( original ) expect( escaped.encoding ).to eq( Encoding::EUC_JP ) expect( escaped ).to eq( "Möhre to ''scape".encode(Encoding::EUC_JP) ) end it "uses the client encoding for escaped literal" do original = "Möhre to 'scape".encode( "utf-16be" ) @conn.set_client_encoding( "euc_jp" ) escaped = @conn.escape_literal( original ) expect( escaped.encoding ).to eq( Encoding::EUC_JP ) expect( escaped ).to eq( "'Möhre to ''scape'".encode(Encoding::EUC_JP) ) end it "uses the client encoding for escaped identifier" do original = "Möhre to 'scape".encode( "utf-16le" ) @conn.set_client_encoding( "euc_jp" ) escaped = @conn.escape_identifier( original ) expect( escaped.encoding ).to eq( Encoding::EUC_JP ) expect( escaped ).to eq( "\"Möhre to 'scape\"".encode(Encoding::EUC_JP) ) end it "uses the client encoding for quote_ident" do original = "Möhre to 'scape".encode( "utf-16le" ) @conn.set_client_encoding( "euc_jp" ) escaped = @conn.quote_ident( original ) expect( escaped.encoding ).to eq( Encoding::EUC_JP ) expect( escaped ).to eq( "\"Möhre to 'scape\"".encode(Encoding::EUC_JP) ) end it "uses the previous string encoding for escaped string" do original = "Möhre to 'scape".encode( "iso-8859-1" ) @conn.set_client_encoding( "euc_jp" ) escaped = described_class.escape( original ) expect( escaped.encoding ).to eq( Encoding::ISO8859_1 ) expect( escaped ).to eq( "Möhre to ''scape".encode(Encoding::ISO8859_1) ) end it "uses the previous string encoding for quote_ident" do original = "Möhre to 'scape".encode( "iso-8859-1" ) @conn.set_client_encoding( "euc_jp" ) escaped = described_class.quote_ident( original ) expect( escaped.encoding ).to eq( Encoding::ISO8859_1 ) expect( escaped.encode ).to eq( "\"Möhre to 'scape\"".encode(Encoding::ISO8859_1) ) end it "raises appropriate error if set_client_encoding is called with invalid arguments" do expect { @conn.set_client_encoding( "invalid" ) }.to raise_error(PG::Error, /invalid value/) expect { @conn.set_client_encoding( :invalid ) }.to raise_error(TypeError) expect { @conn.set_client_encoding( nil ) }.to raise_error(TypeError) end it "can use an encoding with high index for client encoding" do # Allocate a lot of encoding indices, so that MRI's ENCODING_INLINE_MAX is exceeded unless Encoding.name_list.include?("pgtest-0") 256.times do |eidx| Encoding::UTF_8.replicate("pgtest-#{eidx}") end end # Now allocate the JOHAB encoding with an unusual high index @conn.set_client_encoding "JOHAB" val = @conn.exec("SELECT chr(x'3391'::int)").values[0][0] expect( val.encoding.name ).to eq( "JOHAB" ) end end describe "respect and convert character encoding of input strings" do before :each do @conn.internal_encoding = __ENCODING__ end it "should convert query string and parameters to #exec_params" do r = @conn.exec_params("VALUES( $1, $2, $1=$2, 'grün')".encode("utf-16le"), ['grün'.encode('utf-16be'), 'grün'.encode('iso-8859-1')]) expect( r.values ).to eq( [['grün', 'grün', 't', 'grün']] ) end it "should convert query string to #exec" do r = @conn.exec("SELECT 'grün'".encode("utf-16be")) expect( r.values ).to eq( [['grün']] ) end it "should convert strings and parameters to #prepare and #exec_prepared" do @conn.prepare("weiß1".encode("utf-16be"), "VALUES( $1, $2, $1=$2, 'grün')".encode("cp850")) r = @conn.exec_prepared("weiß1".encode("utf-32le"), ['grün'.encode('cp936'), 'grün'.encode('utf-16le')]) expect( r.values ).to eq( [['grün', 'grün', 't', 'grün']] ) end it "should convert strings to #describe_prepared" do @conn.prepare("weiß2", "VALUES(123)") r = @conn.describe_prepared("weiß2".encode("utf-16be")) expect( r.nfields ).to eq( 1 ) end it "should convert strings to #describe_portal" do @conn.exec "DECLARE cörsör CURSOR FOR VALUES(1,2,3)" r = @conn.describe_portal("cörsör".encode("utf-16le")) expect( r.nfields ).to eq( 3 ) end it "should convert query string to #send_query" do @conn.send_query("VALUES('grün')".encode("utf-16be")) expect( @conn.get_last_result.values ).to eq( [['grün']] ) end it "should convert query string and parameters to #send_query_params" do @conn.send_query_params("VALUES( $1, $2, $1=$2, 'grün')".encode("utf-16le"), ['grün'.encode('utf-32be'), 'grün'.encode('iso-8859-1')]) expect( @conn.get_last_result.values ).to eq( [['grün', 'grün', 't', 'grün']] ) end it "should convert strings and parameters to #send_prepare and #send_query_prepared" do @conn.send_prepare("weiß3".encode("iso-8859-1"), "VALUES( $1, $2, $1=$2, 'grün')".encode("utf-16be")) @conn.get_last_result @conn.send_query_prepared("weiß3".encode("utf-32le"), ['grün'.encode('utf-16le'), 'grün'.encode('iso-8859-1')]) expect( @conn.get_last_result.values ).to eq( [['grün', 'grün', 't', 'grün']] ) end it "should convert strings to #send_describe_prepared" do @conn.prepare("weiß4", "VALUES(123)") @conn.send_describe_prepared("weiß4".encode("utf-16be")) expect( @conn.get_last_result.nfields ).to eq( 1 ) end it "should convert strings to #send_describe_portal" do @conn.exec "DECLARE cörsör CURSOR FOR VALUES(1,2,3)" @conn.send_describe_portal("cörsör".encode("utf-16le")) expect( @conn.get_last_result.nfields ).to eq( 3 ) end it "should convert error string to #put_copy_end" do @conn.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" ) @conn.exec( "COPY copytable FROM STDIN" ) @conn.put_copy_end("grün".encode("utf-16be")) expect( @conn.get_result.error_message ).to match(/grün/) @conn.get_result end end it "rejects command strings with zero bytes" do expect{ @conn.exec( "SELECT 1;\x00" ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.exec_params( "SELECT 1;\x00", [] ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.prepare( "abc\x00", "SELECT 1;" ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.prepare( "abc", "SELECT 1;\x00" ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.exec_prepared( "abc\x00", [] ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.describe_prepared( "abc\x00" ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.describe_portal( "abc\x00" ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.send_query( "SELECT 1;\x00" ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.send_query_params( "SELECT 1;\x00", [] ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.send_prepare( "abc\x00", "SELECT 1;" ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.send_prepare( "abc", "SELECT 1;\x00" ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.send_query_prepared( "abc\x00", [] ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.send_describe_prepared( "abc\x00" ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.send_describe_portal( "abc\x00" ) }.to raise_error(ArgumentError, /null byte/) end it "rejects query params with zero bytes" do expect{ @conn.exec_params( "SELECT 1;\x00", ["ab\x00"] ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.exec_prepared( "abc\x00", ["ab\x00"] ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.send_query_params( "SELECT 1;\x00", ["ab\x00"] ) }.to raise_error(ArgumentError, /null byte/) expect{ @conn.send_query_prepared( "abc\x00", ["ab\x00"] ) }.to raise_error(ArgumentError, /null byte/) end it "rejects string with zero bytes in escape" do expect{ @conn.escape( "ab\x00cd" ) }.to raise_error(ArgumentError, /null byte/) end it "rejects string with zero bytes in escape_literal" do expect{ @conn.escape_literal( "ab\x00cd" ) }.to raise_error(ArgumentError, /null byte/) end it "rejects string with zero bytes in escape_identifier" do expect{ @conn.escape_identifier( "ab\x00cd" ) }.to raise_error(ArgumentError, /null byte/) end it "rejects string with zero bytes in quote_ident" do expect{ described_class.quote_ident( "ab\x00cd" ) }.to raise_error(ArgumentError, /null byte/) end it "rejects Array with string with zero bytes" do original = ["xyz", "2\x00"] expect{ described_class.quote_ident( original ) }.to raise_error(ArgumentError, /null byte/) end it "can quote bigger strings with quote_ident" do original = "'01234567\"" * 100 escaped = described_class.quote_ident( original ) expect( escaped ).to eq( "\"" + original.gsub("\"", "\"\"") + "\"" ) end it "can quote Arrays with quote_ident" do original = "'01234567\"" escaped = described_class.quote_ident( [original]*3 ) expected = ["\"" + original.gsub("\"", "\"\"") + "\""] * 3 expect( escaped ).to eq( expected.join(".") ) end it "will raise a TypeError for invalid arguments to quote_ident" do expect{ described_class.quote_ident( nil ) }.to raise_error(TypeError) expect{ described_class.quote_ident( [nil] ) }.to raise_error(TypeError) expect{ described_class.quote_ident( [['a']] ) }.to raise_error(TypeError) end describe "Ruby 1.9.x default_internal encoding" do it "honors the Encoding.default_internal if it's set and the synchronous interface is used", :without_transaction do @conn.transaction do |txn_conn| txn_conn.internal_encoding = Encoding::ISO8859_1 txn_conn.exec( "CREATE TABLE defaultinternaltest ( foo text )" ) txn_conn.exec( "INSERT INTO defaultinternaltest VALUES ('Grün und Weiß')" ) end begin prev_encoding = Encoding.default_internal Encoding.default_internal = Encoding::ISO8859_2 conn = PG.connect( @conninfo ) expect( conn.internal_encoding ).to eq( Encoding::ISO8859_2 ) res = conn.exec( "SELECT foo FROM defaultinternaltest" ) expect( res[0]['foo'].encoding ).to eq( Encoding::ISO8859_2 ) ensure conn.exec( "DROP TABLE defaultinternaltest" ) conn.finish if conn Encoding.default_internal = prev_encoding end end it "allows users of the async interface to set the client_encoding to the default_internal" do begin prev_encoding = Encoding.default_internal Encoding.default_internal = Encoding::KOI8_R @conn.set_default_encoding expect( @conn.internal_encoding ).to eq( Encoding::KOI8_R ) ensure Encoding.default_internal = prev_encoding end end end it "encodes exception messages with the connection's encoding (#96)", :without_transaction do # Use a new connection so the client_encoding isn't set outside of this example conn = PG.connect( @conninfo ) conn.client_encoding = 'iso-8859-15' conn.transaction do conn.exec "CREATE TABLE foo (bar TEXT)" begin query = "INSERT INTO foo VALUES ('Côte d'Ivoire')".encode( 'iso-8859-15' ) conn.exec( query ) rescue => err expect( err.message.encoding ).to eq( Encoding::ISO8859_15 ) else fail "No exception raised?!" end end conn.finish if conn end it "handles clearing result in or after set_notice_receiver" do r = nil @conn.set_notice_receiver do |result| r = result expect( r.cleared? ).to eq(false) end @conn.exec "do $$ BEGIN RAISE NOTICE 'foo'; END; $$ LANGUAGE plpgsql;" sleep 0.2 expect( r ).to be_a( PG::Result ) expect( r.cleared? ).to eq(true) expect( r.autoclear? ).to eq(true) r.clear @conn.set_notice_receiver end it "receives properly encoded messages in the notice callbacks" do [:receiver, :processor].each do |kind| notices = [] @conn.internal_encoding = 'utf-8' if kind == :processor @conn.set_notice_processor do |msg| notices << msg end else @conn.set_notice_receiver do |result| notices << result.error_message end end 3.times do @conn.exec "do $$ BEGIN RAISE NOTICE '世界線航跡蔵'; END; $$ LANGUAGE plpgsql;" end expect( notices.length ).to eq( 3 ) notices.each do |notice| expect( notice ).to match( /^NOTICE:.*世界線航跡蔵/ ) expect( notice.encoding ).to eq( Encoding::UTF_8 ) end @conn.set_notice_receiver @conn.set_notice_processor end end it "receives properly encoded text from wait_for_notify", :without_transaction do @conn.internal_encoding = 'utf-8' @conn.exec( 'LISTEN "Möhre"' ) @conn.exec( %Q{NOTIFY "Möhre", '世界線航跡蔵'} ) event, pid, msg = nil @conn.wait_for_notify( 10 ) do |*args| event, pid, msg = *args end @conn.exec( 'UNLISTEN "Möhre"' ) expect( event ).to eq( "Möhre" ) expect( event.encoding ).to eq( Encoding::UTF_8 ) expect( pid ).to be_a_kind_of(Integer) expect( msg ).to eq( '世界線航跡蔵' ) expect( msg.encoding ).to eq( Encoding::UTF_8 ) end it "returns properly encoded text from notifies", :without_transaction do @conn.internal_encoding = 'utf-8' @conn.exec( 'LISTEN "Möhre"' ) @conn.exec( %Q{NOTIFY "Möhre", '世界線航跡蔵'} ) @conn.exec( 'UNLISTEN "Möhre"' ) notification = @conn.notifies expect( notification[:relname] ).to eq( "Möhre" ) expect( notification[:relname].encoding ).to eq( Encoding::UTF_8 ) expect( notification[:extra] ).to eq( '世界線航跡蔵' ) expect( notification[:extra].encoding ).to eq( Encoding::UTF_8 ) expect( notification[:be_pid] ).to be > 0 end end context "OS thread support" do it "Connection#exec shouldn't block a second thread" do t = Thread.new do @conn.exec( "select pg_sleep(1)" ) end sleep 0.5 expect( t ).to be_alive() t.join end it "Connection.new shouldn't block a second thread" do serv = nil t = Thread.new do serv = TCPServer.new( '127.0.0.1', 54320 ) expect { described_class.new( '127.0.0.1', 54320, "", "", "me", "xxxx", "somedb" ) }.to raise_error(PG::ConnectionBad, /server closed the connection unexpectedly/) end sleep 0.5 expect( t ).to be_alive() serv.close t.join end end describe "type casting" do it "should raise an error on invalid param mapping" do expect{ @conn.exec_params( "SELECT 1", [], nil, :invalid ) }.to raise_error(TypeError) end it "should return nil if no type mapping is set" do expect( @conn.type_map_for_queries ).to be_kind_of(PG::TypeMapAllStrings) expect( @conn.type_map_for_results ).to be_kind_of(PG::TypeMapAllStrings) end it "shouldn't type map params unless requested" do if @conn.server_version < 100000 expect{ @conn.exec_params( "SELECT $1", [5] ) }.to raise_error(PG::IndeterminateDatatype) else # PostgreSQL-10 maps to TEXT type (OID 25) expect( @conn.exec_params( "SELECT $1", [5] ).ftype(0)).to eq(25) end end it "should raise an error on invalid encoder to put_copy_data" do expect{ @conn.put_copy_data [1], :invalid }.to raise_error(TypeError) end it "can type cast parameters to put_copy_data with explicit encoder" do tm = PG::TypeMapByColumn.new [nil] row_encoder = PG::TextEncoder::CopyRow.new type_map: tm @conn.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" ) @conn.copy_data( "COPY copytable FROM STDOUT" ) do |res| @conn.put_copy_data [1], row_encoder @conn.put_copy_data ["2"], row_encoder end @conn.copy_data( "COPY copytable FROM STDOUT", row_encoder ) do |res| @conn.put_copy_data [3] @conn.put_copy_data ["4"] end res = @conn.exec( "SELECT * FROM copytable ORDER BY col1" ) expect( res.values ).to eq( [["1"], ["2"], ["3"], ["4"]] ) end context "with default query type map" do before :each do @conn2 = described_class.new(@conninfo) tm = PG::TypeMapByClass.new tm[Integer] = PG::TextEncoder::Integer.new oid: 20 @conn2.type_map_for_queries = tm row_encoder = PG::TextEncoder::CopyRow.new type_map: tm @conn2.encoder_for_put_copy_data = row_encoder end after :each do @conn2.close end it "should respect a type mapping for params and it's OID and format code" do res = @conn2.exec_params( "SELECT $1", [5] ) expect( res.values ).to eq( [["5"]] ) expect( res.ftype(0) ).to eq( 20 ) end it "should return the current type mapping" do expect( @conn2.type_map_for_queries ).to be_kind_of(PG::TypeMapByClass) end it "should work with arbitrary number of params in conjunction with type casting" do begin 3.step( 12, 0.2 ) do |exp| num_params = (2 ** exp).to_i sql = num_params.times.map{|n| "$#{n+1}" }.join(",") params = num_params.times.to_a res = @conn2.exec_params( "SELECT #{sql}", params ) expect( res.nfields ).to eq( num_params ) expect( res.values ).to eq( [num_params.times.map(&:to_s)] ) end rescue PG::ProgramLimitExceeded # Stop silently as soon the server complains about too many params end end it "can process #copy_data input queries with row encoder and respects character encoding" do @conn2.exec( "CREATE TEMP TABLE copytable (col1 TEXT)" ) @conn2.copy_data( "COPY copytable FROM STDOUT" ) do |res| @conn2.put_copy_data [1] @conn2.put_copy_data ["Möhre".encode("utf-16le")] end res = @conn2.exec( "SELECT * FROM copytable ORDER BY col1" ) expect( res.values ).to eq( [["1"], ["Möhre"]] ) end end context "with default result type map" do before :each do @conn2 = described_class.new(@conninfo) tm = PG::TypeMapByOid.new tm.add_coder PG::TextDecoder::Integer.new oid: 23, format: 0 @conn2.type_map_for_results = tm row_decoder = PG::TextDecoder::CopyRow.new @conn2.decoder_for_get_copy_data = row_decoder end after :each do @conn2.close end it "should respect a type mapping for result" do res = @conn2.exec_params( "SELECT $1::INT", ["5"] ) expect( res.values ).to eq( [[5]] ) end it "should return the current type mapping" do expect( @conn2.type_map_for_results ).to be_kind_of(PG::TypeMapByOid) end it "should work with arbitrary number of params in conjunction with type casting" do begin 3.step( 12, 0.2 ) do |exp| num_params = (2 ** exp).to_i sql = num_params.times.map{|n| "$#{n+1}::INT" }.join(",") params = num_params.times.to_a res = @conn2.exec_params( "SELECT #{sql}", params ) expect( res.nfields ).to eq( num_params ) expect( res.values ).to eq( [num_params.times.to_a] ) end rescue PG::ProgramLimitExceeded # Stop silently as soon the server complains about too many params end end it "can process #copy_data output with row decoder and respects character encoding" do @conn2.internal_encoding = Encoding::ISO8859_1 rows = [] @conn2.copy_data( "COPY (VALUES('1'), ('Möhre')) TO STDOUT".encode("utf-16le") ) do |res| while row=@conn2.get_copy_data rows << row end end expect( rows.last.last.encoding ).to eq( Encoding::ISO8859_1 ) expect( rows ).to eq( [["1"], ["Möhre".encode("iso-8859-1")]] ) end it "can type cast #copy_data output with explicit decoder" do tm = PG::TypeMapByColumn.new [PG::TextDecoder::Integer.new] row_decoder = PG::TextDecoder::CopyRow.new type_map: tm rows = [] @conn.copy_data( "COPY (SELECT 1 UNION ALL SELECT 2) TO STDOUT", row_decoder ) do |res| while row=@conn.get_copy_data rows << row end end @conn.copy_data( "COPY (SELECT 3 UNION ALL SELECT 4) TO STDOUT" ) do |res| while row=@conn.get_copy_data( false, row_decoder ) rows << row end end expect( rows ).to eq( [[1], [2], [3], [4]] ) end end end describe :field_name_type do before :each do @conn2 = PG.connect(@conninfo) end after :each do @conn2.close end it "uses string field names per default" do expect(@conn2.field_name_type).to eq(:string) end it "can set string field names" do @conn2.field_name_type = :string expect(@conn2.field_name_type).to eq(:string) res = @conn2.exec("SELECT 1 as az") expect(res.field_name_type).to eq(:string) expect(res.fields).to eq(["az"]) end it "can set symbol field names" do @conn2.field_name_type = :symbol expect(@conn2.field_name_type).to eq(:symbol) res = @conn2.exec("SELECT 1 as az") expect(res.field_name_type).to eq(:symbol) expect(res.fields).to eq([:az]) end it "can't set invalid values" do expect{ @conn2.field_name_type = :sym }.to raise_error(ArgumentError, /invalid argument :sym/) expect{ @conn2.field_name_type = "symbol" }.to raise_error(ArgumentError, /invalid argument "symbol"/) end end describe "deprecated forms of methods" do if PG::VERSION < "2" it "should forward exec to exec_params" do res = @conn.exec("VALUES($1::INT)", [7]).values expect(res).to eq( [["7"]] ) res = @conn.exec("VALUES($1::INT)", [7], 1).values expect(res).to eq( [[[7].pack("N")]] ) res = @conn.exec("VALUES(8)", [], 1).values expect(res).to eq( [[[8].pack("N")]] ) end it "should forward exec_params to exec" do res = @conn.exec_params("VALUES(3); VALUES(4)").values expect(res).to eq( [["4"]] ) res = @conn.exec_params("VALUES(3); VALUES(4)", nil).values expect(res).to eq( [["4"]] ) res = @conn.exec_params("VALUES(3); VALUES(4)", nil, nil).values expect(res).to eq( [["4"]] ) res = @conn.exec_params("VALUES(3); VALUES(4)", nil, 1).values expect(res).to eq( [["4"]] ) res = @conn.exec_params("VALUES(3); VALUES(4)", nil, nil, nil).values expect(res).to eq( [["4"]] ) expect{ @conn.exec_params("VALUES(3); VALUES(4)", nil, nil, nil, nil).values }.to raise_error(ArgumentError) end it "should forward send_query to send_query_params" do @conn.send_query("VALUES($1)", [5]) expect(@conn.get_last_result.values).to eq( [["5"]] ) end it "should respond_to socket", :unix do expect( @conn.socket ).to eq( @conn.socket_io.fileno ) end else # Method forwarding removed by PG::VERSION >= "2" it "shouldn't forward exec to exec_params" do expect do @conn.exec("VALUES($1::INT)", [7]) end.to raise_error(ArgumentError) end it "shouldn't forward exec_params to exec" do expect do @conn.exec_params("VALUES(3); VALUES(4)") end.to raise_error(ArgumentError) end it "shouldn't forward send_query to send_query_params" do expect do @conn.send_query("VALUES($1)", [5]) end.to raise_error(ArgumentError) end it "shouldn't forward async_exec_params to async_exec" do expect do @conn.async_exec_params("VALUES(1)") end.to raise_error(ArgumentError) end it "shouldn't respond_to socket" do expect do @conn.socket end.to raise_error(ArgumentError) end end it "shouldn't forward send_query_params to send_query" do expect{ @conn.send_query_params("VALUES(4)").values } .to raise_error(ArgumentError) expect{ @conn.send_query_params("VALUES(4)", nil).values } .to raise_error(TypeError) end end end pg-1.2.3/spec/pg/type_map_by_class_spec.rb0000644000004100000410000000726213704151215020555 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative '../helpers' require 'pg' describe PG::TypeMapByClass do let!(:textenc_int){ PG::TextEncoder::Integer.new name: 'INT4', oid: 23 } let!(:textenc_float){ PG::TextEncoder::Float.new name: 'FLOAT8', oid: 701 } let!(:textenc_string){ PG::TextEncoder::String.new name: 'TEXT', oid: 25 } let!(:binaryenc_int){ PG::BinaryEncoder::Int8.new name: 'INT8', oid: 20, format: 1 } let!(:pass_through_type) do type = Class.new(PG::SimpleEncoder) do def encode(*v) v.inspect end end.new type.oid = 25 type.format = 0 type.name = 'pass_through' type end let!(:tm) do tm = PG::TypeMapByClass.new tm[Integer] = binaryenc_int tm[Float] = textenc_float tm[Symbol] = pass_through_type tm end let!(:raise_class) do Class.new end let!(:derived_tm) do tm = Class.new(PG::TypeMapByClass) do def array_type_map_for(value) PG::TextEncoder::Array.new name: '_INT4', oid: 1007, elements_type: PG::TextEncoder::Integer.new end end.new tm[Integer] = proc{|value| textenc_int } tm[raise_class] = proc{|value| /invalid/ } tm[Array] = :array_type_map_for tm end it "should retrieve all conversions" do expect( tm.coders ).to eq( { Integer => binaryenc_int, Float => textenc_float, Symbol => pass_through_type, } ) end it "should retrieve particular conversions" do expect( tm[Integer] ).to eq(binaryenc_int) expect( tm[Float] ).to eq(textenc_float) expect( tm[Range] ).to be_nil expect( derived_tm[raise_class] ).to be_kind_of(Proc) expect( derived_tm[Array] ).to eq(:array_type_map_for) end it "should allow deletion of coders" do tm[Integer] = nil expect( tm[Integer] ).to be_nil expect( tm.coders ).to eq( { Float => textenc_float, Symbol => pass_through_type, } ) end it "forwards query param conversions to the #default_type_map" do tm1 = PG::TypeMapByColumn.new( [textenc_int, nil, nil] ) tm2 = PG::TypeMapByClass.new tm2[Integer] = PG::TextEncoder::Integer.new name: 'INT2', oid: 21 tm2.default_type_map = tm1 res = @conn.exec_params( "SELECT $1, $2, $3::TEXT", ['1', 2, 3], 0, tm2 ) expect( res.ftype(0) ).to eq( 23 ) # tm1 expect( res.ftype(1) ).to eq( 21 ) # tm2 expect( res.getvalue(0,2) ).to eq( "3" ) # TypeMapAllStrings end # # Decoding Examples # it "should raise an error when used for results" do res = @conn.exec_params( "SELECT 1", [], 1 ) expect{ res.type_map = tm }.to raise_error(NotImplementedError, /not suitable to map result values/) end # # Encoding Examples # it "should allow mixed type conversions" do res = @conn.exec_params( "SELECT $1, $2, $3", [5, 1.23, :TestSymbol], 0, tm ) expect( res.values ).to eq([['5', '1.23', "[:TestSymbol, #{@conn.internal_encoding.inspect}]"]]) expect( res.ftype(0) ).to eq(20) end it "should expire the cache after changes to the coders" do res = @conn.exec_params( "SELECT $1", [5], 0, tm ) expect( res.ftype(0) ).to eq(20) tm[Integer] = textenc_int res = @conn.exec_params( "SELECT $1", [5], 0, tm ) expect( res.ftype(0) ).to eq(23) end it "should allow mixed type conversions with derived type map" do res = @conn.exec_params( "SELECT $1, $2", [6, [7]], 0, derived_tm ) expect( res.values ).to eq([['6', '{7}']]) expect( res.ftype(0) ).to eq(23) expect( res.ftype(1) ).to eq(1007) end it "should raise TypeError with derived type map" do expect{ @conn.exec_params( "SELECT $1", [raise_class.new], 0, derived_tm ) }.to raise_error(TypeError, /invalid type Regexp/) end it "should raise error on invalid coder object" do tm[TrueClass] = "dummy" expect{ @conn.exec_params( "SELECT $1", [true], 0, tm ) }.to raise_error(NoMethodError, /undefined method.*call/) end end pg-1.2.3/spec/pg/type_map_by_oid_spec.rb0000644000004100000410000001071113704151215020214 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative '../helpers' require 'pg' describe PG::TypeMapByOid do let!(:textdec_int){ PG::TextDecoder::Integer.new name: 'INT4', oid: 23 } let!(:textdec_float){ PG::TextDecoder::Float.new name: 'FLOAT8', oid: 701 } let!(:textdec_string){ PG::TextDecoder::String.new name: 'TEXT', oid: 25 } let!(:textdec_bytea){ PG::TextDecoder::Bytea.new name: 'BYTEA', oid: 17 } let!(:binarydec_float){ PG::BinaryDecoder::Float.new name: 'FLOAT8', oid: 701, format: 1 } let!(:pass_through_type) do type = Class.new(PG::SimpleDecoder) do def decode(*v) v end end.new type.oid = 1082 type.format = 0 type.name = 'pass_through' type end let!(:tm) do tm = PG::TypeMapByOid.new tm.add_coder textdec_int tm.add_coder textdec_float tm.add_coder binarydec_float tm.add_coder pass_through_type tm end it "should retrieve it's conversions" do expect( tm.coders ).to eq( [ textdec_int, textdec_float, pass_through_type, binarydec_float, ] ) end it "should allow deletion of coders" do expect( tm.rm_coder 0, 701 ).to eq(textdec_float) expect( tm.rm_coder 0, 701 ).to eq(nil) expect( tm.rm_coder 1, 701 ).to eq(binarydec_float) expect( tm.coders ).to eq( [ textdec_int, pass_through_type, ] ) end it "should check format when deleting coders" do expect{ tm.rm_coder(2, 123) }.to raise_error(ArgumentError) expect{ tm.rm_coder(-1, 123) }.to raise_error(ArgumentError) end it "should check format when adding coders" do textdec_int.format = 2 expect{ tm.add_coder textdec_int }.to raise_error(ArgumentError) textdec_int.format = -1 expect{ tm.add_coder textdec_int }.to raise_error(ArgumentError) end it "should check coder type when adding coders" do expect{ tm.add_coder :dummy }.to raise_error(ArgumentError) end it "should allow reading and writing max_rows_for_online_lookup" do expect( tm.max_rows_for_online_lookup ).to eq(10) tm.max_rows_for_online_lookup = 5 expect( tm.max_rows_for_online_lookup ).to eq(5) end it "should allow building new TypeMapByColumn for a given result" do res = @conn.exec( "SELECT 1, 'a', 2.0::FLOAT, '2013-06-30'::DATE" ) tm2 = tm.build_column_map(res) expect( tm2 ).to be_a_kind_of(PG::TypeMapByColumn) expect( tm2.coders ).to eq( [textdec_int, nil, textdec_float, pass_through_type] ) end it "forwards result value conversions to another TypeMapByOid as #default_type_map" do # One run with implicit built TypeMapByColumn and another with online lookup # for each type map. [[0, 0], [0, 10], [10, 0], [10, 10]].each do |max_rows1, max_rows2| tm1 = PG::TypeMapByOid.new tm1.add_coder PG::TextDecoder::Integer.new name: 'INT2', oid: 21 tm1.max_rows_for_online_lookup = max_rows1 tm2 = PG::TypeMapByOid.new tm2.add_coder PG::TextDecoder::Integer.new name: 'INT4', oid: 23 tm2.max_rows_for_online_lookup = max_rows2 tm2.default_type_map = tm1 res = @conn.exec( "SELECT '1'::INT4, '2'::INT2, '3'::INT8" ).map_types!( tm2 ) expect( res.getvalue(0,0) ).to eq( 1 ) # tm2 expect( res.getvalue(0,1) ).to eq( 2 ) # tm1 expect( res.getvalue(0,2) ).to eq( "3" ) # TypeMapAllStrings end end # # Decoding Examples text format # it "should allow mixed type conversions in text format" do res = @conn.exec( "SELECT 1, 'a', 2.0::FLOAT, '2013-06-30'::DATE" ) res.type_map = tm expect( res.values ).to eq( [[1, 'a', 2.0, ['2013-06-30', 0, 3] ]] ) end it "should build a TypeMapByColumn when assigned and the number of rows is high enough" do res = @conn.exec( "SELECT generate_series(1,20), 'a', 2.0::FLOAT, '2013-06-30'::DATE" ) res.type_map = tm expect( res.type_map ).to be_kind_of( PG::TypeMapByColumn ) expect( res.type_map.coders ).to eq( [textdec_int, nil, textdec_float, pass_through_type] ) end it "should use TypeMapByOid for online lookup and the number of rows is low enough" do res = @conn.exec( "SELECT 1, 'a', 2.0::FLOAT, '2013-06-30'::DATE" ) res.type_map = tm expect( res.type_map ).to be_kind_of( PG::TypeMapByOid ) end # # Decoding Examples binary format # it "should allow mixed type conversions in binary format" do res = @conn.exec_params( "SELECT 1, 2.0::FLOAT", [], 1 ) res.type_map = tm expect( res.values ).to eq( [["\x00\x00\x00\x01", 2.0 ]] ) end # # Encoding Examples # it "should raise an error used for query params" do expect{ @conn.exec_params( "SELECT $1", [5], 0, tm ) }.to raise_error(NotImplementedError, /not suitable to map query params/) end end pg-1.2.3/spec/pg/type_map_in_ruby_spec.rb0000644000004100000410000001031713704151215020420 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative '../helpers' require 'pg' describe PG::TypeMapInRuby do context "result values" do it "should be usable non-derived" do tm = PG::TypeMapInRuby.new res = @conn.exec("select 5").map_types!(tm) expect( res.getvalue(0,0) ).to eq( "5" ) end it "should call derived result mapping methods" do tm = Class.new(PG::TypeMapInRuby) do attr_reader :fit_to_result_args def fit_to_result(*args) @fit_to_result_args = args self end def typecast_result_value(*args) [args, super] end end.new res = @conn.exec("select 5,6").map_types!(tm) expect( res.getvalue(0,1) ).to eq( [[res, 0, 1], "6"] ) expect( tm.fit_to_result_args ).to eq( [res] ) end it "should accept only a type map object from fit_to_result" do tm = Class.new(PG::TypeMapInRuby) do def fit_to_result(*args) :invalid end end.new res = @conn.exec("select 5,6") expect{ res.map_types!(tm) }.to raise_error(TypeError, /kind of PG::TypeMap/) end end context "query bind params" do it "should be usable non-derived" do tm = PG::TypeMapInRuby.new res = @conn.exec_params("select $1::int, $2::text", [5, 6], 0, tm) expect( res.values ).to eq( [["5", "6"]] ) end it "should call derived param mapping methods" do tm = Class.new(PG::TypeMapInRuby) do attr_reader :fit_to_query_args attr_reader :typecast_query_param_args def fit_to_query(params) @fit_to_query_args = params @typecast_query_param_args = [] self end def typecast_query_param(*args) @typecast_query_param_args << [args, super] PG::TextEncoder::Integer.new name: 'INT4', oid: 23 end end.new res = @conn.exec_params("select $1, $2", [5, 6], 0, tm) expect( res.ftype(0) ).to eq( 23 ) expect( tm.fit_to_query_args ).to eq( [5, 6] ) expect( tm.typecast_query_param_args ).to eq( [[[5, 0], nil], [[6, 1], nil]] ) end end context "put_copy_data" do it "should be usable non-derived" do tm = PG::TypeMapInRuby.new ce = PG::TextEncoder::CopyRow.new type_map: tm res = ce.encode([5, 6]) expect( res ).to eq( "5\t6\n" ) end it "should call derived data mapping methods" do tm = Class.new(PG::TypeMapInRuby) do attr_reader :fit_to_query_args attr_reader :typecast_query_param_args def fit_to_query(params) @fit_to_query_args = params @typecast_query_param_args = [] self end def typecast_query_param(*args) @typecast_query_param_args << [args, super] PG::TextEncoder::Integer.new name: 'INT4', oid: 23 end end.new ce = PG::TextEncoder::CopyRow.new type_map: tm res = ce.encode([5, 6]) expect( res ).to eq( "5\t6\n" ) expect( tm.fit_to_query_args ).to eq( [5, 6] ) expect( tm.typecast_query_param_args ).to eq( [[[5, 0], nil], [[6, 1], nil]] ) end it "shouldn't accept invalid return from typecast_query_param" do tm = Class.new(PG::TypeMapInRuby) do def typecast_query_param(*args) :invalid end end.new ce = PG::TextEncoder::CopyRow.new type_map: tm expect{ ce.encode([5, 6]) }.to raise_error(TypeError, /nil or kind of PG::Coder/) end end context "get_copy_data" do it "should be usable non-derived" do tm = PG::TypeMapInRuby.new ce = PG::TextDecoder::CopyRow.new type_map: tm res = ce.decode("5\t6\n") expect( res ).to eq( ["5", "6"] ) end it "should call derived data mapping methods" do tm = Class.new(PG::TypeMapInRuby) do attr_reader :fit_to_copy_get_args def fit_to_copy_get(*args) @fit_to_copy_get_args = args 0 end def typecast_copy_get(field_str, fieldno, format, enc) [field_str, fieldno, format, enc, super] end end.new ce = PG::TextDecoder::CopyRow.new type_map: tm res = ce.decode("5\t6\n") expect( tm.fit_to_copy_get_args ).to eq( [] ) expect( res ).to eq( [["5", 0, 0, Encoding::UTF_8, "5"], ["6", 1, 0, Encoding::UTF_8, "6"]] ) end it "shouldn't accept invalid return from fit_to_copy_get" do tm = Class.new(PG::TypeMapInRuby) do def fit_to_copy_get :invalid end end.new ce = PG::TextDecoder::CopyRow.new type_map: tm expect{ ce.decode("5\t6\n") }.to raise_error(TypeError, /kind of Integer/) end end end pg-1.2.3/spec/pg/tuple_spec.rb0000644000004100000410000002416313704151215016210 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative '../helpers' require 'pg' require 'objspace' describe PG::Tuple do let!(:typemap) { PG::BasicTypeMapForResults.new(@conn) } let!(:result2x2) { @conn.exec( "VALUES(1, 'a'), (2, 'b')" ) } let!(:result2x2sym) { @conn.exec( "VALUES(1, 'a'), (2, 'b')" ).field_names_as(:symbol) } let!(:result2x3cast) do @conn.exec( "SELECT * FROM (VALUES(1, TRUE, '3'), (2, FALSE, '4')) AS m (a, b, b)" ) .map_types!(typemap) end let!(:result2x3symcast) do @conn.exec( "SELECT * FROM (VALUES(1, TRUE, '3'), (2, FALSE, '4')) AS m (a, b, b)" ) .map_types!(typemap) .field_names_as(:symbol) end let!(:tuple0) { result2x2.tuple(0) } let!(:tuple0sym) { result2x2sym.tuple(0) } let!(:tuple1) { result2x2.tuple(1) } let!(:tuple1sym) { result2x2sym.tuple(1) } let!(:tuple2) { result2x3cast.tuple(0) } let!(:tuple2sym) { result2x3symcast.tuple(0) } let!(:tuple3) { str = Marshal.dump(result2x3cast.tuple(1)); Marshal.load(str) } let!(:tuple_empty) { PG::Tuple.new } describe "[]" do it "returns nil for invalid keys" do expect( tuple0["x"] ).to be_nil expect( tuple0[0.5] ).to be_nil expect( tuple0[2] ).to be_nil expect( tuple0[-3] ).to be_nil expect( tuple2[-4] ).to be_nil expect{ tuple_empty[0] }.to raise_error(TypeError) end it "supports array like access" do expect( tuple0[0] ).to eq( "1" ) expect( tuple0[1] ).to eq( "a" ) expect( tuple1[0] ).to eq( "2" ) expect( tuple1[1] ).to eq( "b" ) expect( tuple2[0] ).to eq( 1 ) expect( tuple2[1] ).to eq( true ) expect( tuple2[2] ).to eq( "3" ) expect( tuple3[0] ).to eq( 2 ) expect( tuple3[1] ).to eq( false ) expect( tuple3[2] ).to eq( "4" ) end it "supports negative indices" do expect( tuple0[-2] ).to eq( "1" ) expect( tuple0[-1] ).to eq( "a" ) expect( tuple2[-3] ).to eq( 1 ) expect( tuple2[-2] ).to eq( true ) expect( tuple2[-1] ).to eq( "3" ) end it "supports hash like access" do expect( tuple0["column1"] ).to eq( "1" ) expect( tuple0["column2"] ).to eq( "a" ) expect( tuple2["a"] ).to eq( 1 ) expect( tuple2["b"] ).to eq( "3" ) expect( tuple0[:b] ).to be_nil expect( tuple0["x"] ).to be_nil end it "supports hash like access with symbols" do expect( tuple0sym[:column1] ).to eq( "1" ) expect( tuple0sym[:column2] ).to eq( "a" ) expect( tuple2sym[:a] ).to eq( 1 ) expect( tuple2sym[:b] ).to eq( "3" ) expect( tuple2sym["b"] ).to be_nil expect( tuple0sym[:x] ).to be_nil end it "casts lazy and caches result" do a = [] deco = Class.new(PG::SimpleDecoder) do define_method(:decode) do |*args| a << args args.last end end.new result2x2.map_types!(PG::TypeMapByColumn.new([deco, deco])) t = result2x2.tuple(1) # cast and cache at first call to [0] a.clear expect( t[0] ).to eq( 0 ) expect( a ).to eq([["2", 1, 0]]) # use cache at second call to [0] a.clear expect( t[0] ).to eq( 0 ) expect( a ).to eq([]) # cast and cache at first call to [1] a.clear expect( t[1] ).to eq( 1 ) expect( a ).to eq([["b", 1, 1]]) end end describe "fetch" do it "raises proper errors for invalid keys" do expect{ tuple0.fetch("x") }.to raise_error(KeyError) expect{ tuple0.fetch(0.5) }.to raise_error(KeyError) expect{ tuple0.fetch(2) }.to raise_error(IndexError) expect{ tuple0.fetch(-3) }.to raise_error(IndexError) expect{ tuple0.fetch(-3) }.to raise_error(IndexError) expect{ tuple2.fetch(-4) }.to raise_error(IndexError) expect{ tuple_empty[0] }.to raise_error(TypeError) end it "supports array like access" do expect( tuple0.fetch(0) ).to eq( "1" ) expect( tuple0.fetch(1) ).to eq( "a" ) expect( tuple2.fetch(0) ).to eq( 1 ) expect( tuple2.fetch(1) ).to eq( true ) expect( tuple2.fetch(2) ).to eq( "3" ) end it "supports default value for indices" do expect( tuple0.fetch(2, 42) ).to eq( 42 ) expect( tuple0.fetch(2){43} ).to eq( 43 ) end it "supports negative indices" do expect( tuple0.fetch(-2) ).to eq( "1" ) expect( tuple0.fetch(-1) ).to eq( "a" ) expect( tuple2.fetch(-3) ).to eq( 1 ) expect( tuple2.fetch(-2) ).to eq( true ) expect( tuple2.fetch(-1) ).to eq( "3" ) end it "supports hash like access" do expect( tuple0.fetch("column1") ).to eq( "1" ) expect( tuple0.fetch("column2") ).to eq( "a" ) expect( tuple2.fetch("a") ).to eq( 1 ) expect( tuple2.fetch("b") ).to eq( "3" ) end it "supports default value for name keys" do expect( tuple0.fetch("x", "defa") ).to eq("defa") expect( tuple0.fetch("x"){"defa"} ).to eq("defa") end end describe "each" do it "can be used as an enumerator" do expect( tuple0.each ).to be_kind_of(Enumerator) expect( tuple0.each.to_a ).to eq( [["column1", "1"], ["column2", "a"]] ) expect( tuple1.each.to_a ).to eq( [["column1", "2"], ["column2", "b"]] ) expect( tuple2.each.to_a ).to eq( [["a", 1], ["b", true], ["b", "3"]] ) expect( tuple3.each.to_a ).to eq( [["a", 2], ["b", false], ["b", "4"]] ) expect{ tuple_empty.each }.to raise_error(TypeError) end it "can be used as an enumerator with symbols" do expect( tuple0sym.each ).to be_kind_of(Enumerator) expect( tuple0sym.each.to_a ).to eq( [[:column1, "1"], [:column2, "a"]] ) expect( tuple2sym.each.to_a ).to eq( [[:a, 1], [:b, true], [:b, "3"]] ) end it "can be used with block" do a = [] tuple0.each do |*v| a << v end expect( a ).to eq( [["column1", "1"], ["column2", "a"]] ) end end describe "each_value" do it "can be used as an enumerator" do expect( tuple0.each_value ).to be_kind_of(Enumerator) expect( tuple0.each_value.to_a ).to eq( ["1", "a"] ) expect( tuple1.each_value.to_a ).to eq( ["2", "b"] ) expect( tuple2.each_value.to_a ).to eq( [1, true, "3"] ) expect( tuple3.each_value.to_a ).to eq( [2, false, "4"] ) expect{ tuple_empty.each_value }.to raise_error(TypeError) end it "can be used with block" do a = [] tuple0.each_value do |v| a << v end expect( a ).to eq( ["1", "a"] ) end end it "responds to values" do expect( tuple0.values ).to eq( ["1", "a"] ) expect( tuple3.values ).to eq( [2, false, "4"] ) expect{ tuple_empty.values }.to raise_error(TypeError) end it "responds to key?" do expect( tuple1.key?("column1") ).to eq( true ) expect( tuple1.key?(:column1) ).to eq( false ) expect( tuple1.key?("other") ).to eq( false ) expect( tuple1.has_key?("column1") ).to eq( true ) expect( tuple1.has_key?("other") ).to eq( false ) end it "responds to key? as symbol" do expect( tuple1sym.key?(:column1) ).to eq( true ) expect( tuple1sym.key?("column1") ).to eq( false ) expect( tuple1sym.key?(:other) ).to eq( false ) expect( tuple1sym.has_key?(:column1) ).to eq( true ) expect( tuple1sym.has_key?(:other) ).to eq( false ) end it "responds to keys" do expect( tuple0.keys ).to eq( ["column1", "column2"] ) expect( tuple2.keys ).to eq( ["a", "b", "b"] ) end it "responds to keys as symbol" do expect( tuple0sym.keys ).to eq( [:column1, :column2] ) expect( tuple2sym.keys ).to eq( [:a, :b, :b] ) end describe "each_key" do it "can be used as an enumerator" do expect( tuple0.each_key ).to be_kind_of(Enumerator) expect( tuple0.each_key.to_a ).to eq( ["column1", "column2"] ) expect( tuple2.each_key.to_a ).to eq( ["a", "b", "b"] ) end it "can be used with block" do a = [] tuple0.each_key do |v| a << v end expect( a ).to eq( ["column1", "column2"] ) end end it "responds to length" do expect( tuple0.length ).to eq( 2 ) expect( tuple0.size ).to eq( 2 ) expect( tuple2.size ).to eq( 3 ) end it "responds to index" do expect( tuple0.index("column1") ).to eq( 0 ) expect( tuple0.index(:column1) ).to eq( nil ) expect( tuple0.index("column2") ).to eq( 1 ) expect( tuple0.index("x") ).to eq( nil ) expect( tuple2.index("a") ).to eq( 0 ) expect( tuple2.index("b") ).to eq( 2 ) end it "responds to index with symbol" do expect( tuple0sym.index(:column1) ).to eq( 0 ) expect( tuple0sym.index("column1") ).to eq( nil ) expect( tuple0sym.index(:column2) ).to eq( 1 ) expect( tuple0sym.index(:x) ).to eq( nil ) expect( tuple2sym.index(:a) ).to eq( 0 ) expect( tuple2sym.index(:b) ).to eq( 2 ) end it "can be used as Enumerable" do expect( tuple0.to_a ).to eq( [["column1", "1"], ["column2", "a"]] ) expect( tuple1.to_a ).to eq( [["column1", "2"], ["column2", "b"]] ) expect( tuple2.to_a ).to eq( [["a", 1], ["b", true], ["b", "3"]] ) expect( tuple3.to_a ).to eq( [["a", 2], ["b", false], ["b", "4"]] ) end it "can be marshaled" do [tuple0, tuple1, tuple2, tuple3, tuple0sym, tuple2sym].each do |t1| str = Marshal.dump(t1) t2 = Marshal.load(str) expect( t2 ).to be_kind_of(t1.class) expect( t2 ).not_to equal(t1) expect( t2.to_a ).to eq(t1.to_a) end end it "passes instance variables when marshaled" do t1 = tuple0 t1.instance_variable_set("@a", 4711) str = Marshal.dump(t1) t2 = Marshal.load(str) expect( t2.instance_variable_get("@a") ).to eq( 4711 ) end it "can't be marshaled when empty" do expect{ Marshal.dump(tuple_empty) }.to raise_error(TypeError) end it "should give account about memory usage" do expect( ObjectSpace.memsize_of(tuple0) ).to be > 40 expect( ObjectSpace.memsize_of(tuple_empty) ).to be > 0 end it "should override #inspect" do expect( tuple1.inspect ).to eq('#') expect( tuple2.inspect ).to eq('#') expect( tuple2sym.inspect ).to eq('#') expect{ tuple_empty.inspect }.to raise_error(TypeError) end context "with cleared result" do it "should raise an error when non-materialized fields are used" do r = result2x2 t = r.tuple(0) t[0] # materialize first field only r.clear # second column should fail expect{ t[1] }.to raise_error(PG::Error) expect{ t.fetch(1) }.to raise_error(PG::Error) expect{ t.fetch("column2") }.to raise_error(PG::Error) # first column should succeed expect( t[0] ).to eq( "1" ) expect( t.fetch(0) ).to eq( "1" ) expect( t.fetch("column1") ).to eq( "1" ) # should fail due to the second column expect{ t.values }.to raise_error(PG::Error) end end end pg-1.2.3/spec/pg/type_spec.rb0000644000004100000410000014131313704151215016035 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require 'pg' require 'time' describe "PG::Type derivations" do let!(:textenc_int) { PG::TextEncoder::Integer.new name: 'Integer', oid: 23 } let!(:textdec_int) { PG::TextDecoder::Integer.new name: 'Integer', oid: 23 } let!(:textenc_boolean) { PG::TextEncoder::Boolean.new } let!(:textdec_boolean) { PG::TextDecoder::Boolean.new } let!(:textenc_float) { PG::TextEncoder::Float.new } let!(:textdec_float) { PG::TextDecoder::Float.new } let!(:textenc_numeric) { PG::TextEncoder::Numeric.new } let!(:textenc_string) { PG::TextEncoder::String.new } let!(:textdec_string) { PG::TextDecoder::String.new } let!(:textenc_timestamp) { PG::TextEncoder::TimestampWithoutTimeZone.new } let!(:textdec_timestamp) { PG::TextDecoder::TimestampWithoutTimeZone.new } let!(:textenc_timestamputc) { PG::TextEncoder::TimestampUtc.new } let!(:textdec_timestamputc) { PG::TextDecoder::TimestampUtc.new } let!(:textdec_timestampul) { PG::TextDecoder::TimestampUtcToLocal.new } let!(:textenc_timestamptz) { PG::TextEncoder::TimestampWithTimeZone.new } let!(:textdec_timestamptz) { PG::TextDecoder::TimestampWithTimeZone.new } let!(:textenc_bytea) { PG::TextEncoder::Bytea.new } let!(:textdec_bytea) { PG::TextDecoder::Bytea.new } let!(:binaryenc_int2) { PG::BinaryEncoder::Int2.new } let!(:binaryenc_int4) { PG::BinaryEncoder::Int4.new } let!(:binaryenc_int8) { PG::BinaryEncoder::Int8.new } let!(:binarydec_integer) { PG::BinaryDecoder::Integer.new } let!(:intenc_incrementer) do Class.new(PG::SimpleEncoder) do def encode(value) (value.to_i + 1).to_s + " " end end.new end let!(:intdec_incrementer) do Class.new(PG::SimpleDecoder) do def decode(string, tuple=nil, field=nil) string.to_i+1 end end.new end let!(:intenc_incrementer_with_encoding) do Class.new(PG::SimpleEncoder) do def encode(value, encoding) r = (value.to_i + 1).to_s + " #{encoding}" r.encode!(encoding) end end.new end let!(:intenc_incrementer_with_int_result) do Class.new(PG::SimpleEncoder) do def encode(value) value.to_i+1 end end.new end it "shouldn't be possible to build a PG::Type directly" do expect{ PG::Coder.new }.to raise_error(TypeError, /cannot/) end describe PG::SimpleCoder do describe '#decode' do it "should offer decode method with tuple/field" do res = textdec_int.decode("123", 1, 1) expect( res ).to eq( 123 ) end it "should offer decode method without tuple/field" do res = textdec_int.decode("234") expect( res ).to eq( 234 ) end it "should decode with ruby decoder" do expect( intdec_incrementer.decode("3") ).to eq( 4 ) end it "should decode integers of different lengths from text format" do 30.times do |zeros| expect( textdec_int.decode("1" + "0"*zeros) ).to eq( 10 ** zeros ) expect( textdec_int.decode(zeros==0 ? "0" : "9"*zeros) ).to eq( 10 ** zeros - 1 ) expect( textdec_int.decode("-1" + "0"*zeros) ).to eq( -10 ** zeros ) expect( textdec_int.decode(zeros==0 ? "0" : "-" + "9"*zeros) ).to eq( -10 ** zeros + 1 ) end 66.times do |bits| expect( textdec_int.decode((2 ** bits).to_s) ).to eq( 2 ** bits ) expect( textdec_int.decode((2 ** bits - 1).to_s) ).to eq( 2 ** bits - 1 ) expect( textdec_int.decode((-2 ** bits).to_s) ).to eq( -2 ** bits ) expect( textdec_int.decode((-2 ** bits + 1).to_s) ).to eq( -2 ** bits + 1 ) end end it 'decodes bytea to a binary string' do expect( textdec_bytea.decode("\\x00010203EF") ).to eq( "\x00\x01\x02\x03\xef".b ) expect( textdec_bytea.decode("\\377\\000") ).to eq( "\xff\0".b ) end context 'timestamps' do it 'decodes timestamps without timezone as local time' do expect( textdec_timestamp.decode('2016-01-02 23:23:59.123456').iso8601(5) ). to eq( Time.new(2016,1,2, 23,23,59.123456).iso8601(5) ) expect( textdec_timestamp.decode('2016-08-02 23:23:59.123456').iso8601(5) ). to eq( Time.new(2016,8,2, 23,23,59.123456).iso8601(5) ) end it 'decodes timestamps with UTC time and returns UTC timezone' do expect( textdec_timestamputc.decode('2016-01-02 23:23:59.123456').iso8601(5) ). to eq( Time.utc(2016,1,2, 23,23,59.123456).iso8601(5) ) expect( textdec_timestamputc.decode('2016-08-02 23:23:59.123456').iso8601(5) ). to eq( Time.utc(2016,8,2, 23,23,59.123456).iso8601(5) ) end it 'decodes timestamps with UTC time and returns local timezone' do expect( textdec_timestampul.decode('2016-01-02 23:23:59.123456').iso8601(5) ). to eq( Time.utc(2016,1,2, 23,23,59.123456).getlocal.iso8601(5) ) expect( textdec_timestampul.decode('2016-08-02 23:23:59.123456').iso8601(5) ). to eq( Time.utc(2016,8,2, 23,23,59.123456).getlocal.iso8601(5) ) end it 'decodes timestamps with hour timezone' do expect( textdec_timestamptz.decode('2016-01-02 23:23:59.123456-04').iso8601(5) ). to eq( Time.new(2016,1,2, 23,23,59.123456, "-04:00").iso8601(5) ) expect( textdec_timestamptz.decode('2016-08-02 23:23:59.123456+10').iso8601(5) ). to eq( Time.new(2016,8,2, 23,23,59.123456, "+10:00").iso8601(5) ) expect( textdec_timestamptz.decode('1913-12-31 23:58:59.1231-03').iso8601(5) ). to eq( Time.new(1913, 12, 31, 23, 58, 59.1231, "-03:00").iso8601(5) ) expect( textdec_timestamptz.decode('4714-11-24 23:58:59.1231-03 BC').iso8601(5) ). to eq( Time.new(-4713, 11, 24, 23, 58, 59.1231, "-03:00").iso8601(5) ) expect( textdec_timestamptz.decode('294276-12-31 23:58:59.1231+03').iso8601(5) ). to eq( Time.new(294276, 12, 31, 23, 58, 59.1231, "+03:00").iso8601(5) ) end it 'decodes timestamps with hour:minute timezone' do expect( textdec_timestamptz.decode('2015-01-26 17:26:42.691511-04:15') ). to be_within(0.000001).of( Time.new(2015,01,26, 17, 26, 42.691511, "-04:15") ) expect( textdec_timestamptz.decode('2015-07-26 17:26:42.691511-04:30') ). to be_within(0.000001).of( Time.new(2015,07,26, 17, 26, 42.691511, "-04:30") ) expect( textdec_timestamptz.decode('2015-01-26 17:26:42.691511+10:45') ). to be_within(0.000001).of( Time.new(2015,01,26, 17, 26, 42.691511, "+10:45") ) end it 'decodes timestamps with hour:minute:sec timezone' do # SET TIME ZONE 'Europe/Dublin'; -- Was UTC−00:25:21 until 1916 # SELECT '1900-01-01'::timestamptz; # -- "1900-01-01 00:00:00-00:25:21" expect( textdec_timestamptz.decode('1916-01-01 00:00:00-00:25:21') ). to be_within(0.000001).of( Time.new(1916, 1, 1, 0, 0, 0, "-00:25:21") ) end it 'decodes timestamps with date before 1823' do expect( textdec_timestamp.decode('1822-01-02 23:23:59.123456').iso8601(5) ). to eq( Time.new(1822,01,02, 23, 23, 59.123456).iso8601(5) ) expect( textdec_timestamputc.decode('1822-01-02 23:23:59.123456').iso8601(5) ). to eq( Time.utc(1822,01,02, 23, 23, 59.123456).iso8601(5) ) expect( textdec_timestampul.decode('1822-01-02 23:23:59.123456').iso8601(5) ). to eq( Time.utc(1822,01,02, 23, 23, 59.123456).getlocal.iso8601(5) ) expect( textdec_timestamptz.decode('1822-01-02 23:23:59.123456+04').iso8601(5) ). to eq( Time.new(1822,01,02, 23, 23, 59.123456, "+04:00").iso8601(5) ) end it 'decodes timestamps with date after 2116' do expect( textdec_timestamp.decode('2117-01-02 23:23:59.123456').iso8601(5) ). to eq( Time.new(2117,01,02, 23, 23, 59.123456).iso8601(5) ) expect( textdec_timestamputc.decode('2117-01-02 23:23:59.123456').iso8601(5) ). to eq( Time.utc(2117,01,02, 23, 23, 59.123456).iso8601(5) ) expect( textdec_timestampul.decode('2117-01-02 23:23:59.123456').iso8601(5) ). to eq( Time.utc(2117,01,02, 23, 23, 59.123456).getlocal.iso8601(5) ) expect( textdec_timestamptz.decode('2117-01-02 23:23:59.123456+04').iso8601(5) ). to eq( Time.new(2117,01,02, 23, 23, 59.123456, "+04:00").iso8601(5) ) end it 'decodes timestamps with variable number of digits for the useconds part' do sec = "59.12345678912345" (4..sec.length).each do |i| expect( textdec_timestamp.decode("2016-01-02 23:23:#{sec[0,i]}") ). to be_within(0.000001).of( Time.new(2016,01,02, 23, 23, sec[0,i].to_f) ) end end it 'decodes timestamps with leap-second' do expect( textdec_timestamp.decode('1998-12-31 23:59:60.1234') ). to be_within(0.000001).of( Time.new(1998,12,31, 23, 59, 60.1234) ) end def textdec_timestamptz_decode_should_fail(str) expect(textdec_timestamptz.decode(str)).to eq(str) end it 'fails when the timestamp is an empty string' do textdec_timestamptz_decode_should_fail('') end it 'fails when the timestamp contains values with less digits than expected' do textdec_timestamptz_decode_should_fail('2016-0-02 23:23:59.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-0 23:23:59.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 2:23:59.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:2:59.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:23:5.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:23:59.+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:23:59.123456+0:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:23:59.123456+00:2:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:23:59.123456+00:25:2') end it 'fails when the timestamp contains values with more digits than expected' do textdec_timestamptz_decode_should_fail('2016-011-02 23:23:59.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-022 23:23:59.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 233:23:59.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:233:59.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:23:599.123456+00:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:23:59.123456+000:25:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:23:59.123456+00:255:21') textdec_timestamptz_decode_should_fail('2016-01-02 23:23:59.123456+00:25:211') end it 'fails when the timestamp contains values with invalid characters' do str = '2013-01-02 23:23:59.123456+00:25:21' str.length.times do |i| textdec_timestamptz_decode_should_fail(str[0,i] + "x" + str[i+1..-1]) end end it 'fails when the timestamp contains leading characters' do textdec_timestamptz_decode_should_fail(' 2016-01-02 23:23:59.123456') end it 'fails when the timestamp contains trailing characters' do textdec_timestamptz_decode_should_fail('2016-01-02 23:23:59.123456 ') end it 'fails when the timestamp contains non ASCII character' do textdec_timestamptz_decode_should_fail('2016-01ª02 23:23:59.123456') end end context 'identifier quotation' do it 'should build an array out of an quoted identifier string' do quoted_type = PG::TextDecoder::Identifier.new expect( quoted_type.decode(%["A.".".B"]) ).to eq( ["A.", ".B"] ) expect( quoted_type.decode(%["'A"".""B'"]) ).to eq( ['\'A"."B\''] ) end it 'should split unquoted identifier string' do quoted_type = PG::TextDecoder::Identifier.new expect( quoted_type.decode(%[a.b]) ).to eq( ['a','b'] ) expect( quoted_type.decode(%[a]) ).to eq( ['a'] ) end it 'should split identifier string with correct character encoding' do quoted_type = PG::TextDecoder::Identifier.new v = quoted_type.decode(%[Héllo].encode("iso-8859-1")).first expect( v.encoding ).to eq( Encoding::ISO_8859_1 ) expect( v ).to eq( %[Héllo].encode(Encoding::ISO_8859_1) ) end end it "should raise when decode method is called with wrong args" do expect{ textdec_int.decode() }.to raise_error(ArgumentError) expect{ textdec_int.decode("123", 2, 3, 4) }.to raise_error(ArgumentError) expect{ textdec_int.decode(2, 3, 4) }.to raise_error(TypeError) expect( intdec_incrementer.decode(2, 3, 4) ).to eq( 3 ) end it "should pass through nil values" do expect( textdec_string.decode( nil )).to be_nil expect( textdec_int.decode( nil )).to be_nil end it "should be defined on an encoder but not on a decoder instance" do expect( textdec_int.respond_to?(:decode) ).to be_truthy expect( textenc_int.respond_to?(:decode) ).to be_falsey end end describe '#encode' do it "should offer encode method for text type" do res = textenc_int.encode(123) expect( res ).to eq( "123" ) end it "should offer encode method for binary type" do res = binaryenc_int8.encode(123) expect( res ).to eq( [123].pack("q>") ) end it "should encode integers from string to binary format" do expect( binaryenc_int2.encode(" -123 ") ).to eq( [-123].pack("s>") ) expect( binaryenc_int4.encode(" -123 ") ).to eq( [-123].pack("l>") ) expect( binaryenc_int8.encode(" -123 ") ).to eq( [-123].pack("q>") ) expect( binaryenc_int2.encode(" 123-xyz ") ).to eq( [123].pack("s>") ) expect( binaryenc_int4.encode(" 123-xyz ") ).to eq( [123].pack("l>") ) expect( binaryenc_int8.encode(" 123-xyz ") ).to eq( [123].pack("q>") ) end it "should encode integers of different lengths to text format" do 30.times do |zeros| expect( textenc_int.encode(10 ** zeros) ).to eq( "1" + "0"*zeros ) expect( textenc_int.encode(10 ** zeros - 1) ).to eq( zeros==0 ? "0" : "9"*zeros ) expect( textenc_int.encode(-10 ** zeros) ).to eq( "-1" + "0"*zeros ) expect( textenc_int.encode(-10 ** zeros + 1) ).to eq( zeros==0 ? "0" : "-" + "9"*zeros ) end 66.times do |bits| expect( textenc_int.encode(2 ** bits) ).to eq( (2 ** bits).to_s ) expect( textenc_int.encode(2 ** bits - 1) ).to eq( (2 ** bits - 1).to_s ) expect( textenc_int.encode(-2 ** bits) ).to eq( (-2 ** bits).to_s ) expect( textenc_int.encode(-2 ** bits + 1) ).to eq( (-2 ** bits + 1).to_s ) end end it "should encode integers from string to text format" do expect( textenc_int.encode(" -123 ") ).to eq( "-123" ) expect( textenc_int.encode(" 123-xyz ") ).to eq( "123" ) end it "should encode boolean values" do expect( textenc_boolean.encode(false) ).to eq( "f" ) expect( textenc_boolean.encode(true) ).to eq( "t" ) ["any", :other, "value", 0, 1, 2].each do |value| expect( textenc_boolean.encode(value) ).to eq( value.to_s ) end end it "should encode floats" do expect( textenc_float.encode(0) ).to eq( "0.0" ) expect( textenc_float.encode(-1) ).to eq( "-1.0" ) expect( textenc_float.encode(-1.234567890123456789) ).to eq( "-1.234567890123457" ) expect( textenc_float.encode(9) ).to eq( "9.0" ) expect( textenc_float.encode(10) ).to eq( "10.0" ) expect( textenc_float.encode(-99) ).to eq( "-99.0" ) expect( textenc_float.encode(-100) ).to eq( "-100.0" ) expect( textenc_float.encode(999) ).to eq( "999.0" ) expect( textenc_float.encode(-1000) ).to eq( "-1000.0" ) expect( textenc_float.encode(1234.567890123456789) ).to eq( "1234.567890123457" ) expect( textenc_float.encode(-9999) ).to eq( "-9999.0" ) expect( textenc_float.encode(10000) ).to eq( "10000.0" ) expect( textenc_float.encode(99999) ).to eq( "99999.0" ) expect( textenc_float.encode(-100000) ).to eq( "-100000.0" ) expect( textenc_float.encode(-999999) ).to eq( "-999999.0" ) expect( textenc_float.encode(1000000) ).to eq( "1000000.0" ) expect( textenc_float.encode(9999999) ).to eq( "9999999.0" ) expect( textenc_float.encode(-100000000000000) ).to eq( "-100000000000000.0" ) expect( textenc_float.encode(123456789012345) ).to eq( "123456789012345.0" ) expect( textenc_float.encode(-999999999999999) ).to eq( "-999999999999999.0" ) expect( textenc_float.encode(1000000000000000) ).to eq( "1e15" ) expect( textenc_float.encode(-1234567890123456) ).to eq( "-1.234567890123456e15" ) expect( textenc_float.encode(9999999999999999) ).to eq( "1e16" ) expect( textenc_float.encode(-0.0) ).to eq( "0.0" ) expect( textenc_float.encode(0.1) ).to eq( "0.1" ) expect( textenc_float.encode(0.1234567890123456789) ).to eq( "0.1234567890123457" ) expect( textenc_float.encode(-0.9) ).to eq( "-0.9" ) expect( textenc_float.encode(-0.01234567890123456789) ).to eq( "-0.01234567890123457" ) expect( textenc_float.encode(0.09) ).to eq( "0.09" ) expect( textenc_float.encode(0.001234567890123456789) ).to eq( "0.001234567890123457" ) expect( textenc_float.encode(-0.009) ).to eq( "-0.009" ) expect( textenc_float.encode(-0.0001234567890123456789) ).to eq( "-0.0001234567890123457" ) expect( textenc_float.encode(0.0009) ).to eq( "0.0009" ) expect( textenc_float.encode(0.00001) ).to eq( "1e-5" ) expect( textenc_float.encode(0.00001234567890123456789) ).to eq( "1.234567890123457e-5" ) expect( textenc_float.encode(-0.00009) ).to eq( "-9e-5" ) expect( textenc_float.encode(-0.11) ).to eq( "-0.11" ) expect( textenc_float.encode(10.11) ).to eq( "10.11" ) expect( textenc_float.encode(-1.234567890123456789E-280) ).to eq( "-1.234567890123457e-280" ) expect( textenc_float.encode(-1.234567890123456789E280) ).to eq( "-1.234567890123457e280" ) expect( textenc_float.encode(9876543210987654321E280) ).to eq( "9.87654321098765e298" ) expect( textenc_float.encode(9876543210987654321E-400) ).to eq( "0.0" ) expect( textenc_float.encode(9876543210987654321E400) ).to eq( "Infinity" ) end it "should encode special floats equally to Float#to_s" do expect( textenc_float.encode(Float::INFINITY) ).to eq( Float::INFINITY.to_s ) expect( textenc_float.encode(-Float::INFINITY) ).to eq( (-Float::INFINITY).to_s ) expect( textenc_float.encode(-Float::NAN) ).to eq( Float::NAN.to_s ) end it "should encode various inputs to numeric format" do expect( textenc_numeric.encode(0) ).to eq( "0" ) expect( textenc_numeric.encode(1) ).to eq( "1" ) expect( textenc_numeric.encode(-12345678901234567890123) ).to eq( "-12345678901234567890123" ) expect( textenc_numeric.encode(0.0) ).to eq( "0.0" ) expect( textenc_numeric.encode(1.0) ).to eq( "1.0" ) expect( textenc_numeric.encode(-1.23456789012e45) ).to eq( "-1.23456789012e45" ) expect( textenc_numeric.encode(Float::NAN) ).to eq( Float::NAN.to_s ) expect( textenc_numeric.encode(BigDecimal(0)) ).to eq( "0.0" ) expect( textenc_numeric.encode(BigDecimal(1)) ).to eq( "1.0" ) expect( textenc_numeric.encode(BigDecimal("-12345678901234567890.1234567")) ).to eq( "-12345678901234567890.1234567" ) expect( textenc_numeric.encode(" 123 ") ).to eq( " 123 " ) end it "encodes binary string to bytea" do expect( textenc_bytea.encode("\x00\x01\x02\x03\xef".b) ).to eq( "\\x00010203ef" ) end context 'timestamps' do it 'encodes timestamps without timezone' do expect( textenc_timestamp.encode(Time.new(2016,1,2, 23, 23, 59.123456, 3*60*60)) ). to match( /^2016-01-02 23:23:59.12345\d+$/ ) expect( textenc_timestamp.encode(Time.new(2016,8,2, 23, 23, 59.123456, 3*60*60)) ). to match( /^2016-08-02 23:23:59.12345\d+$/ ) end it 'encodes timestamps with UTC timezone' do expect( textenc_timestamputc.encode(Time.new(2016,1,2, 23, 23, 59.123456, 3*60*60)) ). to match( /^2016-01-02 20:23:59.12345\d+$/ ) expect( textenc_timestamputc.encode(Time.new(2016,8,2, 23, 23, 59.123456, 3*60*60)) ). to match( /^2016-08-02 20:23:59.12345\d+$/ ) end it 'encodes timestamps with hour timezone' do expect( textenc_timestamptz.encode(Time.new(2016,1,02, 23, 23, 59.123456, -4*60*60)) ). to match( /^2016-01-02 23:23:59.12345\d+ \-04:00$/ ) expect( textenc_timestamptz.encode(Time.new(2016,8,02, 23, 23, 59.123456, 10*60*60)) ). to match( /^2016-08-02 23:23:59.12345\d+ \+10:00$/ ) end end context 'identifier quotation' do it 'should quote and escape identifier' do quoted_type = PG::TextEncoder::Identifier.new expect( quoted_type.encode(['schema','table','col']) ).to eq( %["schema"."table"."col"] ) expect( quoted_type.encode(['A.','.B']) ).to eq( %["A.".".B"] ) expect( quoted_type.encode(%['A"."B']) ).to eq( %["'A"".""B'"] ) expect( quoted_type.encode( nil ) ).to be_nil end it 'should quote identifiers with correct character encoding' do quoted_type = PG::TextEncoder::Identifier.new v = quoted_type.encode(['Héllo'], "iso-8859-1") expect( v ).to eq( %["Héllo"].encode(Encoding::ISO_8859_1) ) expect( v.encoding ).to eq( Encoding::ISO_8859_1 ) end it "will raise a TypeError for invalid arguments to quote_ident" do quoted_type = PG::TextEncoder::Identifier.new expect{ quoted_type.encode( [nil] ) }.to raise_error(TypeError) expect{ quoted_type.encode( [['a']] ) }.to raise_error(TypeError) end end it "should encode with ruby encoder" do expect( intenc_incrementer.encode(3) ).to eq( "4 " ) end it "should encode with ruby encoder and given character encoding" do r = intenc_incrementer_with_encoding.encode(3, Encoding::CP850) expect( r ).to eq( "4 CP850" ) expect( r.encoding ).to eq( Encoding::CP850 ) end it "should return when ruby encoder returns non string values" do expect( intenc_incrementer_with_int_result.encode(3) ).to eq( 4 ) end it "should pass through nil values" do expect( textenc_string.encode( nil )).to be_nil expect( textenc_int.encode( nil )).to be_nil end it "should be defined on a decoder but not on an encoder instance" do expect( textenc_int.respond_to?(:encode) ).to be_truthy expect( textdec_int.respond_to?(:encode) ).to be_falsey end end it "should be possible to marshal encoders" do mt = Marshal.dump(textenc_int) lt = Marshal.load(mt) expect( lt.to_h ).to eq( textenc_int.to_h ) end it "should be possible to marshal decoders" do mt = Marshal.dump(textdec_int) lt = Marshal.load(mt) expect( lt.to_h ).to eq( textdec_int.to_h ) end it "should respond to to_h" do expect( textenc_int.to_h ).to eq( { name: 'Integer', oid: 23, format: 0, flags: 0 } ) end it "should have reasonable default values" do t = PG::TextEncoder::String.new expect( t.format ).to eq( 0 ) expect( t.oid ).to eq( 0 ) expect( t.name ).to be_nil t = PG::BinaryEncoder::Int4.new expect( t.format ).to eq( 1 ) expect( t.oid ).to eq( 0 ) expect( t.name ).to be_nil t = PG::TextDecoder::String.new expect( t.format ).to eq( 0 ) expect( t.oid ).to eq( 0 ) expect( t.name ).to be_nil t = PG::BinaryDecoder::String.new expect( t.format ).to eq( 1 ) expect( t.oid ).to eq( 0 ) expect( t.name ).to be_nil end end describe PG::CompositeCoder do describe "Array types" do let!(:textenc_string_array) { PG::TextEncoder::Array.new elements_type: textenc_string } let!(:textdec_string_array) { PG::TextDecoder::Array.new elements_type: textdec_string } let!(:textdec_string_array_raise) { PG::TextDecoder::Array.new elements_type: textdec_string, flags: PG::Coder:: FORMAT_ERROR_TO_RAISE } let!(:textenc_int_array) { PG::TextEncoder::Array.new elements_type: textenc_int, needs_quotation: false } let!(:textdec_int_array) { PG::TextDecoder::Array.new elements_type: textdec_int, needs_quotation: false } let!(:textenc_float_array) { PG::TextEncoder::Array.new elements_type: textenc_float, needs_quotation: false } let!(:textdec_float_array) { PG::TextDecoder::Array.new elements_type: textdec_float, needs_quotation: false } let!(:textenc_timestamp_array) { PG::TextEncoder::Array.new elements_type: textenc_timestamp, needs_quotation: false } let!(:textdec_timestamp_array) { PG::TextDecoder::Array.new elements_type: textdec_timestamp, needs_quotation: false } let!(:textenc_string_array_with_delimiter) { PG::TextEncoder::Array.new elements_type: textenc_string, delimiter: ';' } let!(:textdec_string_array_with_delimiter) { PG::TextDecoder::Array.new elements_type: textdec_string, delimiter: ';' } let!(:textdec_bytea_array) { PG::TextDecoder::Array.new elements_type: textdec_bytea } # # Array parser specs are thankfully borrowed from here: # https://github.com/dockyard/pg_array_parser # describe '#decode' do context 'one dimensional arrays' do context 'empty' do it 'returns an empty array' do expect( textdec_string_array.decode(%[{}]) ).to eq( [] ) end end context 'no strings' do it 'returns an array of strings' do expect( textdec_string_array.decode(%[{1,2,3}]) ).to eq( ['1','2','3'] ) end end context 'NULL values' do it 'returns an array of strings, with nils replacing NULL characters' do expect( textdec_string_array.decode(%[{1,NULL,NULL}]) ).to eq( ['1',nil,nil] ) end end context 'quoted NULL' do it 'returns an array with the word NULL' do expect( textdec_string_array.decode(%[{1,"NULL",3}]) ).to eq( ['1','NULL','3'] ) end end context 'strings' do it 'returns an array of strings when containing commas in a quoted string' do expect( textdec_string_array.decode(%[{1,"2,3",4}]) ).to eq( ['1','2,3','4'] ) end it 'returns an array of strings when containing an escaped quote' do expect( textdec_string_array.decode(%[{1,"2\\",3",4}]) ).to eq( ['1','2",3','4'] ) end it 'returns an array of strings when containing an escaped backslash' do expect( textdec_string_array.decode(%[{1,"2\\\\",3,4}]) ).to eq( ['1','2\\','3','4'] ) expect( textdec_string_array.decode(%[{1,"2\\\\\\",3",4}]) ).to eq( ['1','2\\",3','4'] ) end it 'returns an array containing empty strings' do expect( textdec_string_array.decode(%[{1,"",3,""}]) ).to eq( ['1', '', '3', ''] ) end it 'returns an array containing unicode strings' do expect( textdec_string_array.decode(%[{"Paragraph 399(b)(i) – “valid leave” – meaning"}]) ).to eq(['Paragraph 399(b)(i) – “valid leave” – meaning']) end it 'respects a different delimiter' do expect( textdec_string_array_with_delimiter.decode(%[{1;2;3}]) ).to eq( ['1','2','3'] ) end it 'ignores array dimensions' do expect( textdec_string_array.decode(%[[2:4]={1,2,3}]) ).to eq( ['1','2','3'] ) expect( textdec_string_array.decode(%[[]={1,2,3}]) ).to eq( ['1','2','3'] ) expect( textdec_string_array.decode(%[ [-1:+2]= {4,3,2,1}]) ).to eq( ['4','3','2','1'] ) end it 'ignores spaces after array' do expect( textdec_string_array.decode(%[[2:4]={1,2,3} ]) ).to eq( ['1','2','3'] ) expect( textdec_string_array.decode(%[{1,2,3} ]) ).to eq( ['1','2','3'] ) end describe "with malformed syntax are deprecated" do it 'accepts broken array dimensions' do expect( textdec_string_array.decode(%([2:4={1,2,3})) ).to eq([['1','2','3']]) expect( textdec_string_array.decode(%(2:4]={1,2,3})) ).to eq([['1','2','3']]) expect( textdec_string_array.decode(%(={1,2,3})) ).to eq([['1','2','3']]) expect( textdec_string_array.decode(%([x]={1,2,3})) ).to eq([['1','2','3']]) expect( textdec_string_array.decode(%([]{1,2,3})) ).to eq([['1','2','3']]) expect( textdec_string_array.decode(%(1,2,3)) ).to eq(['','2']) end it 'accepts malformed arrays' do expect( textdec_string_array.decode(%({1,2,3)) ).to eq(['1','2']) expect( textdec_string_array.decode(%({1,2,3}})) ).to eq(['1','2','3']) expect( textdec_string_array.decode(%({1,2,3}x)) ).to eq(['1','2','3']) expect( textdec_string_array.decode(%({{1,2},{2,3})) ).to eq([['1','2'],['2','3']]) expect( textdec_string_array.decode(%({{1,2},{2,3}}x)) ).to eq([['1','2'],['2','3']]) expect( textdec_string_array.decode(%({[1,2},{2,3}}})) ).to eq(['[1','2']) end end describe "with malformed syntax are raised with pg-2.0+" do it 'complains about broken array dimensions' do expect{ textdec_string_array_raise.decode(%([2:4={1,2,3})) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%(2:4]={1,2,3})) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%(={1,2,3})) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%([x]={1,2,3})) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%([]{1,2,3})) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%(1,2,3)) }.to raise_error(TypeError) end it 'complains about malformed array' do expect{ textdec_string_array_raise.decode(%({1,2,3)) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%({1,2,3}})) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%({1,2,3}x)) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%({{1,2},{2,3})) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%({{1,2},{2,3}}x)) }.to raise_error(TypeError) expect{ textdec_string_array_raise.decode(%({[1,2},{2,3}}})) }.to raise_error(TypeError) end end end context 'bytea' do it 'returns an array of binary strings' do expect( textdec_bytea_array.decode(%[{"\\\\x00010203EF","2,3",\\377}]) ).to eq( ["\x00\x01\x02\x03\xef".b,"2,3".b,"\xff".b] ) end end end context 'two dimensional arrays' do context 'empty' do it 'returns an empty array' do expect( textdec_string_array.decode(%[{{}}]) ).to eq( [[]] ) expect( textdec_string_array.decode(%[{{},{}}]) ).to eq( [[],[]] ) end end context 'no strings' do it 'returns an array of strings with a sub array' do expect( textdec_string_array.decode(%[{1,{2,3},4}]) ).to eq( ['1',['2','3'],'4'] ) end end context 'strings' do it 'returns an array of strings with a sub array' do expect( textdec_string_array.decode(%[{1,{"2,3"},4}]) ).to eq( ['1',['2,3'],'4'] ) end it 'returns an array of strings with a sub array and a quoted }' do expect( textdec_string_array.decode(%[{1,{"2,}3",NULL},4}]) ).to eq( ['1',['2,}3',nil],'4'] ) end it 'returns an array of strings with a sub array and a quoted {' do expect( textdec_string_array.decode(%[{1,{"2,{3"},4}]) ).to eq( ['1',['2,{3'],'4'] ) end it 'returns an array of strings with a sub array and a quoted { and escaped quote' do expect( textdec_string_array.decode(%[{1,{"2\\",{3"},4}]) ).to eq( ['1',['2",{3'],'4'] ) end it 'returns an array of strings with a sub array with empty strings' do expect( textdec_string_array.decode(%[{1,{""},4,{""}}]) ).to eq( ['1',[''],'4',['']] ) end end context 'timestamps' do it 'decodes an array of timestamps with sub arrays' do expect( textdec_timestamp_array.decode('{2014-12-31 00:00:00,{NULL,2016-01-02 23:23:59.0000000}}') ). to eq( [Time.new(2014,12,31),[nil, Time.new(2016,01,02, 23, 23, 59)]] ) end end end context 'three dimensional arrays' do context 'empty' do it 'returns an empty array' do expect( textdec_string_array.decode(%[{{{}}}]) ).to eq( [[[]]] ) expect( textdec_string_array.decode(%[{{{},{}},{{},{}}}]) ).to eq( [[[],[]],[[],[]]] ) end end it 'returns an array of strings with sub arrays' do expect( textdec_string_array.decode(%[{1,{2,{3,4}},{NULL,6},7}]) ).to eq( ['1',['2',['3','4']],[nil,'6'],'7'] ) end end it 'should decode array of types with decoder in ruby space' do array_type = PG::TextDecoder::Array.new elements_type: intdec_incrementer expect( array_type.decode(%[{3,4}]) ).to eq( [4,5] ) end it 'should decode array of nil types' do array_type = PG::TextDecoder::Array.new elements_type: nil expect( array_type.decode(%[{3,4}]) ).to eq( ['3','4'] ) end end describe '#encode' do context 'three dimensional arrays' do it 'encodes an array of strings and numbers with sub arrays' do expect( textenc_string_array.encode(['1',['2',['3','4']],[nil,6],7.8]) ).to eq( %[{1,{2,{3,4}},{NULL,6},7.8}] ) end it 'encodes an array of strings with quotes' do expect( textenc_string_array.encode(['',[' ',['{','}','\\',',','"','\t']]]) ).to eq( %[{"",{" ",{"{","}","\\\\",",","\\"","\\\\t"}}}] ) end it 'encodes an array of int8 with sub arrays' do expect( textenc_int_array.encode([1,[2,[3,4]],[nil,6],7]) ).to eq( %[{1,{2,{3,4}},{NULL,6},7}] ) end it 'encodes an array of int8 with strings' do expect( textenc_int_array.encode(['1',['2'],'3']) ).to eq( %[{1,{2},3}] ) end it 'encodes an array of float8 with sub arrays' do expect( textenc_float_array.encode([1000.11,[-0.00000221,[3.31,-441]],[nil,6.61],-7.71]) ).to match(Regexp.new(%[^{1000.1*,{-2.2*e-*6,{3.3*,-441.0}},{NULL,6.6*},-7.7*}$].gsub(/([\.\+\{\}\,])/, "\\\\\\1").gsub(/\*/, "\\d*"))) end end context 'two dimensional arrays' do it 'encodes an array of timestamps with sub arrays' do expect( textenc_timestamp_array.encode([Time.new(2014,12,31),[nil, Time.new(2016,01,02, 23, 23, 59.99)]]) ). to eq( %[{2014-12-31 00:00:00.000000000,{NULL,2016-01-02 23:23:59.990000000}}] ) end end context 'one dimensional array' do it 'can encode empty arrays' do expect( textenc_int_array.encode([]) ).to eq( '{}' ) expect( textenc_string_array.encode([]) ).to eq( '{}' ) end it 'encodes an array of NULL strings w/wo quotes' do expect( textenc_string_array.encode(['NUL', 'NULL', 'NULLL', 'nul', 'null', 'nulll']) ).to eq( %[{NUL,"NULL",NULLL,nul,"null",nulll}] ) end it 'respects a different delimiter' do expect( textenc_string_array_with_delimiter.encode(['a','b,','c']) ).to eq( '{a;b,;c}' ) end end context 'array of types with encoder in ruby space' do it 'encodes with quotation and default character encoding' do array_type = PG::TextEncoder::Array.new elements_type: intenc_incrementer, needs_quotation: true r = array_type.encode([3,4]) expect( r ).to eq( %[{"4 ","5 "}] ) expect( r.encoding ).to eq( Encoding::ASCII_8BIT ) end it 'encodes with quotation and given character encoding' do array_type = PG::TextEncoder::Array.new elements_type: intenc_incrementer, needs_quotation: true r = array_type.encode([3,4], Encoding::CP850) expect( r ).to eq( %[{"4 ","5 "}] ) expect( r.encoding ).to eq( Encoding::CP850 ) end it 'encodes without quotation' do array_type = PG::TextEncoder::Array.new elements_type: intenc_incrementer, needs_quotation: false expect( array_type.encode([3,4]) ).to eq( %[{4 ,5 }] ) end it 'encodes with default character encoding' do array_type = PG::TextEncoder::Array.new elements_type: intenc_incrementer_with_encoding r = array_type.encode([3,4]) expect( r ).to eq( %[{"4 ASCII-8BIT","5 ASCII-8BIT"}] ) expect( r.encoding ).to eq( Encoding::ASCII_8BIT ) end it 'encodes with given character encoding' do array_type = PG::TextEncoder::Array.new elements_type: intenc_incrementer_with_encoding r = array_type.encode([3,4], Encoding::CP850) expect( r ).to eq( %[{"4 CP850","5 CP850"}] ) expect( r.encoding ).to eq( Encoding::CP850 ) end it "should raise when ruby encoder returns non string values" do array_type = PG::TextEncoder::Array.new elements_type: intenc_incrementer_with_int_result, needs_quotation: false expect{ array_type.encode([3,4]) }.to raise_error(TypeError) end end it "should pass through non Array inputs" do expect( textenc_float_array.encode("text") ).to eq( "text" ) expect( textenc_float_array.encode(1234) ).to eq( "1234" ) end context 'literal quotation' do it 'should quote and escape literals' do quoted_type = PG::TextEncoder::QuotedLiteral.new elements_type: textenc_string_array expect( quoted_type.encode(["'A\",","\\B'"]) ).to eq( %['{"''A\\",","\\\\B''"}'] ) end it 'should quote literals with correct character encoding' do quoted_type = PG::TextEncoder::QuotedLiteral.new elements_type: textenc_string_array v = quoted_type.encode(["Héllo"], "iso-8859-1") expect( v.encoding ).to eq( Encoding::ISO_8859_1 ) expect( v ).to eq( %['{Héllo}'].encode(Encoding::ISO_8859_1) ) end end end it "should be possible to marshal encoders" do mt = Marshal.dump(textenc_int_array) lt = Marshal.load(mt) expect( lt.to_h ).to eq( textenc_int_array.to_h ) end it "should be possible to marshal decoders" do mt = Marshal.dump(textdec_string_array_raise) lt = Marshal.load(mt) expect( lt.to_h ).to eq( textdec_string_array_raise.to_h ) end it "should respond to to_h" do expect( textenc_int_array.to_h ).to eq( { name: nil, oid: 0, format: 0, flags: 0, elements_type: textenc_int, needs_quotation: false, delimiter: ',' } ) end it "shouldn't accept invalid elements_types" do expect{ PG::TextEncoder::Array.new elements_type: false }.to raise_error(TypeError) end it "should have reasonable default values" do t = PG::TextEncoder::Array.new expect( t.format ).to eq( 0 ) expect( t.oid ).to eq( 0 ) expect( t.name ).to be_nil expect( t.needs_quotation? ).to eq( true ) expect( t.delimiter ).to eq( ',' ) expect( t.elements_type ).to be_nil end end it "should encode Strings as base64 in TextEncoder" do e = PG::TextEncoder::ToBase64.new expect( e.encode("") ).to eq("") expect( e.encode("x") ).to eq("eA==") expect( e.encode("xx") ).to eq("eHg=") expect( e.encode("xxx") ).to eq("eHh4") expect( e.encode("xxxx") ).to eq("eHh4eA==") expect( e.encode("xxxxx") ).to eq("eHh4eHg=") expect( e.encode("\0\n\t") ).to eq("AAoJ") expect( e.encode("(\xFBm") ).to eq("KPtt") end it 'should encode Strings as base64 with correct character encoding' do e = PG::TextEncoder::ToBase64.new v = e.encode("Héllo".encode("utf-16le"), "iso-8859-1") expect( v ).to eq("SOlsbG8=") expect( v.encoding ).to eq(Encoding::ISO_8859_1) end it "should encode Strings as base64 in BinaryDecoder" do e = PG::BinaryDecoder::ToBase64.new expect( e.decode("x") ).to eq("eA==") v = e.decode("Héllo".encode("utf-16le")) expect( v ).to eq("SADpAGwAbABvAA==") expect( v.encoding ).to eq(Encoding::ASCII_8BIT) end it "should encode Integers as base64" do # Not really useful, but ensures that two-pass element and composite element encoders work. e = PG::TextEncoder::ToBase64.new( elements_type: PG::TextEncoder::Array.new( elements_type: PG::TextEncoder::Integer.new, needs_quotation: false )) expect( e.encode([1]) ).to eq(["{1}"].pack("m").chomp) expect( e.encode([12]) ).to eq(["{12}"].pack("m").chomp) expect( e.encode([123]) ).to eq(["{123}"].pack("m").chomp) expect( e.encode([1234]) ).to eq(["{1234}"].pack("m").chomp) expect( e.encode([12345]) ).to eq(["{12345}"].pack("m").chomp) expect( e.encode([123456]) ).to eq(["{123456}"].pack("m").chomp) expect( e.encode([1234567]) ).to eq(["{1234567}"].pack("m").chomp) end it "should decode base64 to Strings in TextDecoder" do e = PG::TextDecoder::FromBase64.new expect( e.decode("") ).to eq("") expect( e.decode("eA==") ).to eq("x") expect( e.decode("eHg=") ).to eq("xx") expect( e.decode("eHh4") ).to eq("xxx") expect( e.decode("eHh4eA==") ).to eq("xxxx") expect( e.decode("eHh4eHg=") ).to eq("xxxxx") expect( e.decode("AAoJ") ).to eq("\0\n\t") expect( e.decode("KPtt") ).to eq("(\xFBm") end it "should decode base64 in BinaryEncoder" do e = PG::BinaryEncoder::FromBase64.new expect( e.encode("eA==") ).to eq("x") e = PG::BinaryEncoder::FromBase64.new( elements_type: PG::TextEncoder::Integer.new ) expect( e.encode(124) ).to eq("124=".unpack("m")[0]) end it "should decode base64 to Integers" do # Not really useful, but ensures that composite element encoders work. e = PG::TextDecoder::FromBase64.new( elements_type: PG::TextDecoder::Array.new( elements_type: PG::TextDecoder::Integer.new )) expect( e.decode(["{1}"].pack("m")) ).to eq([1]) expect( e.decode(["{12}"].pack("m")) ).to eq([12]) expect( e.decode(["{123}"].pack("m")) ).to eq([123]) expect( e.decode(["{1234}"].pack("m")) ).to eq([1234]) expect( e.decode(["{12345}"].pack("m")) ).to eq([12345]) expect( e.decode(["{123456}"].pack("m")) ).to eq([123456]) expect( e.decode(["{1234567}"].pack("m")) ).to eq([1234567]) expect( e.decode(["{12345678}"].pack("m")) ).to eq([12345678]) e = PG::TextDecoder::FromBase64.new( elements_type: PG::BinaryDecoder::Integer.new ) expect( e.decode("ALxhTg==") ).to eq(12345678) end it "should decode base64 with garbage" do e = PG::TextDecoder::FromBase64.new format: 1 expect( e.decode("=") ).to eq("=".unpack("m")[0]) expect( e.decode("==") ).to eq("==".unpack("m")[0]) expect( e.decode("===") ).to eq("===".unpack("m")[0]) expect( e.decode("====") ).to eq("====".unpack("m")[0]) expect( e.decode("a=") ).to eq("a=".unpack("m")[0]) expect( e.decode("a==") ).to eq("a==".unpack("m")[0]) expect( e.decode("a===") ).to eq("a===".unpack("m")[0]) expect( e.decode("a====") ).to eq("a====".unpack("m")[0]) expect( e.decode("aa=") ).to eq("aa=".unpack("m")[0]) expect( e.decode("aa==") ).to eq("aa==".unpack("m")[0]) expect( e.decode("aa===") ).to eq("aa===".unpack("m")[0]) expect( e.decode("aa====") ).to eq("aa====".unpack("m")[0]) expect( e.decode("aaa=") ).to eq("aaa=".unpack("m")[0]) expect( e.decode("aaa==") ).to eq("aaa==".unpack("m")[0]) expect( e.decode("aaa===") ).to eq("aaa===".unpack("m")[0]) expect( e.decode("aaa====") ).to eq("aaa====".unpack("m")[0]) expect( e.decode("=aa") ).to eq("=aa=".unpack("m")[0]) expect( e.decode("=aa=") ).to eq("=aa=".unpack("m")[0]) expect( e.decode("=aa==") ).to eq("=aa==".unpack("m")[0]) expect( e.decode("=aa===") ).to eq("=aa===".unpack("m")[0]) end end describe PG::CopyCoder do describe PG::TextEncoder::CopyRow do context "with default typemap" do let!(:encoder) do PG::TextEncoder::CopyRow.new end it "should encode different types of Ruby objects" do expect( encoder.encode([:xyz, 123, 2456, 34567, 456789, 5678901, [1,2,3], 12.1, "abcdefg", nil]) ). to eq("xyz\t123\t2456\t34567\t456789\t5678901\t[1, 2, 3]\t12.1\tabcdefg\t\\N\n") end it 'should output a string with correct character encoding' do v = encoder.encode(["Héllo"], "iso-8859-1") expect( v.encoding ).to eq( Encoding::ISO_8859_1 ) expect( v ).to eq( "Héllo\n".encode(Encoding::ISO_8859_1) ) end end context "with TypeMapByClass" do let!(:tm) do tm = PG::TypeMapByClass.new tm[Integer] = textenc_int tm[Float] = intenc_incrementer tm[Array] = PG::TextEncoder::Array.new elements_type: textenc_string tm end let!(:encoder) do PG::TextEncoder::CopyRow.new type_map: tm end it "should have reasonable default values" do expect( encoder.name ).to be_nil expect( encoder.delimiter ).to eq( "\t" ) expect( encoder.null_string ).to eq( "\\N" ) end it "copies all attributes with #dup" do encoder.name = "test" encoder.delimiter = "#" encoder.null_string = "NULL" encoder.type_map = PG::TypeMapByColumn.new [] encoder2 = encoder.dup expect( encoder.object_id ).to_not eq( encoder2.object_id ) expect( encoder2.name ).to eq( "test" ) expect( encoder2.delimiter ).to eq( "#" ) expect( encoder2.null_string ).to eq( "NULL" ) expect( encoder2.type_map ).to be_a_kind_of( PG::TypeMapByColumn ) end describe '#encode' do it "should encode different types of Ruby objects" do expect( encoder.encode([]) ).to eq("\n") expect( encoder.encode(["a"]) ).to eq("a\n") expect( encoder.encode([:xyz, 123, 2456, 34567, 456789, 5678901, [1,2,3], 12.1, "abcdefg", nil]) ). to eq("xyz\t123\t2456\t34567\t456789\t5678901\t{1,2,3}\t13 \tabcdefg\t\\N\n") end it "should escape special characters" do expect( encoder.encode([" \0\t\n\r\\"]) ).to eq(" \0#\t#\n#\r#\\\n".gsub("#", "\\")) end it "should escape with different delimiter" do encoder.delimiter = " " encoder.null_string = "NULL" expect( encoder.encode([nil, " ", "\0", "\t", "\n", "\r", "\\"]) ).to eq("NULL # \0 \t #\n #\r #\\\n".gsub("#", "\\")) end end end end describe PG::TextDecoder::CopyRow do context "with default typemap" do let!(:decoder) do PG::TextDecoder::CopyRow.new end describe '#decode' do it "should decode COPY text format to array of strings" do expect( decoder.decode("123\t \0#\t#\n#\r#\\ \t234\t#\x01#\002\n".gsub("#", "\\"))).to eq( ["123", " \0\t\n\r\\ ", "234", "\x01\x02"] ) end it 'should respect input character encoding' do v = decoder.decode("Héllo\n".encode("iso-8859-1")).first expect( v.encoding ).to eq(Encoding::ISO_8859_1) expect( v ).to eq("Héllo".encode("iso-8859-1")) end end end context "with TypeMapByColumn" do let!(:tm) do PG::TypeMapByColumn.new [textdec_int, textdec_string, intdec_incrementer, nil] end let!(:decoder) do PG::TextDecoder::CopyRow.new type_map: tm end describe '#decode' do it "should decode different types of Ruby objects" do expect( decoder.decode("123\t \0#\t#\n#\r#\\ \t234\t#\x01#\002\n".gsub("#", "\\"))).to eq( [123, " \0\t\n\r\\ ", 235, "\x01\x02"] ) end end end end end describe PG::RecordCoder do describe PG::TextEncoder::Record do context "with default typemap" do let!(:encoder) do PG::TextEncoder::Record.new end it "should encode different types of Ruby objects" do expect( encoder.encode([:xyz, 123, 2456, 34567, 456789, 5678901, [1,2,3], 12.1, "abcdefg", nil]) ). to eq('("xyz","123","2456","34567","456789","5678901","[1, 2, 3]","12.1","abcdefg",)') end it 'should output a string with correct character encoding' do v = encoder.encode(["Héllo"], "iso-8859-1") expect( v.encoding ).to eq( Encoding::ISO_8859_1 ) expect( v ).to eq( '("Héllo")'.encode(Encoding::ISO_8859_1) ) end end context "with TypeMapByClass" do let!(:tm) do tm = PG::TypeMapByClass.new tm[Integer] = textenc_int tm[Float] = intenc_incrementer tm[Array] = PG::TextEncoder::Array.new elements_type: textenc_string tm end let!(:encoder) do PG::TextEncoder::Record.new type_map: tm end it "should have reasonable default values" do expect( encoder.name ).to be_nil end it "copies all attributes with #dup" do encoder.name = "test" encoder.type_map = PG::TypeMapByColumn.new [] encoder2 = encoder.dup expect( encoder.object_id ).to_not eq( encoder2.object_id ) expect( encoder2.name ).to eq( "test" ) expect( encoder2.type_map ).to be_a_kind_of( PG::TypeMapByColumn ) end describe '#encode' do it "should encode different types of Ruby objects" do expect( encoder.encode([]) ).to eq("()") expect( encoder.encode(["a"]) ).to eq('("a")') expect( encoder.encode([:xyz, 123, 2456, 34567, 456789, 5678901, [1,2,3], 12.1, "abcdefg", nil]) ). to eq('("xyz","123","2456","34567","456789","5678901","{1,2,3}","13 ","abcdefg",)') end it "should escape special characters" do expect( encoder.encode([" \"\t\n\\\r"]) ).to eq("(\" \"\"\t\n##\r\")".gsub("#", "\\")) end end end end describe PG::TextDecoder::Record do context "with default typemap" do let!(:decoder) do PG::TextDecoder::Record.new end describe '#decode' do it "should decode composite text format to array of strings" do expect( decoder.decode('("fuzzy dice",,"",42,)') ).to eq( ["fuzzy dice",nil, "", "42", nil] ) end it 'should respect input character encoding' do v = decoder.decode("(Héllo)".encode("iso-8859-1")).first expect( v.encoding ).to eq(Encoding::ISO_8859_1) expect( v ).to eq("Héllo".encode("iso-8859-1")) end it 'should raise an error on malformed input' do expect{ decoder.decode('') }.to raise_error(ArgumentError, /"" - Missing left parenthesis/) expect{ decoder.decode('(') }.to raise_error(ArgumentError, /"\(" - Unexpected end of input/) expect{ decoder.decode('(\\') }.to raise_error(ArgumentError, /"\(\\" - Unexpected end of input/) expect{ decoder.decode('()x') }.to raise_error(ArgumentError, /"\(\)x" - Junk after right parenthesis/) end end end context "with TypeMapByColumn" do let!(:tm) do PG::TypeMapByColumn.new [textdec_int, textdec_string, intdec_incrementer, nil] end let!(:decoder) do PG::TextDecoder::Record.new type_map: tm end describe '#decode' do it "should decode different types of Ruby objects" do expect( decoder.decode("(123,\" #,#\n#\r#\\ \",234,#\x01#\002)".gsub("#", "\\"))).to eq( [123, " ,\n\r\\ ", 235, "\x01\x02"] ) end end end end end end pg-1.2.3/spec/pg/basic_type_mapping_spec.rb0000644000004100000410000005741513704151215020722 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative '../helpers' require 'pg' require 'time' def restore_type(types) [0, 1].each do |format| [types].flatten.each do |type| PG::BasicTypeRegistry.alias_type(format, "restore_#{type}", type) end end yield ensure [0, 1].each do |format| [types].flatten.each do |type| PG::BasicTypeRegistry.alias_type(format, type, "restore_#{type}") end end end describe 'Basic type mapping' do describe PG::BasicTypeMapForQueries do let!(:basic_type_mapping) do PG::BasicTypeMapForQueries.new @conn end # # Encoding Examples # it "should do basic param encoding" do res = @conn.exec_params( "SELECT $1::int8, $2::float, $3, $4::TEXT", [1, 2.1, true, "b"], nil, basic_type_mapping ) expect( res.values ).to eq( [ [ "1", "2.1", "t", "b" ], ] ) expect( result_typenames(res) ).to eq( ['bigint', 'double precision', 'boolean', 'text'] ) end it "should do basic Time encoding" do res = @conn.exec_params( "SELECT $1 AT TIME ZONE '-02'", [Time.new(2019, 12, 8, 20, 38, 12.123, "-01:00")], nil, basic_type_mapping ) expect( res.values ).to eq( [[ "2019-12-08 23:38:12.123" ]] ) end it "should do basic param encoding of various float values" do res = @conn.exec_params( "SELECT $1::float, $2::float, $3::float, $4::float, $5::float, $6::float, $7::float, $8::float, $9::float, $10::float, $11::float, $12::float", [0, 7, 9, 0.1, 0.9, -0.11, 10.11, 9876543210987654321e-400, 9876543210987654321e400, -1.234567890123456789e-280, -1.234567890123456789e280, 9876543210987654321e280 ], nil, basic_type_mapping ) expect( res.values[0][0, 9] ).to eq( [ "0", "7", "9", "0.1", "0.9", "-0.11", "10.11", "0", "Infinity" ] ) expect( res.values[0][9] ).to match( /^-1\.2345678901234\d*e\-280$/ ) expect( res.values[0][10] ).to match( /^-1\.2345678901234\d*e\+280$/ ) expect( res.values[0][11] ).to match( /^9\.8765432109876\d*e\+298$/ ) expect( result_typenames(res) ).to eq( ['double precision'] * 12 ) end it "should do default array-as-array param encoding" do expect( basic_type_mapping.encode_array_as).to eq(:array) res = @conn.exec_params( "SELECT $1,$2,$3,$4,$5,$6", [ [1, 2, 3], # Integer -> bigint[] [[1, 2], [3, nil]], # Integer two dimensions -> bigint[] [1.11, 2.21], # Float -> double precision[] ['/,"'.gsub("/", "\\"), nil, 'abcäöü'], # String -> text[] [BigDecimal("123.45")], # BigDecimal -> numeric[] [IPAddr.new('1234::5678')], # IPAddr -> inet[] ], nil, basic_type_mapping ) expect( res.values ).to eq( [[ '{1,2,3}', '{{1,2},{3,NULL}}', '{1.11,2.21}', '{"//,/"",NULL,abcäöü}'.gsub("/", "\\"), '{123.45}', '{1234::5678}', ]] ) expect( result_typenames(res) ).to eq( ['bigint[]', 'bigint[]', 'double precision[]', 'text[]', 'numeric[]', 'inet[]'] ) end it "should do default array-as-array param encoding with Time objects" do res = @conn.exec_params( "SELECT $1", [ [Time.new(2019, 12, 8, 20, 38, 12.123, "-01:00")], # Time -> timestamptz[] ], nil, basic_type_mapping ) expect( res.values[0][0] ).to match( /\{\"2019-12-08 \d\d:38:12.123[+-]\d\d\"\}/ ) expect( result_typenames(res) ).to eq( ['timestamp with time zone[]'] ) end it "should do array-as-json encoding" do basic_type_mapping.encode_array_as = :json expect( basic_type_mapping.encode_array_as).to eq(:json) res = @conn.exec_params( "SELECT $1::JSON, $2::JSON", [ [1, {a: 5}, true, ["a", 2], [3.4, nil]], ['/,"'.gsub("/", "\\"), nil, 'abcäöü'], ], nil, basic_type_mapping ) expect( res.values ).to eq( [[ '[1,{"a":5},true,["a",2],[3.4,null]]', '["//,/"",null,"abcäöü"]'.gsub("/", "\\"), ]] ) expect( result_typenames(res) ).to eq( ['json', 'json'] ) end it "should do hash-as-json encoding" do res = @conn.exec_params( "SELECT $1::JSON, $2::JSON", [ {a: 5, b: ["a", 2], c: nil}, {qu: '/,"'.gsub("/", "\\"), ni: nil, uml: 'abcäöü'}, ], nil, basic_type_mapping ) expect( res.values ).to eq( [[ '{"a":5,"b":["a",2],"c":null}', '{"qu":"//,/"","ni":null,"uml":"abcäöü"}'.gsub("/", "\\"), ]] ) expect( result_typenames(res) ).to eq( ['json', 'json'] ) end describe "Record encoding" do before :all do @conn.exec("CREATE TYPE test_record1 AS (i int, d float, t text)") @conn.exec("CREATE TYPE test_record2 AS (i int, r test_record1)") end after :all do @conn.exec("DROP TYPE IF EXISTS test_record2 CASCADE") @conn.exec("DROP TYPE IF EXISTS test_record1 CASCADE") end it "should do array-as-record encoding" do basic_type_mapping.encode_array_as = :record expect( basic_type_mapping.encode_array_as).to eq(:record) res = @conn.exec_params( "SELECT $1::test_record1, $2::test_record2, $3::text", [ [5, 3.4, "txt"], [1, [2, 4.5, "bcd"]], [4, 5, 6], ], nil, basic_type_mapping ) expect( res.values ).to eq( [[ '(5,3.4,txt)', '(1,"(2,4.5,bcd)")', '("4","5","6")', ]] ) expect( result_typenames(res) ).to eq( ['test_record1', 'test_record2', 'text'] ) end end it "should do bigdecimal param encoding" do large = ('123456790'*10) << '.' << ('012345679') res = @conn.exec_params( "SELECT $1::numeric,$2::numeric", [BigDecimal('1'), BigDecimal(large)], nil, basic_type_mapping ) expect( res.values ).to eq( [ [ "1.0", large ], ] ) expect( result_typenames(res) ).to eq( ['numeric', 'numeric'] ) end it "should do IPAddr param encoding" do res = @conn.exec_params( "SELECT $1::inet,$2::inet,$3::cidr,$4::cidr", ['1.2.3.4', IPAddr.new('1234::5678'), '1.2.3.4', IPAddr.new('1234:5678::/32')], nil, basic_type_mapping ) expect( res.values ).to eq( [ [ '1.2.3.4', '1234::5678', '1.2.3.4/32', '1234:5678::/32'], ] ) expect( result_typenames(res) ).to eq( ['inet', 'inet', 'cidr', 'cidr'] ) end it "should do array of string encoding on unknown classes" do iv = Class.new do def to_s "abc" end end.new res = @conn.exec_params( "SELECT $1", [ [iv, iv], # Unknown -> text[] ], nil, basic_type_mapping ) expect( res.values ).to eq( [[ '{abc,abc}', ]] ) expect( result_typenames(res) ).to eq( ['text[]'] ) end end describe PG::BasicTypeMapForResults do let!(:basic_type_mapping) do PG::BasicTypeMapForResults.new @conn end # # Decoding Examples # it "should do OID based type conversions" do res = @conn.exec( "SELECT 1, 'a', 2.0::FLOAT, TRUE, '2013-06-30'::DATE, generate_series(4,5)" ) expect( res.map_types!(basic_type_mapping).values ).to eq( [ [ 1, 'a', 2.0, true, Date.new(2013,6,30), 4 ], [ 1, 'a', 2.0, true, Date.new(2013,6,30), 5 ], ] ) end # # Decoding Examples text+binary format converters # describe "connection wide type mapping" do before :each do @conn.type_map_for_results = basic_type_mapping end after :each do @conn.type_map_for_results = PG::TypeMapAllStrings.new end it "should do boolean type conversions" do [1, 0].each do |format| res = @conn.exec_params( "SELECT true::BOOLEAN, false::BOOLEAN, NULL::BOOLEAN", [], format ) expect( res.values ).to eq( [[true, false, nil]] ) end end it "should do binary type conversions" do [1, 0].each do |format| res = @conn.exec_params( "SELECT E'\\\\000\\\\377'::BYTEA", [], format ) expect( res.values ).to eq( [[["00ff"].pack("H*")]] ) expect( res.values[0][0].encoding ).to eq( Encoding::ASCII_8BIT ) if Object.const_defined? :Encoding end end it "should do integer type conversions" do [1, 0].each do |format| res = @conn.exec_params( "SELECT -8999::INT2, -899999999::INT4, -8999999999999999999::INT8", [], format ) expect( res.values ).to eq( [[-8999, -899999999, -8999999999999999999]] ) end end it "should do string type conversions" do @conn.internal_encoding = 'utf-8' if Object.const_defined? :Encoding [1, 0].each do |format| res = @conn.exec_params( "SELECT 'abcäöü'::TEXT", [], format ) expect( res.values ).to eq( [['abcäöü']] ) expect( res.values[0][0].encoding ).to eq( Encoding::UTF_8 ) if Object.const_defined? :Encoding end end it "should do float type conversions" do [1, 0].each do |format| res = @conn.exec_params( "SELECT -8.999e3::FLOAT4, 8.999e10::FLOAT4, -8999999999e-99::FLOAT8, NULL::FLOAT4, 'NaN'::FLOAT4, 'Infinity'::FLOAT4, '-Infinity'::FLOAT4 ", [], format ) expect( res.getvalue(0,0) ).to be_within(1e-2).of(-8.999e3) expect( res.getvalue(0,1) ).to be_within(1e5).of(8.999e10) expect( res.getvalue(0,2) ).to be_within(1e-109).of(-8999999999e-99) expect( res.getvalue(0,3) ).to be_nil expect( res.getvalue(0,4) ).to be_nan expect( res.getvalue(0,5) ).to eq( Float::INFINITY ) expect( res.getvalue(0,6) ).to eq( -Float::INFINITY ) end end it "should do text datetime without time zone type conversions" do # for backward compat text timestamps without time zone are treated as local times res = @conn.exec_params( "SELECT CAST('2013-12-31 23:58:59+02' AS TIMESTAMP WITHOUT TIME ZONE), CAST('1913-12-31 23:58:59.1231-03' AS TIMESTAMP WITHOUT TIME ZONE), CAST('4714-11-24 23:58:59.1231-03 BC' AS TIMESTAMP WITHOUT TIME ZONE), CAST('294276-12-31 23:58:59.1231-03' AS TIMESTAMP WITHOUT TIME ZONE), CAST('infinity' AS TIMESTAMP WITHOUT TIME ZONE), CAST('-infinity' AS TIMESTAMP WITHOUT TIME ZONE)", [], 0 ) expect( res.getvalue(0,0) ).to eq( Time.new(2013, 12, 31, 23, 58, 59) ) expect( res.getvalue(0,1).iso8601(3) ).to eq( Time.new(1913, 12, 31, 23, 58, 59.1231).iso8601(3) ) expect( res.getvalue(0,2).iso8601(3) ).to eq( Time.new(-4713, 11, 24, 23, 58, 59.1231).iso8601(3) ) expect( res.getvalue(0,3).iso8601(3) ).to eq( Time.new(294276, 12, 31, 23, 58, 59.1231).iso8601(3) ) expect( res.getvalue(0,4) ).to eq( 'infinity' ) expect( res.getvalue(0,5) ).to eq( '-infinity' ) end [1, 0].each do |format| it "should convert format #{format} timestamps per TimestampUtc" do restore_type("timestamp") do PG::BasicTypeRegistry.register_type 0, 'timestamp', nil, PG::TextDecoder::TimestampUtc @conn.type_map_for_results = PG::BasicTypeMapForResults.new(@conn) res = @conn.exec_params( "SELECT CAST('2013-07-31 23:58:59+02' AS TIMESTAMP WITHOUT TIME ZONE), CAST('1913-12-31 23:58:59.1231-03' AS TIMESTAMP WITHOUT TIME ZONE), CAST('4714-11-24 23:58:59.1231-03 BC' AS TIMESTAMP WITHOUT TIME ZONE), CAST('294276-12-31 23:58:59.1231-03' AS TIMESTAMP WITHOUT TIME ZONE), CAST('infinity' AS TIMESTAMP WITHOUT TIME ZONE), CAST('-infinity' AS TIMESTAMP WITHOUT TIME ZONE)", [], format ) expect( res.getvalue(0,0).iso8601(3) ).to eq( Time.utc(2013, 7, 31, 23, 58, 59).iso8601(3) ) expect( res.getvalue(0,1).iso8601(3) ).to eq( Time.utc(1913, 12, 31, 23, 58, 59.1231).iso8601(3) ) expect( res.getvalue(0,2).iso8601(3) ).to eq( Time.utc(-4713, 11, 24, 23, 58, 59.1231).iso8601(3) ) expect( res.getvalue(0,3).iso8601(3) ).to eq( Time.utc(294276, 12, 31, 23, 58, 59.1231).iso8601(3) ) expect( res.getvalue(0,4) ).to eq( 'infinity' ) expect( res.getvalue(0,5) ).to eq( '-infinity' ) end end end [1, 0].each do |format| it "should convert format #{format} timestamps per TimestampUtcToLocal" do restore_type("timestamp") do PG::BasicTypeRegistry.register_type 0, 'timestamp', nil, PG::TextDecoder::TimestampUtcToLocal PG::BasicTypeRegistry.register_type 1, 'timestamp', nil, PG::BinaryDecoder::TimestampUtcToLocal @conn.type_map_for_results = PG::BasicTypeMapForResults.new(@conn) res = @conn.exec_params( "SELECT CAST('2013-07-31 23:58:59+02' AS TIMESTAMP WITHOUT TIME ZONE), CAST('1913-12-31 23:58:59.1231-03' AS TIMESTAMP WITHOUT TIME ZONE), CAST('4714-11-24 23:58:59.1231-03 BC' AS TIMESTAMP WITHOUT TIME ZONE), CAST('294276-12-31 23:58:59.1231-03' AS TIMESTAMP WITHOUT TIME ZONE), CAST('infinity' AS TIMESTAMP WITHOUT TIME ZONE), CAST('-infinity' AS TIMESTAMP WITHOUT TIME ZONE)", [], format ) expect( res.getvalue(0,0).iso8601(3) ).to eq( Time.utc(2013, 7, 31, 23, 58, 59).getlocal.iso8601(3) ) expect( res.getvalue(0,1).iso8601(3) ).to eq( Time.utc(1913, 12, 31, 23, 58, 59.1231).getlocal.iso8601(3) ) expect( res.getvalue(0,2).iso8601(3) ).to eq( Time.utc(-4713, 11, 24, 23, 58, 59.1231).getlocal.iso8601(3) ) expect( res.getvalue(0,3).iso8601(3) ).to eq( Time.utc(294276, 12, 31, 23, 58, 59.1231).getlocal.iso8601(3) ) expect( res.getvalue(0,4) ).to eq( 'infinity' ) expect( res.getvalue(0,5) ).to eq( '-infinity' ) end end end [1, 0].each do |format| it "should convert format #{format} timestamps per TimestampLocal" do restore_type("timestamp") do PG::BasicTypeRegistry.register_type 0, 'timestamp', nil, PG::TextDecoder::TimestampLocal PG::BasicTypeRegistry.register_type 1, 'timestamp', nil, PG::BinaryDecoder::TimestampLocal @conn.type_map_for_results = PG::BasicTypeMapForResults.new(@conn) res = @conn.exec_params( "SELECT CAST('2013-07-31 23:58:59' AS TIMESTAMP WITHOUT TIME ZONE), CAST('1913-12-31 23:58:59.1231' AS TIMESTAMP WITHOUT TIME ZONE), CAST('4714-11-24 23:58:59.1231-03 BC' AS TIMESTAMP WITHOUT TIME ZONE), CAST('294276-12-31 23:58:59.1231+03' AS TIMESTAMP WITHOUT TIME ZONE), CAST('infinity' AS TIMESTAMP WITHOUT TIME ZONE), CAST('-infinity' AS TIMESTAMP WITHOUT TIME ZONE)", [], format ) expect( res.getvalue(0,0).iso8601(3) ).to eq( Time.new(2013, 7, 31, 23, 58, 59).iso8601(3) ) expect( res.getvalue(0,1).iso8601(3) ).to eq( Time.new(1913, 12, 31, 23, 58, 59.1231).iso8601(3) ) expect( res.getvalue(0,2).iso8601(3) ).to eq( Time.new(-4713, 11, 24, 23, 58, 59.1231).iso8601(3) ) expect( res.getvalue(0,3).iso8601(3) ).to eq( Time.new(294276, 12, 31, 23, 58, 59.1231).iso8601(3) ) expect( res.getvalue(0,4) ).to eq( 'infinity' ) expect( res.getvalue(0,5) ).to eq( '-infinity' ) end end end [0, 1].each do |format| it "should convert format #{format} timestamps with time zone" do res = @conn.exec_params( "SELECT CAST('2013-12-31 23:58:59+02' AS TIMESTAMP WITH TIME ZONE), CAST('1913-12-31 23:58:59.1231-03' AS TIMESTAMP WITH TIME ZONE), CAST('4714-11-24 23:58:59.1231-03 BC' AS TIMESTAMP WITH TIME ZONE), CAST('294276-12-31 23:58:59.1231+03' AS TIMESTAMP WITH TIME ZONE), CAST('infinity' AS TIMESTAMP WITH TIME ZONE), CAST('-infinity' AS TIMESTAMP WITH TIME ZONE)", [], format ) expect( res.getvalue(0,0) ).to be_within(1e-3).of( Time.new(2013, 12, 31, 23, 58, 59, "+02:00").getlocal ) expect( res.getvalue(0,1) ).to be_within(1e-3).of( Time.new(1913, 12, 31, 23, 58, 59.1231, "-03:00").getlocal ) expect( res.getvalue(0,2) ).to be_within(1e-3).of( Time.new(-4713, 11, 24, 23, 58, 59.1231, "-03:00").getlocal ) expect( res.getvalue(0,3) ).to be_within(1e-3).of( Time.new(294276, 12, 31, 23, 58, 59.1231, "+03:00").getlocal ) expect( res.getvalue(0,4) ).to eq( 'infinity' ) expect( res.getvalue(0,5) ).to eq( '-infinity' ) end end it "should do date type conversions" do [0].each do |format| res = @conn.exec_params( "SELECT CAST('2113-12-31' AS DATE), CAST('1913-12-31' AS DATE), CAST('infinity' AS DATE), CAST('-infinity' AS DATE)", [], format ) expect( res.getvalue(0,0) ).to eq( Date.new(2113, 12, 31) ) expect( res.getvalue(0,1) ).to eq( Date.new(1913, 12, 31) ) expect( res.getvalue(0,2) ).to eq( 'infinity' ) expect( res.getvalue(0,3) ).to eq( '-infinity' ) end end it "should do numeric type conversions" do [0].each do |format| small = '123456790123.12' large = ('123456790'*10) << '.' << ('012345679') numerics = [ '1', '1.0', '1.2', small, large, ] sql_numerics = numerics.map { |v| "CAST(#{v} AS numeric)" } res = @conn.exec_params( "SELECT #{sql_numerics.join(',')}", [], format ) expect( res.getvalue(0,0) ).to eq( BigDecimal('1') ) expect( res.getvalue(0,1) ).to eq( BigDecimal('1') ) expect( res.getvalue(0,2) ).to eq( BigDecimal('1.2') ) expect( res.getvalue(0,3) ).to eq( BigDecimal(small) ) expect( res.getvalue(0,4) ).to eq( BigDecimal(large) ) end end it "should do JSON conversions", :postgresql_94 do [0].each do |format| ['JSON', 'JSONB'].each do |type| res = @conn.exec_params( "SELECT CAST('123' AS #{type}), CAST('12.3' AS #{type}), CAST('true' AS #{type}), CAST('false' AS #{type}), CAST('null' AS #{type}), CAST('[1, \"a\", null]' AS #{type}), CAST('{\"b\" : [2,3]}' AS #{type})", [], format ) expect( res.getvalue(0,0) ).to eq( 123 ) expect( res.getvalue(0,1) ).to be_within(0.1).of( 12.3 ) expect( res.getvalue(0,2) ).to eq( true ) expect( res.getvalue(0,3) ).to eq( false ) expect( res.getvalue(0,4) ).to eq( nil ) expect( res.getvalue(0,5) ).to eq( [1, "a", nil] ) expect( res.getvalue(0,6) ).to eq( {"b" => [2, 3]} ) end end end it "should do array type conversions" do [0].each do |format| res = @conn.exec_params( "SELECT CAST('{1,2,3}' AS INT2[]), CAST('{{1,2},{3,4}}' AS INT2[][]), CAST('{1,2,3}' AS INT4[]), CAST('{1,2,3}' AS INT8[]), CAST('{1,2,3}' AS TEXT[]), CAST('{1,2,3}' AS VARCHAR[]), CAST('{1,2,3}' AS FLOAT4[]), CAST('{1,2,3}' AS FLOAT8[]) ", [], format ) expect( res.getvalue(0,0) ).to eq( [1,2,3] ) expect( res.getvalue(0,1) ).to eq( [[1,2],[3,4]] ) expect( res.getvalue(0,2) ).to eq( [1,2,3] ) expect( res.getvalue(0,3) ).to eq( [1,2,3] ) expect( res.getvalue(0,4) ).to eq( ['1','2','3'] ) expect( res.getvalue(0,5) ).to eq( ['1','2','3'] ) expect( res.getvalue(0,6) ).to eq( [1.0,2.0,3.0] ) expect( res.getvalue(0,7) ).to eq( [1.0,2.0,3.0] ) end end it "should do inet type conversions" do [0].each do |format| vals = [ '1.2.3.4', '0.0.0.0/0', '1.0.0.0/8', '1.2.0.0/16', '1.2.3.0/24', '1.2.3.4/24', '1.2.3.4/32', '1.2.3.128/25', '1234:3456:5678:789a:9abc:bced:edf0:f012', '::/0', '1234:3456::/32', '1234:3456:5678:789a::/64', '1234:3456:5678:789a:9abc:bced::/96', '1234:3456:5678:789a:9abc:bced:edf0:f012/128', '1234:3456:5678:789a:9abc:bced:edf0:f012/0', '1234:3456:5678:789a:9abc:bced:edf0:f012/32', '1234:3456:5678:789a:9abc:bced:edf0:f012/64', '1234:3456:5678:789a:9abc:bced:edf0:f012/96', ] sql_vals = vals.map{|v| "CAST('#{v}' AS inet)"} res = @conn.exec_params(("SELECT " + sql_vals.join(', ')), [], format ) vals.each_with_index do |v, i| expect( res.getvalue(0,i) ).to eq( IPAddr.new(v) ) end end end it "should do cidr type conversions" do [0].each do |format| vals = [ '0.0.0.0/0', '1.0.0.0/8', '1.2.0.0/16', '1.2.3.0/24', '1.2.3.4/32', '1.2.3.128/25', '::/0', '1234:3456::/32', '1234:3456:5678:789a::/64', '1234:3456:5678:789a:9abc:bced::/96', '1234:3456:5678:789a:9abc:bced:edf0:f012/128', ] sql_vals = vals.map { |v| "CAST('#{v}' AS cidr)" } res = @conn.exec_params(("SELECT " + sql_vals.join(', ')), [], format ) vals.each_with_index do |v, i| val = res.getvalue(0,i) ip, prefix = v.split('/', 2) expect( val.to_s ).to eq( ip ) if val.respond_to?(:prefix) val_prefix = val.prefix else default_prefix = (val.family == Socket::AF_INET ? 32 : 128) range = val.to_range val_prefix = default_prefix - Math.log(((range.end.to_i - range.begin.to_i) + 1), 2).to_i end if v.include?('/') expect( val_prefix ).to eq( prefix.to_i ) elsif v.include?('.') expect( val_prefix ).to eq( 32 ) else expect( val_prefix ).to eq( 128 ) end end end end end context "with usage of result oids for copy decoder selection" do it "can type cast #copy_data output with explicit decoder" do @conn.exec( "CREATE TEMP TABLE copytable (t TEXT, i INT, ai INT[])" ) @conn.exec( "INSERT INTO copytable VALUES ('a', 123, '{5,4,3}'), ('b', 234, '{2,3}')" ) # Retrieve table OIDs per empty result. res = @conn.exec( "SELECT * FROM copytable LIMIT 0" ) tm = basic_type_mapping.build_column_map( res ) row_decoder = PG::TextDecoder::CopyRow.new type_map: tm rows = [] @conn.copy_data( "COPY copytable TO STDOUT", row_decoder ) do |res| while row=@conn.get_copy_data rows << row end end expect( rows ).to eq( [['a', 123, [5,4,3]], ['b', 234, [2,3]]] ) end end end describe PG::BasicTypeMapBasedOnResult do let!(:basic_type_mapping) do PG::BasicTypeMapBasedOnResult.new @conn end context "with usage of result oids for bind params encoder selection" do it "can type cast query params" do @conn.exec( "CREATE TEMP TABLE copytable (t TEXT, i INT, ai INT[])" ) # Retrieve table OIDs per empty result. res = @conn.exec( "SELECT * FROM copytable LIMIT 0" ) tm = basic_type_mapping.build_column_map( res ) @conn.exec_params( "INSERT INTO copytable VALUES ($1, $2, $3)", ['a', 123, [5,4,3]], 0, tm ) @conn.exec_params( "INSERT INTO copytable VALUES ($1, $2, $3)", ['b', 234, [2,3]], 0, tm ) res = @conn.exec( "SELECT * FROM copytable" ) expect( res.values ).to eq( [['a', '123', '{5,4,3}'], ['b', '234', '{2,3}']] ) end it "can do JSON conversions", :postgresql_94 do ['JSON', 'JSONB'].each do |type| sql = "SELECT CAST('123' AS #{type}), CAST('12.3' AS #{type}), CAST('true' AS #{type}), CAST('false' AS #{type}), CAST('null' AS #{type}), CAST('[1, \"a\", null]' AS #{type}), CAST('{\"b\" : [2,3]}' AS #{type})" tm = basic_type_mapping.build_column_map( @conn.exec( sql ) ) expect( tm.coders.map(&:name) ).to eq( [type.downcase] * 7 ) res = @conn.exec_params( "SELECT $1, $2, $3, $4, $5, $6, $7", [ 123, 12.3, true, false, nil, [1, "a", nil], {"b" => [2, 3]}, ], 0, tm ) expect( res.getvalue(0,0) ).to eq( "123" ) expect( res.getvalue(0,1) ).to eq( "12.3" ) expect( res.getvalue(0,2) ).to eq( "true" ) expect( res.getvalue(0,3) ).to eq( "false" ) expect( res.getvalue(0,4) ).to eq( nil ) expect( res.getvalue(0,5).gsub(" ","") ).to eq( "[1,\"a\",null]" ) expect( res.getvalue(0,6).gsub(" ","") ).to eq( "{\"b\":[2,3]}" ) end end end context "with usage of result oids for copy encoder selection" do it "can type cast #copy_data input with explicit encoder" do @conn.exec( "CREATE TEMP TABLE copytable (t TEXT, i INT, ai INT[])" ) # Retrieve table OIDs per empty result set. res = @conn.exec( "SELECT * FROM copytable LIMIT 0" ) tm = basic_type_mapping.build_column_map( res ) row_encoder = PG::TextEncoder::CopyRow.new type_map: tm @conn.copy_data( "COPY copytable FROM STDIN", row_encoder ) do |res| @conn.put_copy_data ['a', 123, [5,4,3]] @conn.put_copy_data ['b', 234, [2,3]] end res = @conn.exec( "SELECT * FROM copytable" ) expect( res.values ).to eq( [['a', '123', '{5,4,3}'], ['b', '234', '{2,3}']] ) end end end end pg-1.2.3/spec/pg/result_spec.rb0000644000004100000410000005572413704151215016404 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative '../helpers' require 'pg' require 'objspace' describe PG::Result do describe :field_name_type do let!(:res) { @conn.exec('SELECT 1 AS a, 2 AS "B"') } it "uses string field names per default" do expect(res.field_name_type).to eq(:string) end it "can set string field names" do res.field_name_type = :string expect(res.field_name_type).to eq(:string) end it "can set symbol field names" do res.field_name_type = :symbol expect(res.field_name_type).to eq(:symbol) end it "can set static_symbol field names" do res.field_name_type = :static_symbol expect(res.field_name_type).to eq(:static_symbol) end it "can't set symbol field names after #fields" do res.fields expect{ res.field_name_type = :symbol }.to raise_error(ArgumentError, /already materialized/) expect(res.field_name_type).to eq(:string) end it "can't set invalid values" do expect{ res.field_name_type = :sym }.to raise_error(ArgumentError, /invalid argument :sym/) expect{ res.field_name_type = "symbol" }.to raise_error(ArgumentError, /invalid argument "symbol"/) end end it "acts as an array of hashes" do res = @conn.exec("SELECT 1 AS a, 2 AS b") expect( res[0]['a'] ).to eq( '1' ) expect( res[0]['b'] ).to eq( '2' ) end it "acts as an array of hashes with symbols" do res = @conn.exec("SELECT 1 AS a, 2 AS b") res.field_name_type = :symbol expect( res[0][:a] ).to eq( '1' ) expect( res[0][:b] ).to eq( '2' ) end it "acts as an array of hashes with static_symbols" do res = @conn.exec("SELECT 1 AS a, 2 AS b") res.field_name_type = :static_symbol expect( res[0][:a] ).to eq( '1' ) expect( res[0][:b] ).to eq( '2' ) end it "yields a row as an array" do res = @conn.exec("SELECT 1 AS a, 2 AS b") list = [] res.each_row { |r| list << r } expect( list ).to eq [['1', '2']] end it "yields a row as an Enumerator" do res = @conn.exec("SELECT 1 AS a, 2 AS b") e = res.each_row expect( e ).to be_a_kind_of(Enumerator) expect( e.size ).to eq( 1 ) expect( e.to_a ).to eq [['1', '2']] end it "yields a row as an Enumerator of hashs" do res = @conn.exec("SELECT 1 AS a, 2 AS b") e = res.each expect( e ).to be_a_kind_of(Enumerator) expect( e.size ).to eq( 1 ) expect( e.to_a ).to eq [{'a'=>'1', 'b'=>'2'}] end it "yields a row as an Enumerator of hashs with symbols" do res = @conn.exec("SELECT 1 AS a, 2 AS b") res.field_name_type = :symbol expect( res.each.to_a ).to eq [{:a=>'1', :b=>'2'}] end context "result streaming in single row mode" do let!(:textdec_int){ PG::TextDecoder::Integer.new name: 'INT4', oid: 23 } it "can iterate over all rows as Hash" do @conn.send_query( "SELECT generate_series(2,4) AS a; SELECT 1 AS b, generate_series(5,6) AS c" ) @conn.set_single_row_mode expect( @conn.get_result.stream_each.to_a ).to eq( [{'a'=>"2"}, {'a'=>"3"}, {'a'=>"4"}] ) expect( @conn.get_result.enum_for(:stream_each).to_a ).to eq( [{'b'=>"1", 'c'=>"5"}, {'b'=>"1", 'c'=>"6"}] ) expect( @conn.get_result ).to be_nil end it "can iterate over all rows as Hash with symbols and typemap" do @conn.send_query( "SELECT generate_series(2,4) AS a" ) @conn.set_single_row_mode res = @conn.get_result.field_names_as(:symbol) res.type_map = PG::TypeMapByColumn.new [textdec_int] expect( res.stream_each.to_a ).to eq( [{:a=>2}, {:a=>3}, {:a=>4}] ) expect( @conn.get_result ).to be_nil end it "keeps last result on error while iterating stream_each" do @conn.send_query( "SELECT generate_series(2,4) AS a" ) @conn.set_single_row_mode res = @conn.get_result expect do res.stream_each_row do raise ZeroDivisionError end end.to raise_error(ZeroDivisionError) expect( res.values ).to eq([["2"]]) end it "can iterate over all rows as Array" do @conn.send_query( "SELECT generate_series(2,4) AS a; SELECT 1 AS b, generate_series(5,6) AS c" ) @conn.set_single_row_mode expect( @conn.get_result.enum_for(:stream_each_row).to_a ).to eq( [["2"], ["3"], ["4"]] ) expect( @conn.get_result.stream_each_row.to_a ).to eq( [["1", "5"], ["1", "6"]] ) expect( @conn.get_result ).to be_nil end it "keeps last result on error while iterating stream_each_row" do @conn.send_query( "SELECT generate_series(2,4) AS a" ) @conn.set_single_row_mode res = @conn.get_result expect do res.stream_each_row do raise ZeroDivisionError end end.to raise_error(ZeroDivisionError) expect( res.values ).to eq([["2"]]) end it "can iterate over all rows as PG::Tuple" do @conn.send_query( "SELECT generate_series(2,4) AS a; SELECT 1 AS b, generate_series(5,6) AS c" ) @conn.set_single_row_mode tuples = @conn.get_result.stream_each_tuple.to_a expect( tuples[0][0] ).to eq( "2" ) expect( tuples[1]["a"] ).to eq( "3" ) expect( tuples.size ).to eq( 3 ) tuples = @conn.get_result.enum_for(:stream_each_tuple).to_a expect( tuples[-1][-1] ).to eq( "6" ) expect( tuples[-2]["b"] ).to eq( "1" ) expect( tuples.size ).to eq( 2 ) expect( @conn.get_result ).to be_nil end it "clears result on error while iterating stream_each_tuple" do @conn.send_query( "SELECT generate_series(2,4) AS a" ) @conn.set_single_row_mode res = @conn.get_result expect do res.stream_each_tuple do raise ZeroDivisionError end end.to raise_error(ZeroDivisionError) expect( res.cleared? ).to eq(true) end it "should reuse field names in stream_each_tuple" do @conn.send_query( "SELECT generate_series(2,3) AS a" ) @conn.set_single_row_mode tuple1, tuple2 = *@conn.get_result.stream_each_tuple.to_a expect( tuple1.keys[0].object_id ).to eq(tuple2.keys[0].object_id) end it "can iterate over all rows as PG::Tuple with symbols and typemap" do @conn.send_query( "SELECT generate_series(2,4) AS a" ) @conn.set_single_row_mode res = @conn.get_result.field_names_as(:symbol) res.type_map = PG::TypeMapByColumn.new [textdec_int] tuples = res.stream_each_tuple.to_a expect( tuples[0][0] ).to eq( 2 ) expect( tuples[1][:a] ).to eq( 3 ) expect( @conn.get_result ).to be_nil end it "complains when not in single row mode" do @conn.send_query( "SELECT generate_series(2,4)" ) expect{ @conn.get_result.stream_each_row.to_a }.to raise_error(PG::InvalidResultStatus, /not in single row mode/) end it "complains when intersected with get_result" do @conn.send_query( "SELECT 1" ) @conn.set_single_row_mode expect{ @conn.get_result.stream_each_row.each{ @conn.get_result } }.to raise_error(PG::NoResultError, /no result received/) end it "raises server errors" do @conn.send_query( "SELECT 0/0" ) expect{ @conn.get_result.stream_each_row.to_a }.to raise_error(PG::DivisionByZero) end end it "inserts nil AS NULL and return NULL as nil" do res = @conn.exec_params("SELECT $1::int AS n", [nil]) expect( res[0]['n'] ).to be_nil() end it "encapsulates errors in a PG::Error object" do exception = nil begin @conn.exec( "SELECT * FROM nonexistant_table" ) rescue PG::Error => err exception = err end result = exception.result expect( result ).to be_a( described_class() ) expect( result.error_field(PG::PG_DIAG_SEVERITY) ).to eq( 'ERROR' ) expect( result.error_field(PG::PG_DIAG_SQLSTATE) ).to eq( '42P01' ) expect( result.error_field(PG::PG_DIAG_MESSAGE_PRIMARY) ).to eq( 'relation "nonexistant_table" does not exist' ) expect( result.error_field(PG::PG_DIAG_MESSAGE_DETAIL) ).to be_nil() expect( result.error_field(PG::PG_DIAG_MESSAGE_HINT) ).to be_nil() expect( result.error_field(PG::PG_DIAG_STATEMENT_POSITION) ).to eq( '15' ) expect( result.error_field(PG::PG_DIAG_INTERNAL_POSITION) ).to be_nil() expect( result.error_field(PG::PG_DIAG_INTERNAL_QUERY) ).to be_nil() expect( result.error_field(PG::PG_DIAG_CONTEXT) ).to be_nil() expect( result.error_field(PG::PG_DIAG_SOURCE_FILE) ).to match( /parse_relation\.c$|namespace\.c$/ ) expect( result.error_field(PG::PG_DIAG_SOURCE_LINE) ).to match( /^\d+$/ ) expect( result.error_field(PG::PG_DIAG_SOURCE_FUNCTION) ).to match( /^parserOpenTable$|^RangeVarGetRelid$/ ) end it "encapsulates PG_DIAG_SEVERITY_NONLOCALIZED error in a PG::Error object", :postgresql_96 do result = nil begin @conn.exec( "SELECT * FROM nonexistant_table" ) rescue PG::Error => err result = err.result end expect( result.error_field(PG::PG_DIAG_SEVERITY_NONLOCALIZED) ).to eq( 'ERROR' ) end it "encapsulates database object names for integrity constraint violations", :postgresql_93 do @conn.exec( "CREATE TABLE integrity (id SERIAL PRIMARY KEY)" ) exception = nil begin @conn.exec( "INSERT INTO integrity VALUES (NULL)" ) rescue PG::Error => err exception = err end result = exception.result expect( result.error_field(PG::PG_DIAG_SCHEMA_NAME) ).to eq( 'public' ) expect( result.error_field(PG::PG_DIAG_TABLE_NAME) ).to eq( 'integrity' ) expect( result.error_field(PG::PG_DIAG_COLUMN_NAME) ).to eq( 'id' ) expect( result.error_field(PG::PG_DIAG_DATATYPE_NAME) ).to be_nil expect( result.error_field(PG::PG_DIAG_CONSTRAINT_NAME) ).to be_nil end it "detects division by zero as SQLSTATE 22012" do sqlstate = nil begin @conn.exec("SELECT 1/0") rescue PG::Error => e sqlstate = e.result.result_error_field( PG::PG_DIAG_SQLSTATE ).to_i end expect( sqlstate ).to eq( 22012 ) end it "provides the error message" do @conn.send_query("SELECT xyz") res = @conn.get_result; @conn.get_result expect( res.error_message ).to match(/"xyz"/) expect( res.result_error_message ).to match(/"xyz"/) end it "provides a verbose error message", :postgresql_96 do @conn.send_query("SELECT xyz") res = @conn.get_result; @conn.get_result # PQERRORS_TERSE should give a single line result expect( res.verbose_error_message(PG::PQERRORS_TERSE, PG::PQSHOW_CONTEXT_ALWAYS) ).to match(/\A.*\n\z/) # PQERRORS_VERBOSE should give a multi line result expect( res.result_verbose_error_message(PG::PQERRORS_VERBOSE, PG::PQSHOW_CONTEXT_NEVER) ).to match(/\n.*\n/) end it "provides a verbose error message with SQLSTATE", :postgresql_12 do @conn.send_query("SELECT xyz") res = @conn.get_result; @conn.get_result expect( res.verbose_error_message(PG::PQERRORS_SQLSTATE, PG::PQSHOW_CONTEXT_NEVER) ).to match(/42703/) end it "returns the same bytes in binary format that are sent in binary format" do binary_file = File.join(Dir.pwd, 'spec/data', 'random_binary_data') bytes = File.open(binary_file, 'rb').read res = @conn.exec_params('VALUES ($1::bytea)', [ { :value => bytes, :format => 1 } ], 1) expect( res[0]['column1'] ).to eq( bytes ) expect( res.getvalue(0,0) ).to eq( bytes ) expect( res.values[0][0] ).to eq( bytes ) expect( res.column_values(0)[0] ).to eq( bytes ) end it "returns the same bytes in binary format that are sent as inline text" do binary_file = File.join(Dir.pwd, 'spec/data', 'random_binary_data') bytes = File.open(binary_file, 'rb').read @conn.exec("SET standard_conforming_strings=on") res = @conn.exec_params("VALUES ('#{PG::Connection.escape_bytea(bytes)}'::bytea)", [], 1) expect( res[0]['column1'] ).to eq( bytes ) expect( res.getvalue(0,0) ).to eq( bytes ) expect( res.values[0][0] ).to eq( bytes ) expect( res.column_values(0)[0] ).to eq( bytes ) end it "returns the same bytes in text format that are sent in binary format" do binary_file = File.join(Dir.pwd, 'spec/data', 'random_binary_data') bytes = File.open(binary_file, 'rb').read res = @conn.exec_params('VALUES ($1::bytea)', [ { :value => bytes, :format => 1 } ]) expect( PG::Connection.unescape_bytea(res[0]['column1']) ).to eq( bytes ) end it "returns the same bytes in text format that are sent as inline text" do binary_file = File.join(Dir.pwd, 'spec/data', 'random_binary_data') in_bytes = File.open(binary_file, 'rb').read out_bytes = nil @conn.exec("SET standard_conforming_strings=on") res = @conn.exec_params("VALUES ('#{PG::Connection.escape_bytea(in_bytes)}'::bytea)", [], 0) out_bytes = PG::Connection.unescape_bytea(res[0]['column1']) expect( out_bytes ).to eq( in_bytes ) end it "returns the parameter type of the specified prepared statement parameter" do query = 'SELECT * FROM pg_stat_activity WHERE user = $1::name AND query = $2::text' @conn.prepare( 'queryfinder', query ) res = @conn.describe_prepared( 'queryfinder' ) expect( @conn.exec_params( 'SELECT format_type($1, -1)', [res.paramtype(0)] ).getvalue( 0, 0 ) ).to eq( 'name' ) expect( @conn.exec_params( 'SELECT format_type($1, -1)', [res.paramtype(1)] ).getvalue( 0, 0 ) ).to eq( 'text' ) end it "raises an exception when a negative index is given to #fformat" do res = @conn.exec('SELECT * FROM pg_stat_activity') expect { res.fformat( -1 ) }.to raise_error( ArgumentError, /column number/i ) end it "raises an exception when a negative index is given to #fmod" do res = @conn.exec('SELECT * FROM pg_stat_activity') expect { res.fmod( -1 ) }.to raise_error( ArgumentError, /column number/i ) end it "raises an exception when a negative index is given to #[]" do res = @conn.exec('SELECT * FROM pg_stat_activity') expect { res[ -1 ] }.to raise_error( IndexError, /-1 is out of range/i ) end it "raises allow for conversion to an array of arrays" do @conn.exec( 'CREATE TABLE valuestest ( foo varchar(33) )' ) @conn.exec( 'INSERT INTO valuestest ("foo") values (\'bar\')' ) @conn.exec( 'INSERT INTO valuestest ("foo") values (\'bar2\')' ) res = @conn.exec( 'SELECT * FROM valuestest' ) expect( res.values ).to eq( [ ["bar"], ["bar2"] ] ) end it "can retrieve field names" do res = @conn.exec('SELECT 1 AS a, 2 AS "B"') expect(res.fields).to eq(["a", "B"]) end it "can retrieve field names as symbols" do res = @conn.exec('SELECT 1 AS a, 2 AS "B"') res.field_name_type = :symbol expect(res.fields).to eq([:a, :B]) end it "can retrieve single field names" do res = @conn.exec('SELECT 1 AS a, 2 AS "B"') expect(res.fname(0)).to eq("a") expect(res.fname(1)).to eq("B") expect{res.fname(2)}.to raise_error(ArgumentError) end it "can retrieve single field names as symbol" do res = @conn.exec('SELECT 1 AS a, 2 AS "B"') res.field_name_type = :symbol expect(res.fname(0)).to eq(:a) expect(res.fname(1)).to eq(:B) expect{res.fname(2)}.to raise_error(ArgumentError) end # PQfmod it "can return the type modifier for a result column" do @conn.exec( 'CREATE TABLE fmodtest ( foo varchar(33) )' ) res = @conn.exec( 'SELECT * FROM fmodtest' ) expect( res.fmod(0) ).to eq( 33 + 4 ) # Column length + varlena size (4) end it "raises an exception when an invalid index is passed to PG::Result#fmod" do @conn.exec( 'CREATE TABLE fmodtest ( foo varchar(33) )' ) res = @conn.exec( 'SELECT * FROM fmodtest' ) expect { res.fmod(1) }.to raise_error( ArgumentError ) end it "raises an exception when an invalid (negative) index is passed to PG::Result#fmod" do @conn.exec( 'CREATE TABLE fmodtest ( foo varchar(33) )' ) res = @conn.exec( 'SELECT * FROM fmodtest' ) expect { res.fmod(-11) }.to raise_error( ArgumentError ) end it "doesn't raise an exception when a valid index is passed to PG::Result#fmod for a" + " column with no typemod" do @conn.exec( 'CREATE TABLE fmodtest ( foo text )' ) res = @conn.exec( 'SELECT * FROM fmodtest' ) expect( res.fmod(0) ).to eq( -1 ) end # PQftable it "can return the oid of the table from which a result column was fetched" do @conn.exec( 'CREATE TABLE ftabletest ( foo text )' ) res = @conn.exec( 'SELECT * FROM ftabletest' ) expect( res.ftable(0) ).to be_nonzero() end it "raises an exception when an invalid index is passed to PG::Result#ftable" do @conn.exec( 'CREATE TABLE ftabletest ( foo text )' ) res = @conn.exec( 'SELECT * FROM ftabletest' ) expect { res.ftable(18) }.to raise_error( ArgumentError ) end it "raises an exception when an invalid (negative) index is passed to PG::Result#ftable" do @conn.exec( 'CREATE TABLE ftabletest ( foo text )' ) res = @conn.exec( 'SELECT * FROM ftabletest' ) expect { res.ftable(-2) }.to raise_error( ArgumentError ) end it "doesn't raise an exception when a valid index is passed to PG::Result#ftable for a " + "column with no corresponding table" do @conn.exec( 'CREATE TABLE ftabletest ( foo text )' ) res = @conn.exec( 'SELECT foo, LENGTH(foo) as length FROM ftabletest' ) expect( res.ftable(1) ).to eq( PG::INVALID_OID ) end # PQftablecol it "can return the column number (within its table) of a column in a result" do @conn.exec( 'CREATE TABLE ftablecoltest ( foo text, bar numeric )' ) res = @conn.exec( 'SELECT * FROM ftablecoltest' ) expect( res.ftablecol(0) ).to eq( 1 ) expect( res.ftablecol(1) ).to eq( 2 ) end it "raises an exception when an invalid index is passed to PG::Result#ftablecol" do @conn.exec( 'CREATE TABLE ftablecoltest ( foo text, bar numeric )' ) res = @conn.exec( 'SELECT * FROM ftablecoltest' ) expect { res.ftablecol(32) }.to raise_error( ArgumentError ) end it "raises an exception when an invalid (negative) index is passed to PG::Result#ftablecol" do @conn.exec( 'CREATE TABLE ftablecoltest ( foo text, bar numeric )' ) res = @conn.exec( 'SELECT * FROM ftablecoltest' ) expect { res.ftablecol(-1) }.to raise_error( ArgumentError ) end it "doesnn't raise an exception when a valid index is passed to PG::Result#ftablecol for a " + "column with no corresponding table" do @conn.exec( 'CREATE TABLE ftablecoltest ( foo text )' ) res = @conn.exec( 'SELECT foo, LENGTH(foo) as length FROM ftablecoltest' ) expect( res.ftablecol(1) ).to eq( 0 ) end it "can be manually checked for failed result status (async API)" do @conn.send_query( "SELECT * FROM nonexistant_table" ) res = @conn.get_result expect { res.check }.to raise_error( PG::Error, /relation "nonexistant_table" does not exist/ ) end it "can return the values of a single field" do res = @conn.exec( "SELECT 1 AS x, 'a' AS y UNION ALL SELECT 2, 'b'" ) expect( res.field_values('x') ).to eq( ['1', '2'] ) expect( res.field_values('y') ).to eq( ['a', 'b'] ) expect( res.field_values(:x) ).to eq( ['1', '2'] ) expect{ res.field_values('') }.to raise_error(IndexError) expect{ res.field_values(0) }.to raise_error(TypeError) end it "can return the values of a single tuple" do res = @conn.exec( "SELECT 1 AS x, 'a' AS y UNION ALL SELECT 2, 'b'" ) expect( res.tuple_values(0) ).to eq( ['1', 'a'] ) expect( res.tuple_values(1) ).to eq( ['2', 'b'] ) expect{ res.tuple_values(2) }.to raise_error(IndexError) expect{ res.tuple_values(-1) }.to raise_error(IndexError) expect{ res.tuple_values("x") }.to raise_error(TypeError) end it "can return the values of a single vary lazy tuple" do res = @conn.exec( "VALUES(1),(2)" ) expect( res.tuple(0) ).to be_kind_of( PG::Tuple ) expect( res.tuple(1) ).to be_kind_of( PG::Tuple ) expect{ res.tuple(2) }.to raise_error(IndexError) expect{ res.tuple(-1) }.to raise_error(IndexError) expect{ res.tuple("x") }.to raise_error(TypeError) end it "raises a proper exception for a nonexistant table" do expect { @conn.exec( "SELECT * FROM nonexistant_table" ) }.to raise_error( PG::UndefinedTable, /relation "nonexistant_table" does not exist/ ) end it "raises a more generic exception for an unknown SQLSTATE" do old_error = PG::ERROR_CLASSES.delete('42P01') begin expect { @conn.exec( "SELECT * FROM nonexistant_table" ) }.to raise_error{|error| expect( error ).to be_an_instance_of(PG::SyntaxErrorOrAccessRuleViolation) expect( error.to_s ).to match(/relation "nonexistant_table" does not exist/) } ensure PG::ERROR_CLASSES['42P01'] = old_error end end it "raises a ServerError for an unknown SQLSTATE class" do old_error1 = PG::ERROR_CLASSES.delete('42P01') old_error2 = PG::ERROR_CLASSES.delete('42') begin expect { @conn.exec( "SELECT * FROM nonexistant_table" ) }.to raise_error{|error| expect( error ).to be_an_instance_of(PG::ServerError) expect( error.to_s ).to match(/relation "nonexistant_table" does not exist/) } ensure PG::ERROR_CLASSES['42P01'] = old_error1 PG::ERROR_CLASSES['42'] = old_error2 end end it "raises a proper exception for a nonexistant schema" do expect { @conn.exec( "DROP SCHEMA nonexistant_schema" ) }.to raise_error( PG::InvalidSchemaName, /schema "nonexistant_schema" does not exist/ ) end it "the raised result is nil in case of a connection error" do c = PG::Connection.connect_start( '127.0.0.1', 54320, "", "", "me", "xxxx", "somedb" ) expect { c.exec "select 1" }.to raise_error {|error| expect( error ).to be_an_instance_of(PG::UnableToSend) expect( error.result ).to eq( nil ) } end it "does not clear the result itself" do r = @conn.exec "select 1" expect( r.autoclear? ).to eq(false) expect( r.cleared? ).to eq(false) r.clear expect( r.cleared? ).to eq(true) end it "can be inspected before and after clear" do r = @conn.exec "select 1" expect( r.inspect ).to match(/status=PGRES_TUPLES_OK/) r.clear expect( r.inspect ).to match(/cleared/) end it "should give account about memory usage" do r = @conn.exec "select 1" expect( ObjectSpace.memsize_of(r) ).to be > 1000 r.clear expect( ObjectSpace.memsize_of(r) ).to be < 100 end context 'result value conversions with TypeMapByColumn' do let!(:textdec_int){ PG::TextDecoder::Integer.new name: 'INT4', oid: 23 } let!(:textdec_float){ PG::TextDecoder::Float.new name: 'FLOAT4', oid: 700 } it "should allow reading, assigning and diabling type conversions" do res = @conn.exec( "SELECT 123" ) expect( res.type_map ).to be_kind_of(PG::TypeMapAllStrings) res.type_map = PG::TypeMapByColumn.new [textdec_int] expect( res.type_map ).to be_an_instance_of(PG::TypeMapByColumn) expect( res.type_map.coders ).to eq( [textdec_int] ) res.type_map = PG::TypeMapByColumn.new [textdec_float] expect( res.type_map.coders ).to eq( [textdec_float] ) res.type_map = PG::TypeMapAllStrings.new expect( res.type_map ).to be_kind_of(PG::TypeMapAllStrings) end it "should be applied to all value retrieving methods" do res = @conn.exec( "SELECT 123 as f" ) res.type_map = PG::TypeMapByColumn.new [textdec_int] expect( res.values ).to eq( [[123]] ) expect( res.getvalue(0,0) ).to eq( 123 ) expect( res[0] ).to eq( {'f' => 123 } ) expect( res.enum_for(:each_row).to_a ).to eq( [[123]] ) expect( res.enum_for(:each).to_a ).to eq( [{'f' => 123}] ) expect( res.column_values(0) ).to eq( [123] ) expect( res.field_values('f') ).to eq( [123] ) expect( res.field_values(:f) ).to eq( [123] ) expect( res.tuple_values(0) ).to eq( [123] ) end it "should be usable for several querys" do colmap = PG::TypeMapByColumn.new [textdec_int] res = @conn.exec( "SELECT 123" ) res.type_map = colmap expect( res.values ).to eq( [[123]] ) res = @conn.exec( "SELECT 456" ) res.type_map = colmap expect( res.values ).to eq( [[456]] ) end it "shouldn't allow invalid type maps" do res = @conn.exec( "SELECT 1" ) expect{ res.type_map = 1 }.to raise_error(TypeError) end end end pg-1.2.3/spec/pg/type_map_spec.rb0000644000004100000410000000104313704151215016665 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative '../helpers' require 'pg' describe PG::TypeMap do let!(:tm){ PG::TypeMap.new } it "should raise an error when used for param type casts" do expect{ @conn.exec_params( "SELECT $1", [5], 0, tm ) }.to raise_error(NotImplementedError, /not suitable to map query params/) end it "should raise an error when used for result type casts" do res = @conn.exec( "SELECT 1" ) expect{ res.map_types!(tm) }.to raise_error(NotImplementedError, /not suitable to map result values/) end end pg-1.2.3/spec/data/0000755000004100000410000000000013704151215014015 5ustar www-datawww-datapg-1.2.3/spec/data/expected_trace.out0000644000004100000410000000103613704151215017525 0ustar www-datawww-dataTo backend> Msg Q To backend> "SELECT 1 AS one" To backend> Msg complete, length 21 From backend> T From backend (#4)> 28 From backend (#2)> 1 From backend> "one" From backend (#4)> 0 From backend (#2)> 0 From backend (#4)> 23 From backend (#2)> 4 From backend (#4)> -1 From backend (#2)> 0 From backend> D From backend (#4)> 11 From backend (#2)> 1 From backend (#4)> 1 From backend (1)> 1 From backend> C From backend (#4)> 11 From backend> "SELECT" From backend> Z From backend (#4)> 5 From backend> Z From backend (#4)> 5 From backend> T pg-1.2.3/spec/data/random_binary_data0000644000004100000410000001200013704151215017546 0ustar www-datawww-datako W 5Py%{/b4fdyo0|p6@ hnK()EzMq~%du0/bQfXzթP:HX2r?;ӍT.0l686UiDlV+4IShIXJDqNi-Q~K(s P)1;ؽ'3fҳ_|?e0@=1WǦ"#їsʁbM($S&NmZq2ݩ&@&HىSb]V+nk#LL(W g`=U cԌ8gn#JoHj5n'|D 4P򕣎|}*YX\>@Ɣ&1,[\W]+H?vk[$}/DsNe-g-]+2Ȗ*SVRt00v|ʤIhz>wr`y:YL`s{T.w_0N[JvR&lcHaϒWAE=;2Hf^DZU^ܢqePv36QkPzwI]dׇkƮ)SgZb~PX3Baɻ%w g˂L_sV6=}JF6*}5G~иpz0I[|-H@F+2Fgㆋ%9P; BT_pOcM h>p3SӞ9^-5 60x@ e+F48 :F S3D@,鋾AQz nRMR=#H3Lfokhw^Е|ݱvw.HzYw (Vj'*Sѥsєpfk@ o!ˆ!B(bΐ%:?K徃mo]C^5 N k*nKW9jr7; (=΄ ~t)/Qq3X\AAnQTMǔƹ0XYFaȄYvG{~FmQg?A3=!etaEǞ|1=*yČTliH,e(_y.G)HU&12S7+#PMy/`)x{on-06t>YO}GߜZȜ'mecDȞ2pUn֣?6wִݿo/9?pIp)yP1hw8E!\)? R'iLR3{I.oQ%'pHFO40sɇ[bE:}ap?大A_x2h6&Tf ᅎK˃bKp`!Y¹NlKƭ+agI;qDf WvN>Ct}3rSVFZ6b+g x^LKG(_ܮN?ʒ?:cNk%L["v`;˜J)OɬG4xaܶB%6Qp-O+rqy .'(9_jswnSŖsC,aB;"I{[r-HQA:>{v  ˁ**]O ZSrͲY+'…ܽXKkьd3=V/l#aP҈E&kHHn1('m)8ʎxܹ?֬ 3{?h2U1\L)]tѓ;ߘu) S4UVi#[ xXL9j+20OX3Mepv9=>!P;MDqM_팅`Fh}Ha. KxPx3#1ۭZGrȢ0T~N@kx \7;гLpr;'Tkۻ`up<$mo~fU ' lJ^ſz>*/H(;K}Rao~ NmE5_|dO95Av hCѿ) =L]&(lB2+&tsވ] qX# jG674!{B^=Etс{YBE,vW&U|>%뻻Mui-UJnF &k;+s:z¯-tWtCjHs$k#ikNHi0('xգs3jh!e CY@? ʔ}x`s(ˍS ctLqUG]lJ ms9B`zw:\g7LJ3PT ;?uT Eqs#5zr22;cdaUCߎg=% )>>e_'^Gz|W'?L{ȸq tk-"ؽ^S*"jVM[<1V{VQ./x0иrY=M[N9S(j#A~~?yoX 5l~~C^P>D>oͬ"_$ʵP`jxz=}?vzέ_PD D 淟}a^,HbR%ɵz~c"x;tOm# mJF6b,94K+}vV=ǻboILQm .5JxkȪ5;ѣA?In Wj2L^,zJΧdgH M~Ž᥇%jlanυ 0U-[ k!Xݮ!JTYrVio *`|Q)p;&YZG!(n^rbv X"W/ jT'^.LⅎͲ8b#b}6ק|ɷV5͊oGH)寊+GB3%`2r%>z'0-uPBNS0D)>-K%񌀬@2ir_]TV)fpQƏny2Y=߇G?U2}v%A:ik D&dq ͩb-ӰJN(iÜ*#2ّ chZUl \E Zeu*$(K 'j?C wP{| O~O(|(i+sx W{y diTܕZ&GOD7Pp%=&i=U>*8RyY(~]M}5 JF^]H-aF%j=;{$ȻYѹ(GLS \AޥwAo^np`C2;d+; M76%M-Ϻq.G=)[(p4!(D =rL4Nqe^fw{KCx> 9l e)XRP'AU#gyGͦCR {| afU]uy؆^p/_/g%AG.{^\>pg-1.2.3/spec/helpers.rb0000644000004100000410000002365213704151215015103 0ustar www-datawww-data# -*- ruby -*- require 'pathname' require 'rspec' require 'shellwords' require 'pg' DEFAULT_TEST_DIR_STR = File.join(Dir.pwd, "tmp_test_specs") TEST_DIR_STR = ENV['RUBY_PG_TEST_DIR'] || DEFAULT_TEST_DIR_STR TEST_DIRECTORY = Pathname.new(TEST_DIR_STR) module PG::TestingHelpers ### Automatically set up the database when it's used, and wrap a transaction around ### examples that don't disable it. def self::included( mod ) super if mod.respond_to?( :around ) mod.before( :all ) { @conn = setup_testing_db(described_class ? described_class.name : mod.description) } mod.around( :each ) do |example| begin @conn.set_default_encoding @conn.exec( 'BEGIN' ) unless example.metadata[:without_transaction] desc = example.source_location.join(':') @conn.exec %Q{SET application_name TO '%s'} % [@conn.escape_string(desc.slice(-60))] example.run ensure @conn.exec( 'ROLLBACK' ) unless example.metadata[:without_transaction] end end mod.after( :all ) { teardown_testing_db(@conn) } end end # # Examples # # Set some ANSI escape code constants (Shamelessly stolen from Perl's # Term::ANSIColor by Russ Allbery and Zenin ANSI_ATTRIBUTES = { 'clear' => 0, 'reset' => 0, 'bold' => 1, 'dark' => 2, 'underline' => 4, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'concealed' => 8, 'black' => 30, 'on_black' => 40, 'red' => 31, 'on_red' => 41, 'green' => 32, 'on_green' => 42, 'yellow' => 33, 'on_yellow' => 43, 'blue' => 34, 'on_blue' => 44, 'magenta' => 35, 'on_magenta' => 45, 'cyan' => 36, 'on_cyan' => 46, 'white' => 37, 'on_white' => 47 } ############### module_function ############### ### Create a string that contains the ANSI codes specified and return it def ansi_code( *attributes ) attributes.flatten! attributes.collect! {|at| at.to_s } return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM'] attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';') # $stderr.puts " attr is: %p" % [attributes] if attributes.empty? return '' else return "\e[%sm" % attributes end end ### Colorize the given +string+ with the specified +attributes+ and return it, handling ### line-endings, color reset, etc. def colorize( *args ) string = '' if block_given? string = yield else string = args.shift end ending = string[/(\s)$/] || '' string = string.rstrip return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending end ### Output a message with highlighting. def message( *msg ) $stderr.puts( colorize(:bold) { msg.flatten.join(' ') } ) end ### Output a logging message if $VERBOSE is true def trace( *msg ) return unless $VERBOSE output = colorize( msg.flatten.join(' '), 'yellow' ) $stderr.puts( output ) end ### Return the specified args as a string, quoting any that have a space. def quotelist( *args ) return args.flatten.collect {|part| part.to_s =~ /\s/ ? part.to_s.inspect : part.to_s } end ### Run the specified command +cmd+ with system(), failing if the execution ### fails. def run( *cmd ) cmd.flatten! if cmd.length > 1 trace( quotelist(*cmd) ) else trace( cmd ) end system( *cmd ) raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success? end ### Run the specified command +cmd+ after redirecting stdout and stderr to the specified ### +logpath+, failing if the execution fails. def log_and_run( logpath, *cmd ) cmd.flatten! if cmd.length > 1 trace( quotelist(*cmd) ) else trace( cmd ) end # Eliminate the noise of creating/tearing down the database by # redirecting STDERR/STDOUT to a logfile logfh = File.open( logpath, File::WRONLY|File::CREAT|File::APPEND ) system( *cmd, [STDOUT, STDERR] => logfh ) raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success? end ### Check the current directory for directories that look like they're ### testing directories from previous tests, and tell any postgres instances ### running in them to shut down. def stop_existing_postmasters # tmp_test_0.22329534700318 pat = Pathname.getwd + 'tmp_test_*' Pathname.glob( pat.to_s ).each do |testdir| datadir = testdir + 'data' pidfile = datadir + 'postmaster.pid' if pidfile.exist? && pid = pidfile.read.chomp.to_i trace "pidfile (%p) exists: %d" % [ pidfile, pid ] begin Process.kill( 0, pid ) rescue Errno::ESRCH trace "No postmaster running for %s" % [ datadir ] # Process isn't alive, so don't try to stop it else trace "Stopping lingering database at PID %d" % [ pid ] run 'pg_ctl', '-D', datadir.to_s, '-m', 'fast', 'stop' end else trace "No pidfile (%p)" % [ pidfile ] end end end ### Set up a PostgreSQL database instance for testing. def setup_testing_db( description ) require 'pg' stop_existing_postmasters() trace "Setting up test database for #{description}" @test_pgdata = TEST_DIRECTORY + 'data' @test_pgdata.mkpath ENV['PGPORT'] ||= "54321" @port = ENV['PGPORT'].to_i ENV['PGHOST'] = 'localhost' @conninfo = "host=localhost port=#{@port} dbname=test" @logfile = TEST_DIRECTORY + 'setup.log' trace "Command output logged to #{@logfile}" begin unless (@test_pgdata+"postgresql.conf").exist? FileUtils.rm_rf( @test_pgdata, :verbose => $DEBUG ) trace "Running initdb" log_and_run @logfile, 'initdb', '-E', 'UTF8', '--no-locale', '-D', @test_pgdata.to_s end trace "Starting postgres" log_and_run @logfile, 'pg_ctl', '-w', '-o', "-k #{TEST_DIRECTORY.to_s.dump}", '-D', @test_pgdata.to_s, 'start' sleep 2 trace "Creating the test DB" log_and_run @logfile, 'psql', '-e', '-c', 'DROP DATABASE IF EXISTS test', 'postgres' log_and_run @logfile, 'createdb', '-e', 'test' rescue => err $stderr.puts "%p during test setup: %s" % [ err.class, err.message ] $stderr.puts "See #{@logfile} for details." $stderr.puts err.backtrace if $DEBUG fail end conn = PG.connect( @conninfo ) conn.set_notice_processor do |message| $stderr.puts( description + ':' + message ) if $DEBUG end return conn end def teardown_testing_db( conn ) trace "Tearing down test database" if conn check_for_lingering_connections( conn ) conn.finish end log_and_run @logfile, 'pg_ctl', '-D', @test_pgdata.to_s, 'stop' end def check_for_lingering_connections( conn ) conn.exec( "SELECT * FROM pg_stat_activity" ) do |res| conns = res.find_all {|row| row['pid'].to_i != conn.backend_pid && ["client backend", nil].include?(row["backend_type"]) } unless conns.empty? puts "Lingering connections remain:" conns.each do |row| puts " [%s] {%s} %s -- %s" % row.values_at( 'pid', 'state', 'application_name', 'query' ) end end end end # Retrieve the names of the column types of a given result set. def result_typenames(res) @conn.exec_params( "SELECT " + res.nfields.times.map{|i| "format_type($#{i*2+1},$#{i*2+2})"}.join(","), res.nfields.times.map{|i| [res.ftype(i), res.fmod(i)] }.flatten ). values[0] end # A matcher for checking the status of a PG::Connection to ensure it's still # usable. class ConnStillUsableMatcher def initialize @conn = nil @problem = nil end def matches?( conn ) @conn = conn @problem = self.check_for_problems return @problem.nil? end def check_for_problems return "is finished" if @conn.finished? return "has bad status" unless @conn.status == PG::CONNECTION_OK return "has bad transaction status (%d)" % [ @conn.transaction_status ] unless @conn.transaction_status.between?( PG::PQTRANS_IDLE, PG::PQTRANS_INTRANS ) return "is not usable." unless self.can_exec_query? return nil end def can_exec_query? @conn.send_query( "VALUES (1)" ) @conn.get_last_result.values == [["1"]] end def failure_message return "expected %p to be usable, but it %s" % [ @conn, @problem ] end def failure_message_when_negated "expected %p not to be usable, but it still is" % [ @conn ] end end ### Return a ConnStillUsableMatcher to be used like: ### ### expect( pg_conn ).to still_be_usable ### def still_be_usable return ConnStillUsableMatcher.new end def wait_for_polling_ok(conn, meth = :connect_poll) status = conn.send(meth) while status != PG::PGRES_POLLING_OK if status == PG::PGRES_POLLING_READING select( [conn.socket_io], [], [], 5.0 ) or raise "Asynchronous connection timed out!" elsif status == PG::PGRES_POLLING_WRITING select( [], [conn.socket_io], [], 5.0 ) or raise "Asynchronous connection timed out!" end status = conn.send(meth) end end def wait_for_query_result(conn) result = nil loop do # Buffer any incoming data on the socket until a full result is ready. conn.consume_input while conn.is_busy select( [conn.socket_io], nil, nil, 5.0 ) or raise "Timeout waiting for query response." conn.consume_input end # Fetch the next result. If there isn't one, the query is finished result = conn.get_result || break end result end end RSpec.configure do |config| config.include( PG::TestingHelpers ) config.run_all_when_everything_filtered = true config.filter_run :focus config.order = 'random' config.mock_with( :rspec ) do |mock| mock.syntax = :expect end if RUBY_PLATFORM =~ /mingw|mswin/ config.filter_run_excluding :unix else config.filter_run_excluding :windows end config.filter_run_excluding( :postgresql_93 ) if PG.library_version < 90300 config.filter_run_excluding( :postgresql_94 ) if PG.library_version < 90400 config.filter_run_excluding( :postgresql_95 ) if PG.library_version < 90500 config.filter_run_excluding( :postgresql_96 ) if PG.library_version < 90600 config.filter_run_excluding( :postgresql_10 ) if PG.library_version < 100000 config.filter_run_excluding( :postgresql_12 ) if PG.library_version < 120000 end pg-1.2.3/spec/pg_spec.rb0000644000004100000410000000224313704151215015052 0ustar www-datawww-data# -*- rspec -*- # encoding: utf-8 require_relative 'helpers' require 'pg' describe PG do it "knows what version of the libpq library is loaded" do expect( PG.library_version ).to be_an( Integer ) expect( PG.library_version ).to be >= 90100 end it "can select which of both security libraries to initialize" do # This setting does nothing here, because there is already a connection # to the server, at this point in time. PG.init_openssl(false, true) PG.init_openssl(1, 0) end it "can select whether security libraries to initialize" do # This setting does nothing here, because there is already a connection # to the server, at this point in time. PG.init_ssl(false) PG.init_ssl(1) end it "knows whether or not the library is threadsafe" do expect( PG ).to be_threadsafe() end it "does have hierarchical error classes" do expect( PG::UndefinedTable.ancestors[0,4] ).to eq([ PG::UndefinedTable, PG::SyntaxErrorOrAccessRuleViolation, PG::ServerError, PG::Error ]) expect( PG::InvalidSchemaName.ancestors[0,3] ).to eq([ PG::InvalidSchemaName, PG::ServerError, PG::Error ]) end end pg-1.2.3/History.rdoc0000644000004100000410000005127413704151215014475 0ustar www-datawww-data== v1.2.3 [2020-03-18] Michael Granger Bugfixes: - Fix possible segfault at `PG::Coder#encode`, `decode` or their implicit calls through a typemap after GC.compact. #327 - Fix possible segfault in `PG::TypeMapByClass` after GC.compact. #328 == v1.2.2 [2020-01-06] Michael Granger Enhancements: - Add a binary gem for Ruby 2.7. == v1.2.1 [2020-01-02] Michael Granger Enhancements: - Added internal API for sequel_pg compatibility. == v1.2.0 [2019-12-20] Michael Granger Repository: - Our primary repository has been moved to Github https://github.com/ged/ruby-pg . Most of the issues from https://bitbucket.org/ged/ruby-pg have been migrated. #43 API enhancements: - Add PG::Result#field_name_type= and siblings to allow symbols to be used as field names. #306 - Add new methods for error reporting: - PG::Connection#set_error_context_visibility - PG::Result#verbose_error_message - PG::Result#result_verbose_error_message (alias) - Update errorcodes and error classes to PostgreSQL-12.0. - New constants: PG_DIAG_SEVERITY_NONLOCALIZED, PQERRORS_SQLSTATE, PQSHOW_CONTEXT_NEVER, PQSHOW_CONTEXT_ERRORS, PQSHOW_CONTEXT_ALWAYS Type cast enhancements: - Add PG::TextEncoder::Record and PG::TextDecoder::Record for en/decoding of Composite Types. #258, #36 - Add PG::BasicTypeRegistry.register_coder to register instances instead of classes. This is useful to register parametrized en/decoders like PG::TextDecoder::Record . - Add PG::BasicTypeMapForQueries#encode_array_as= to switch between various interpretations of ruby arrays. - Add Time, Array