Skip to content

Commit

Permalink
Add ability to use symbols as field names instead of strings
Browse files Browse the repository at this point in the history
This adds:
* PG::Result#field_name_type=
* PG::Result#field_name_type
* PG::Result#field_names_as

Symbol comparison and lookup is faster, since the string hash is generated at Symbol creation.
So retrieval of Symbol based field names is somewhat slower, but this is easily outperformed in frameworks that do a lot of lookups.

Fixes #278
  • Loading branch information
larskanis committed Nov 10, 2019
1 parent 435990e commit d63dbd0
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 18 deletions.
5 changes: 4 additions & 1 deletion ext/pg.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -135,6 +135,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.
*/
Expand Down
117 changes: 104 additions & 13 deletions ext/pg_result.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@

#include "pg.h"

#define PG_RESULT_FIELD_NAMES_MASK 0x03
#define PG_RESULT_FIELD_NAMES_SYMBOL 0x01
#define PG_RESULT_FIELD_NAMES_STATIC_SYMBOL 0x02

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 );
Expand Down Expand Up @@ -190,6 +194,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 ){
Expand Down Expand Up @@ -390,6 +395,27 @@ pgresult_get(VALUE self)
return this->pgresult;
}

static VALUE pg_cstr_to_sym(char *cstr, unsigned int flags, int enc_idx)
{
VALUE fname;
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 ){
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)
{
Expand All @@ -400,12 +426,9 @@ static void pgresult_init_fnames(VALUE self)
int nfields = PQnfields(this->pgresult);

for( i=0; i<nfields; i++ ){
VALUE fname = rb_tainted_str_new2(PQfname(this->pgresult, 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;
}
Expand Down Expand Up @@ -583,24 +606,23 @@ 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_.
*/
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);
}

/*
Expand Down Expand Up @@ -1101,8 +1123,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 );
Expand Down Expand Up @@ -1209,7 +1235,7 @@ pgresult_each(VALUE self)
* call-seq:
* res.fields() -> Array
*
* Returns an array of Strings representing the names of the fields in the result.
* Returns an array of strings or symbols representing the names of the fields in the result.
*/
static VALUE
pgresult_fields(VALUE self)
Expand Down Expand Up @@ -1309,7 +1335,9 @@ yield_tuple(VALUE self, int ntuples, int nfields)
int tuple_num;
t_pg_result *this = pgresult_get_this(self);
VALUE result = pg_new_result(this->pgresult, this->connection);
t_pg_result *newthis = pgresult_get_this(result);
UNUSED(nfields);
newthis->flags = this->flags;

for(tuple_num = 0; tuple_num < ntuples; tuple_num++) {
VALUE tuple = pgresult_tuple(result, INT2FIX(tuple_num));
Expand Down Expand Up @@ -1436,10 +1464,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 +: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 = DATA_PTR(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 = DATA_PTR(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);
Expand Down Expand Up @@ -1496,4 +1584,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 );
}
13 changes: 12 additions & 1 deletion lib/pg/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit d63dbd0

Please sign in to comment.