diff --git a/Rakefile b/Rakefile index 0887b4531..8df8481af 100644 --- a/Rakefile +++ b/Rakefile @@ -69,7 +69,7 @@ $hoespec = Hoe.spec 'pg' do self.spec_extras[:extensions] = [ 'ext/extconf.rb' ] - self.require_ruby_version( '>= 2.0.0' ) + self.require_ruby_version( '>= 2.2' ) self.hg_sign_tags = true if self.respond_to?( :hg_sign_tags= ) self.check_history_on_release = true if self.respond_to?( :check_history_on_release= ) diff --git a/ext/pg.h b/ext/pg.h index 1807e0b5f..24f59b689 100644 --- a/ext/pg.h +++ b/ext/pg.h @@ -78,7 +78,7 @@ typedef long suseconds_t; #define RARRAY_AREF(a, i) (RARRAY_PTR(a)[i]) #endif -#define PG_ENC_IDX_BITS 30 +#define PG_ENC_IDX_BITS 28 /* The data behind each PG::Connection object */ typedef struct { @@ -102,6 +102,8 @@ typedef struct { VALUE decoder_for_get_copy_data; /* Ruby encoding index of the client/internal encoding */ int enc_idx : PG_ENC_IDX_BITS; + /* flags controlling Symbol/String field names */ + unsigned int flags : 2; #if defined(_WIN32) /* File descriptor to be used for rb_w32_unwrap_io_handle() */ @@ -135,6 +137,9 @@ typedef struct { */ unsigned int autoclear : 1; + /* flags controlling Symbol/String field names */ + unsigned int flags : 2; + /* Number of fields in fnames[] . * Set to -1 if fnames[] is not yet initialized. */ @@ -149,7 +154,7 @@ typedef struct { /* Hash with fnames[] to field number mapping. */ VALUE field_map; - /* List of field names as frozen String objects. + /* List of field names as frozen String or Symbol objects. * Only valid if nfields != -1 */ VALUE fnames[0]; @@ -165,6 +170,10 @@ typedef VALUE (* t_pg_typecast_result)(t_typemap *, VALUE, int, int); typedef t_pg_coder *(* t_pg_typecast_query_param)(t_typemap *, VALUE, int); typedef VALUE (* t_pg_typecast_copy_get)( t_typemap *, VALUE, int, int, int ); +#define PG_RESULT_FIELD_NAMES_MASK 0x03 +#define PG_RESULT_FIELD_NAMES_SYMBOL 0x01 +#define PG_RESULT_FIELD_NAMES_STATIC_SYMBOL 0x02 + #define PG_CODER_TIMESTAMP_DB_UTC 0x0 #define PG_CODER_TIMESTAMP_DB_LOCAL 0x1 #define PG_CODER_TIMESTAMP_APP_UTC 0x0 diff --git a/ext/pg_connection.c b/ext/pg_connection.c index 2c212b607..f6923a964 100644 --- a/ext/pg_connection.c +++ b/ext/pg_connection.c @@ -13,6 +13,7 @@ VALUE rb_cPGconn; static ID s_id_encode; static VALUE sym_type, sym_format, sym_value; +static VALUE sym_symbol, sym_string, sym_static_symbol; static PQnoticeReceiver default_notice_receiver = NULL; static PQnoticeProcessor default_notice_processor = NULL; @@ -4060,6 +4061,58 @@ pgconn_decoder_for_get_copy_data_get(VALUE self) return this->decoder_for_get_copy_data; } +/* + * call-seq: + * conn.field_name_type = Symbol + * + * Set default type of field names of results retrieved by this connection. + * It can be set to one of: + * * +:string+ to use String based field names + * * +:symbol+ to use Symbol based field names + * + * The default is +:string+ . + * + * Settings the type of field names affects only future results. + * + * See further description at PG::Result#field_name_type= + * + */ +static VALUE +pgconn_field_name_type_set(VALUE self, VALUE sym) +{ + t_pg_connection *this = pg_get_connection( self ); + + this->flags &= ~PG_RESULT_FIELD_NAMES_MASK; + if( sym == sym_symbol ) this->flags |= PG_RESULT_FIELD_NAMES_SYMBOL; + else if ( sym == sym_static_symbol ) this->flags |= PG_RESULT_FIELD_NAMES_STATIC_SYMBOL; + else if ( sym == sym_string ); + else rb_raise(rb_eArgError, "invalid argument %+"PRIsVALUE, sym); + + return sym; +} + +/* + * call-seq: + * conn.field_name_type -> Symbol + * + * Get type of field names. + * + * See description at #field_name_type= + */ +static VALUE +pgconn_field_name_type_get(VALUE self) +{ + t_pg_connection *this = pg_get_connection( self ); + + if( this->flags & PG_RESULT_FIELD_NAMES_SYMBOL ){ + return sym_symbol; + } else if( this->flags & PG_RESULT_FIELD_NAMES_STATIC_SYMBOL ){ + return sym_static_symbol; + } else { + return sym_string; + } +} + /* * Document-class: PG::Connection @@ -4071,6 +4124,9 @@ init_pg_connection() sym_type = ID2SYM(rb_intern("type")); sym_format = ID2SYM(rb_intern("format")); sym_value = ID2SYM(rb_intern("value")); + sym_string = ID2SYM(rb_intern("string")); + sym_symbol = ID2SYM(rb_intern("symbol")); + sym_static_symbol = ID2SYM(rb_intern("static_symbol")); rb_cPGconn = rb_define_class_under( rb_mPG, "Connection", rb_cObject ); rb_include_module(rb_cPGconn, rb_mPGconstants); @@ -4255,4 +4311,7 @@ init_pg_connection() rb_define_method(rb_cPGconn, "encoder_for_put_copy_data", pgconn_encoder_for_put_copy_data_get, 0); rb_define_method(rb_cPGconn, "decoder_for_get_copy_data=", pgconn_decoder_for_get_copy_data_set, 1); rb_define_method(rb_cPGconn, "decoder_for_get_copy_data", pgconn_decoder_for_get_copy_data_get, 0); + + rb_define_method(rb_cPGconn, "field_name_type=", pgconn_field_name_type_set, 1 ); + rb_define_method(rb_cPGconn, "field_name_type", pgconn_field_name_type_get, 0 ); } diff --git a/ext/pg_result.c b/ext/pg_result.c index 8e57690ca..a8de1e709 100644 --- a/ext/pg_result.c +++ b/ext/pg_result.c @@ -6,8 +6,8 @@ #include "pg.h" - VALUE rb_cPGresult; +static VALUE sym_symbol, sym_string, sym_static_symbol; static VALUE pgresult_type_map_set( VALUE, VALUE ); static t_pg_result *pgresult_get_this( VALUE ); @@ -190,6 +190,7 @@ pg_new_result2(PGresult *result, VALUE rb_pgconn) this->nfields = -1; this->tuple_hash = Qnil; this->field_map = Qnil; + this->flags = 0; self = TypedData_Wrap_Struct(rb_cPGresult, &pgresult_type, this); if( result ){ @@ -201,6 +202,7 @@ pg_new_result2(PGresult *result, VALUE rb_pgconn) this->enc_idx = p_conn->enc_idx; this->typemap = p_typemap->funcs.fit_to_result( typemap, self ); this->p_typemap = DATA_PTR( this->typemap ); + this->flags = p_conn->flags; } else { this->enc_idx = rb_locale_encindex(); } @@ -404,6 +406,31 @@ pgresult_get(VALUE self) return this->pgresult; } +static VALUE pg_cstr_to_sym(char *cstr, unsigned int flags, int enc_idx) +{ + VALUE fname; +#ifdef TRUFFLERUBY + if( flags & (PG_RESULT_FIELD_NAMES_SYMBOL | PG_RESULT_FIELD_NAMES_STATIC_SYMBOL) ){ +#else + if( flags & PG_RESULT_FIELD_NAMES_SYMBOL ){ + rb_encoding *enc = rb_enc_from_index(enc_idx); + fname = rb_check_symbol_cstr(cstr, strlen(cstr), enc); + if( fname == Qnil ){ + fname = rb_tainted_str_new2(cstr); + PG_ENCODING_SET_NOCHECK(fname, enc_idx); + fname = rb_str_intern(fname); + } + } else if( flags & PG_RESULT_FIELD_NAMES_STATIC_SYMBOL ){ +#endif + rb_encoding *enc = rb_enc_from_index(enc_idx); + fname = ID2SYM(rb_intern3(cstr, strlen(cstr), enc)); + } else { + fname = rb_tainted_str_new2(cstr); + PG_ENCODING_SET_NOCHECK(fname, enc_idx); + fname = rb_obj_freeze(fname); + } + return fname; +} static void pgresult_init_fnames(VALUE self) { @@ -414,12 +441,9 @@ static void pgresult_init_fnames(VALUE self) int nfields = PQnfields(this->pgresult); for( i=0; ipgresult, i)); - PG_ENCODING_SET_NOCHECK(fname, this->enc_idx); - this->fnames[i] = rb_obj_freeze(fname); + char *cfname = PQfname(this->pgresult, i); + this->fnames[i] = pg_cstr_to_sym(cfname, this->flags, this->enc_idx); this->nfields = i + 1; - - RB_GC_GUARD(fname); } this->nfields = nfields; } @@ -597,24 +621,25 @@ pgresult_nfields(VALUE self) /* * call-seq: - * res.fname( index ) -> String + * res.fname( index ) -> String or Symbol * * Returns the name of the column corresponding to _index_. + * Depending on #field_name_type= it's a String or Symbol. + * */ static VALUE pgresult_fname(VALUE self, VALUE index) { - VALUE fname; t_pg_result *this = pgresult_get_this_safe(self); int i = NUM2INT(index); + char *cfname; if (i < 0 || i >= PQnfields(this->pgresult)) { rb_raise(rb_eArgError,"invalid field number %d", i); } - fname = rb_tainted_str_new2(PQfname(this->pgresult, i)); - PG_ENCODING_SET_NOCHECK(fname, this->enc_idx); - return rb_obj_freeze(fname); + cfname = PQfname(this->pgresult, i); + return pg_cstr_to_sym(cfname, this->flags, this->enc_idx); } /* @@ -1115,8 +1140,12 @@ static VALUE pgresult_field_values( VALUE self, VALUE field ) { PGresult *result = pgresult_get( self ); - const char *fieldname = StringValueCStr( field ); - int fnum = PQfnumber( result, fieldname ); + const char *fieldname; + int fnum; + + if( RB_TYPE_P(field, T_SYMBOL) ) field = rb_sym_to_s( field ); + fieldname = StringValueCStr( field ); + fnum = PQfnumber( result, fieldname ); if ( fnum < 0 ) rb_raise( rb_eIndexError, "no such field '%s' in result", fieldname ); @@ -1230,7 +1259,7 @@ pgresult_each(VALUE self) * call-seq: * res.fields() -> Array * - * Returns an array of Strings representing the names of the fields in the result. + * Depending on #field_name_type= returns an array of strings or symbols representing the names of the fields in the result. */ static VALUE pgresult_fields(VALUE self) @@ -1466,10 +1495,70 @@ pgresult_stream_each_tuple(VALUE self) return pgresult_stream_any(self, yield_tuple); } +/* + * call-seq: + * res.field_name_type = Symbol + * + * Set type of field names specific to this result. + * It can be set to one of: + * * +:string+ to use String based field names + * * +:symbol+ to use Symbol based field names + * * +:static_symbol+ to use pinned Symbol (can not be garbage collected) - Don't use this, it will probably removed in future. + * + * The default is retrieved from PG::Connection#field_name_type , which defaults to +:string+ . + * + * This setting affects several result methods: + * * keys of Hash returned by #[] , #each and #stream_each + * * #fields + * * #fname + * * field names used by #tuple and #stream_each_tuple + * + * The type of field names can only be changed before any of the affected methods have been called. + * + */ +static VALUE +pgresult_field_name_type_set(VALUE self, VALUE sym) +{ + t_pg_result *this = pgresult_get_this(self); + if( this->nfields != -1 ) rb_raise(rb_eArgError, "field names are already materialized"); + + this->flags &= ~PG_RESULT_FIELD_NAMES_MASK; + if( sym == sym_symbol ) this->flags |= PG_RESULT_FIELD_NAMES_SYMBOL; + else if ( sym == sym_static_symbol ) this->flags |= PG_RESULT_FIELD_NAMES_STATIC_SYMBOL; + else if ( sym == sym_string ); + else rb_raise(rb_eArgError, "invalid argument %+"PRIsVALUE, sym); + + return sym; +} + +/* + * call-seq: + * res.field_name_type -> Symbol + * + * Get type of field names. + * + * See description at #field_name_type= + */ +static VALUE +pgresult_field_name_type_get(VALUE self) +{ + t_pg_result *this = pgresult_get_this(self); + if( this->flags & PG_RESULT_FIELD_NAMES_SYMBOL ){ + return sym_symbol; + } else if( this->flags & PG_RESULT_FIELD_NAMES_STATIC_SYMBOL ){ + return sym_static_symbol; + } else { + return sym_string; + } +} void init_pg_result() { + sym_string = ID2SYM(rb_intern("string")); + sym_symbol = ID2SYM(rb_intern("symbol")); + sym_static_symbol = ID2SYM(rb_intern("static_symbol")); + rb_cPGresult = rb_define_class_under( rb_mPG, "Result", rb_cData ); rb_include_module(rb_cPGresult, rb_mEnumerable); rb_include_module(rb_cPGresult, rb_mPGconstants); @@ -1526,4 +1615,7 @@ init_pg_result() rb_define_method(rb_cPGresult, "stream_each", pgresult_stream_each, 0); rb_define_method(rb_cPGresult, "stream_each_row", pgresult_stream_each_row, 0); rb_define_method(rb_cPGresult, "stream_each_tuple", pgresult_stream_each_tuple, 0); + + rb_define_method(rb_cPGresult, "field_name_type=", pgresult_field_name_type_set, 1 ); + rb_define_method(rb_cPGresult, "field_name_type", pgresult_field_name_type_get, 0 ); } diff --git a/ext/pg_tuple.c b/ext/pg_tuple.c index 29b98c1de..82a7ce2a4 100644 --- a/ext/pg_tuple.c +++ b/ext/pg_tuple.c @@ -212,7 +212,7 @@ pg_tuple_materialize(t_pg_tuple *this) * An integer +key+ is interpreted as column index. * Negative values of index count from the end of the array. * - * A string +key+ is interpreted as column name. + * Depending on Result#field_name_type= a string or symbol +key+ is interpreted as column name. * * If the key can't be found, there are several options: * With no other arguments, it will raise a IndexError exception; @@ -265,9 +265,16 @@ pg_tuple_fetch(int argc, VALUE *argv, VALUE self) /* * call-seq: - * res[ name ] -> value + * tup[ key ] -> value * - * Returns field _name_. + * Returns a field value by either column index or column name. + * + * An integer +key+ is interpreted as column index. + * Negative values of index count from the end of the array. + * + * Depending on Result#field_name_type= a string or symbol +key+ is interpreted as column name. + * + * If the key can't be found, it returns +nil+ . */ static VALUE pg_tuple_aref(VALUE self, VALUE key) diff --git a/lib/pg/result.rb b/lib/pg/result.rb index 506339b89..007007c14 100644 --- a/lib/pg/result.rb +++ b/lib/pg/result.rb @@ -9,12 +9,23 @@ class PG::Result # # +type_map+: a PG::TypeMap instance. # - # See PG::BasicTypeMapForResults + # This method is equal to #type_map= , but returns self, so that calls can be chained. + # + # See also PG::BasicTypeMapForResults def map_types!(type_map) self.type_map = type_map return self end + # Set the data type for all field name returning methods. + # + # +type+: a Symbol defining the field name type. + # + # This method is equal to #field_name_type= , but returns self, so that calls can be chained. + def field_names_as(type) + self.field_name_type = type + return self + end ### Return a String representation of the object suitable for debugging. def inspect diff --git a/pg.gemspec b/pg.gemspec index fd5c9fe97..67ba80c2e 100644 --- a/pg.gemspec +++ b/pg.gemspec @@ -1,26 +1,26 @@ # -*- encoding: utf-8 -*- -# stub: pg 1.2.0.pre20181226130406 ruby lib +# stub: pg 1.2.0.pre20191110215258 ruby lib # stub: ext/extconf.rb Gem::Specification.new do |s| s.name = "pg".freeze - s.version = "1.2.0.pre20181226130406" + s.version = "1.2.0.pre20191110215258" s.required_rubygems_version = Gem::Requirement.new("> 1.3.1".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Michael Granger".freeze, "Lars Kanis".freeze] s.cert_chain = ["certs/ged.pem".freeze] - s.date = "2018-12-26" + s.date = "2019-11-10" 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, "Manifest.txt".freeze, "README-OS_X.rdoc".freeze, "README-Windows.rdoc".freeze, "README.ja.rdoc".freeze, "README.rdoc".freeze, "ext/errorcodes.txt".freeze, "Contributors.rdoc".freeze, "History.rdoc".freeze, "README-OS_X.rdoc".freeze, "README-Windows.rdoc".freeze, "README.ja.rdoc".freeze, "README.rdoc".freeze, "POSTGRES".freeze, "LICENSE".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_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/util.c".freeze] - s.files = ["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_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/util.c".freeze, "ext/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.extra_rdoc_files = ["Contributors.rdoc".freeze, "History.rdoc".freeze, "Manifest.txt".freeze, "README-OS_X.rdoc".freeze, "README-Windows.rdoc".freeze, "README.ja.rdoc".freeze, "README.rdoc".freeze, "ext/errorcodes.txt".freeze, "Contributors.rdoc".freeze, "History.rdoc".freeze, "README-OS_X.rdoc".freeze, "README-Windows.rdoc".freeze, "README.ja.rdoc".freeze, "README.rdoc".freeze, "POSTGRES".freeze, "LICENSE".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 = ["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.licenses = ["BSD-3-Clause".freeze] s.rdoc_options = ["--main".freeze, "README.rdoc".freeze] - s.required_ruby_version = Gem::Requirement.new(">= 2.0.0".freeze) - s.rubygems_version = "3.0.1".freeze + s.required_ruby_version = Gem::Requirement.new(">= 2.2".freeze) + s.rubygems_version = "3.0.3".freeze s.summary = "Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]".freeze if s.respond_to? :specification_version then diff --git a/spec/pg/connection_spec.rb b/spec/pg/connection_spec.rb index 8246dc88f..f9d19a3d4 100644 --- a/spec/pg/connection_spec.rb +++ b/spec/pg/connection_spec.rb @@ -1824,6 +1824,40 @@ 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 it "should forward exec to exec_params" do res = @conn.exec("VALUES($1::INT)", [7]).values diff --git a/spec/pg/result_spec.rb b/spec/pg/result_spec.rb index 77b80524e..b01cfa81c 100644 --- a/spec/pg/result_spec.rb +++ b/spec/pg/result_spec.rb @@ -9,12 +9,60 @@ 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 = [] @@ -38,7 +86,15 @@ 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 @@ -55,6 +111,19 @@ 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 @@ -130,6 +199,17 @@ 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{ @@ -300,6 +380,32 @@ 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) )' ) @@ -397,8 +503,9 @@ 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(:x) }.to raise_error(TypeError) + expect{ res.field_values(0) }.to raise_error(TypeError) end it "can return the values of a single tuple" do @@ -519,6 +626,7 @@ 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 diff --git a/spec/pg/tuple_spec.rb b/spec/pg/tuple_spec.rb index f5b4676dc..2d100299a 100644 --- a/spec/pg/tuple_spec.rb +++ b/spec/pg/tuple_spec.rb @@ -8,10 +8,22 @@ describe PG::Tuple do let!(:typemap) { PG::BasicTypeMapForResults.new(@conn) } let!(:result2x2) { @conn.exec( "VALUES(1, 'a'), (2, 'b')" ) } - let!(:result2x3cast) { @conn.exec( "SELECT * FROM (VALUES(1, TRUE, '3'), (2, FALSE, '4')) AS m (a, b, b)" ).map_types!(typemap) } + 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 } @@ -51,9 +63,19 @@ 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 @@ -138,6 +160,12 @@ 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| @@ -174,16 +202,30 @@ 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) @@ -208,12 +250,22 @@ 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"]] ) @@ -222,7 +274,7 @@ end it "can be marshaled" do - [tuple0, tuple1, tuple2, tuple3].each do |t1| + [tuple0, tuple1, tuple2, tuple3, tuple0sym, tuple2sym].each do |t1| str = Marshal.dump(t1) t2 = Marshal.load(str) @@ -253,6 +305,7 @@ 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