ext/slim_attrib_ext.c in slim-attributes-0.4.1 vs ext/slim_attrib_ext.c in slim-attributes-0.5.0

- old
+ new

@@ -1,5 +1,8 @@ +// Author: Stephen Sykes +// http://pennysmalls.com + #include "ruby.h" #include "st.h" #include <mysql.h> #include <errmsg.h> @@ -8,23 +11,35 @@ #define GetMysqlRes(obj) (Check_Type(obj, T_DATA), ((struct mysql_res*)DATA_PTR(obj))->res) #define GetCharPtr(obj) (Check_Type(obj, T_DATA), (char*)DATA_PTR(obj)) #define GetCharStarPtr(obj) (Check_Type(obj, T_DATA), (char**)DATA_PTR(obj)) VALUE cRowHash, cClass; +ID pointers_id, row_info_id, field_indexes_id, real_hash_id, to_hash_id; +#define MAX_CACHED_COLUMN_IDS 40 +ID column_ids[MAX_CACHED_COLUMN_IDS]; + // from mysql/ruby struct mysql_res { MYSQL_RES* res; char freed; }; // row info #define SLIM_IS_NULL (char)0x01 #define SLIM_IS_SET (char)0x02 -#define GET_COL_IV_NAME(str, col_number) sprintf(str, "@col_%d", col_number) +#define GET_COL_IV_ID(str, cnum) (cnum < MAX_CACHED_COLUMN_IDS ? column_ids[cnum] : (sprintf(str, "@col_%ld", cnum), rb_intern(str))) +#define REAL_HASH_EXISTS NIL_P(field_indexes = rb_ivar_get(obj, field_indexes_id)) + +// This replaces the usual all_hashes method defined in mysql_adaptor.rb +// +// It copies the data from the mysql result into allocated memory +// ready for creating ruby strings from on demand, instead of creating +// all the data into ruby strings immediately. +// all_hashes returns an array of result rows static VALUE all_hashes(VALUE obj) { MYSQL_RES *res = GetMysqlRes(obj); MYSQL_FIELD *fields = mysql_fetch_fields(res); MYSQL_ROW row; VALUE all_hashes_ary, col_names_hsh, frh; @@ -32,113 +47,156 @@ unsigned int nf = mysql_num_fields(res); unsigned int i, j, s, len; unsigned long *lengths; char *row_info_space, **pointers_space, *p; - /* hash of column names */ + // make a hash of column names col_names_hsh = rb_hash_new(); for (i=0; i < nf; i++) { rb_hash_aset(col_names_hsh, rb_str_new2(fields[i].name), INT2FIX(i)); } - /* array of result rows */ + // make the array of result rows all_hashes_ary = rb_ary_new2(nr); + for (i=0; i < nr; i++) { - row = mysql_fetch_row(res); // get the row - lengths = mysql_fetch_lengths(res); // get lengths + row = mysql_fetch_row(res); // get the row data and lengths from mysql + lengths = mysql_fetch_lengths(res); for (s=j=0; j < nf; j++) s += lengths[j]; // s = total of lengths - pointers_space = malloc((nf + 1) * sizeof(char *) + s); // storage for pointers to data followed by data + pointers_space = ruby_xmalloc((nf + 1) * sizeof(char *) + s); // space for data pointers & data p = *pointers_space = (char *)(pointers_space + nf + 1); // pointer to first data item - row_info_space = calloc(nf, 1); + row_info_space = ruby_xcalloc(nf, 1); // space for flags for each column in this row for (j=0; j < nf; j++) { len = (unsigned int)lengths[j]; - if (!row[j]) row_info_space[j] = SLIM_IS_NULL; - else memcpy(p, row[j], len); // copy row data in - pointers_space[j + 1] = p += len; + if (len) { + memcpy(p, row[j], len); // copy row data in + p += len; + } else if (!row[j]) row_info_space[j] = SLIM_IS_NULL; // flag so we can handle null + pointers_space[j + 1] = p; } - frh = rb_class_new_instance(0, NULL, cRowHash); - rb_iv_set(frh, "@pointers", Data_Wrap_Struct(cClass, 0, free, pointers_space)); - rb_iv_set(frh, "@row_info", Data_Wrap_Struct(cClass, 0, free, row_info_space)); - rb_iv_set(frh, "@field_indexes", col_names_hsh); - rb_ary_store(all_hashes_ary, i, frh); + frh = rb_class_new_instance(0, NULL, cRowHash); // create the row object + rb_ivar_set(frh, pointers_id, Data_Wrap_Struct(cClass, 0, ruby_xfree, pointers_space)); + rb_ivar_set(frh, row_info_id, Data_Wrap_Struct(cClass, 0, ruby_xfree, row_info_space)); + rb_ivar_set(frh, field_indexes_id, col_names_hsh); + rb_ary_store(all_hashes_ary, i, frh); // store it in the array } return all_hashes_ary; } +// This does the actual work of creating a ruby string when one is demanded, typically through +// a call to an active record model property method. static VALUE fetch_by_index(VALUE obj, VALUE index) { VALUE contents; char *row_info, **pointers, *start, col_name[16]; + ID col_id; long col_number = FIX2LONG(index); unsigned int length; - - row_info = GetCharPtr(rb_iv_get(obj, "@row_info")) + col_number; + row_info = GetCharPtr(rb_ivar_get(obj, row_info_id)) + col_number; // flags for this column if (*row_info == SLIM_IS_NULL) return Qnil; // return nil if null from db - GET_COL_IV_NAME(col_name, col_number); - if (*row_info == SLIM_IS_SET) return rb_iv_get(obj, col_name); // was set already, return array entry - - pointers = GetCharStarPtr(rb_iv_get(obj, "@pointers")); + col_id = GET_COL_IV_ID(col_name, col_number); + if (*row_info == SLIM_IS_SET) return rb_ivar_get(obj, col_id); // was made to a string already + pointers = GetCharStarPtr(rb_ivar_get(obj, pointers_id)); // find the data and make ruby string start = pointers[col_number]; length = pointers[col_number + 1] - start; contents = rb_tainted_str_new(start, length); - rb_iv_set(obj, col_name, contents); + rb_ivar_set(obj, col_id, contents); // it is efficient to save the string in an instance variable *row_info = SLIM_IS_SET; return contents; } +// This is the [] method of the row data object. +// It checks for a real hash, but if none exists it will call fetch_by_index static VALUE slim_fetch(VALUE obj, VALUE name) { - VALUE real_hash, hash_lookup; - real_hash = rb_iv_get(obj, "@real_hash"); - if (!NIL_P(real_hash)) return rb_hash_aref(real_hash, name); - hash_lookup = rb_hash_aref(rb_iv_get(obj, "@field_indexes"), name); + VALUE field_indexes, hash_lookup; + + if (REAL_HASH_EXISTS) return rb_hash_aref(rb_ivar_get(obj, real_hash_id), name); + + hash_lookup = rb_hash_aref(field_indexes, name); if (NIL_P(hash_lookup)) return Qnil; return fetch_by_index(obj, hash_lookup); } +// This is the []= method of the row data object. +// It either operates on the real hash if it exists, or sets the appropriate +// column instance variable static VALUE set_element(VALUE obj, VALUE name, VALUE val) { - VALUE real_hash, hash_lookup; + VALUE field_indexes, hash_lookup; long col_number; char col_name[16]; + ID col_id; - real_hash = rb_iv_get(obj, "@real_hash"); - if (!NIL_P(real_hash)) return rb_hash_aset(real_hash, name, val); + if (REAL_HASH_EXISTS) return rb_hash_aset(rb_ivar_get(obj, real_hash_id), name, val); - hash_lookup = rb_hash_aref(rb_iv_get(obj, "@field_indexes"), name); - if (NIL_P(hash_lookup)) return rb_funcall(rb_funcall(obj, rb_intern("to_hash"), 0), rb_intern("[]="), 2, name, val); + hash_lookup = rb_hash_aref(field_indexes, name); + if (NIL_P(hash_lookup)) return rb_hash_aset(rb_funcall(obj, to_hash_id, 0), name, val); col_number = FIX2LONG(hash_lookup); - GET_COL_IV_NAME(col_name, col_number); - rb_iv_set(obj, col_name, val); - GetCharPtr(rb_iv_get(obj, "@row_info"))[col_number] = SLIM_IS_SET; + col_id = GET_COL_IV_ID(col_name, col_number); + rb_ivar_set(obj, col_id, val); + GetCharPtr(rb_ivar_get(obj, row_info_id))[col_number] = SLIM_IS_SET; return val; } +// This is the dup method of the row data object. +// When the query cache is used, the result of all_hashes is dupped before +// being passed back to the user. +// Subsequent queries of the same SQL will get another dup of the results. +// So we must implement dup in an efficient way (without converting to a real hash). +// +// Note: this method currently ignores any columns that have been assigned to using +// []= before calling dup (the original values will be seen in the dup). This works ok +// for active record usage, but perhaps could cause unexpected behaviour if model +// attributes are dupped by the user after changing them. static VALUE dup(VALUE obj) { - VALUE real_hash, frh, field_indexes; + VALUE frh, field_indexes; int nf, i; - char *row_info_space, col_name[16]; - - real_hash = rb_iv_get(obj, "@real_hash"); - if (!NIL_P(real_hash)) return rb_obj_dup(real_hash); + char *row_info_space; - field_indexes = rb_iv_get(obj, "@field_indexes"); + if (REAL_HASH_EXISTS) return rb_obj_dup(rb_ivar_get(obj, real_hash_id)); + nf = RHASH(field_indexes)->tbl->num_entries; - row_info_space = malloc(nf); - memcpy(row_info_space, GetCharPtr(rb_iv_get(obj, "@row_info")), nf); + row_info_space = ruby_xmalloc(nf); // dup needs its own set of flags + memcpy(row_info_space, GetCharPtr(rb_ivar_get(obj, row_info_id)), nf); for (i=0; i < nf; i++) row_info_space[i] &= ~SLIM_IS_SET; // remove any set flags - frh = rb_class_new_instance(0, NULL, cRowHash); - rb_iv_set(frh, "@pointers", rb_iv_get(obj, "@pointers")); - rb_iv_set(frh, "@row_info", Data_Wrap_Struct(cClass, 0, free, row_info_space)); - rb_iv_set(frh, "@field_indexes", field_indexes); + frh = rb_class_new_instance(0, NULL, cRowHash); // make the new row data object + rb_ivar_set(frh, pointers_id, rb_ivar_get(obj, pointers_id)); + rb_ivar_set(frh, row_info_id, Data_Wrap_Struct(cClass, 0, ruby_xfree, row_info_space)); + rb_ivar_set(frh, field_indexes_id, field_indexes); return frh; } +// This is the has_key? method of the row data object. +// Calls to model property methods in AR cause a call to has_key?, so it +// is implemented here in C for speed. +static VALUE has_key(VALUE obj, VALUE name) { + VALUE field_indexes; + + if (REAL_HASH_EXISTS) return (st_lookup(RHASH(rb_ivar_get(obj, real_hash_id))->tbl, name, 0) ? Qtrue : Qfalse); + else return (st_lookup(RHASH(field_indexes)->tbl, name, 0) ? Qtrue : Qfalse); +} + void Init_slim_attrib_ext() { + int i; + char col_name[16]; VALUE c = rb_cObject; + c = rb_const_get_at(c, rb_intern("Mysql")); c = rb_const_get_at(c, rb_intern("Result")); rb_define_method(c, "all_hashes", (VALUE(*)(ANYARGS))all_hashes, 0); cRowHash = rb_const_get_at(c, rb_intern("RowHash")); cClass = rb_define_class("CObjects", cRowHash); + // set up methods rb_define_private_method(cRowHash, "fetch_by_index", (VALUE(*)(ANYARGS))fetch_by_index, 1); rb_define_method(cRowHash, "[]", (VALUE(*)(ANYARGS))slim_fetch, 1); rb_define_method(cRowHash, "[]=", (VALUE(*)(ANYARGS))set_element, 2); rb_define_method(cRowHash, "dup", (VALUE(*)(ANYARGS))dup, 0); + rb_define_method(cRowHash, "has_key?", (VALUE(*)(ANYARGS))has_key, 1); + // set up some symbols that we will need + pointers_id = rb_intern("@pointers"); + row_info_id = rb_intern("@row_info"); + field_indexes_id = rb_intern("@field_indexes"); + real_hash_id = rb_intern("@real_hash"); + to_hash_id = rb_intern("to_hash"); + for(i=0; i < MAX_CACHED_COLUMN_IDS; i++) { + sprintf(col_name, "@col_%d", i); + column_ids[i] = rb_intern(col_name); + } }