Skip to content

Commit

Permalink
add cache_rows option to enable/disable internal row caching for results
Browse files Browse the repository at this point in the history
  • Loading branch information
brianmario committed Aug 27, 2010
1 parent 10222fb commit ae6c33a
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 16 deletions.
16 changes: 13 additions & 3 deletions README.rdoc
Expand Up @@ -100,7 +100,7 @@ The default result type is set to :hash, but you can override a previous setting
I may add support for :as => :csv or even :as => :json to allow for *much* more efficient generation of those data types from result sets.
If you'd like to see either of these (or others), open an issue and start bugging me about it ;)

== Timezones
=== Timezones

Mysql2 now supports two timezone options:

Expand All @@ -112,14 +112,14 @@ Then, if :application_timezone is set to say - :local - Mysql2 will then convert

Both options only allow two values - :local or :utc - with the exception that :application_timezone can be [and defaults to] nil

== Casting "boolean" columns
=== Casting "boolean" columns

You can now tell Mysql2 to cast tinyint(1) fields to boolean values in Ruby with the :cast_booleans option.

client = Mysql2::Client.new
result = client.query("SELECT * FROM table_with_boolean_field", :cast_booleans => true)

== Async
=== Async

Mysql2::Client takes advantage of the MySQL C API's (undocumented) non-blocking function mysql_send_query for *all* queries.
But, in order to take full advantage of it in your Ruby code, you can do:
Expand All @@ -136,6 +136,14 @@ NOTE: Because of the way MySQL's query API works, this method will block until t
So if you really need things to stay async, it's best to just monitor the socket with something like EventMachine.
If you need multiple query concurrency take a look at using a connection pool.

=== Row Caching

By default, Mysql2 will cache rows that have been created in Ruby (since this happens lazily).
This is especially helpful since it saves the cost of creating the row in Ruby if you were to iterate over the collection again.

If you only plan on using each row once, then it's much more efficient to disable this behavior by setting the :cache_rows option to false.
This would be helpful if you wanted to iterate over the results in a streaming manner. Meaning the GC would cleanup rows you don't need anymore as you're iterating over the result set.

== ActiveRecord

To use the ActiveRecord driver, all you should need to do is have this gem installed and set the adapter in your database.yml to "mysql2".
Expand Down Expand Up @@ -182,6 +190,8 @@ For example, if you were to yield 4 rows from a 100 row dataset, only 4 hashes w
Now say you were to iterate over that same collection again, this time yielding 15 rows - the 4 previous rows that had already been turned into ruby hashes would be pulled from an internal cache, then 11 more would be created and stored in that cache.
Once the entire dataset has been converted into ruby objects, Mysql2::Result will free the Mysql C result object as it's no longer needed.

This caching behavior can be disabled by setting the :cache_rows option to false.

As for field values themselves, I'm workin on it - but expect that soon.

== Compatibility
Expand Down
17 changes: 12 additions & 5 deletions ext/mysql2/result.c
Expand Up @@ -12,7 +12,7 @@ static VALUE intern_encoding_from_charset;
static ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code,
intern_localtime, intern_local_offset, intern_civil, intern_new_offset;
static ID sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, sym_application_timezone,
sym_local, sym_utc, sym_cast_booleans;
sym_local, sym_utc, sym_cast_booleans, sym_cache_rows;
static ID intern_merge;

static void rb_mysql_result_mark(void * wrapper) {
Expand Down Expand Up @@ -316,7 +316,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
ID db_timezone, app_timezone, dbTz, appTz;
mysql2_result_wrapper * wrapper;
unsigned long i;
int symbolizeKeys = 0, asArray = 0, castBool = 0;
int symbolizeKeys = 0, asArray = 0, castBool = 0, cacheRows = 1;

GetMysql2Result(self, wrapper);

Expand All @@ -339,6 +339,10 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
castBool = 1;
}

if (rb_hash_aref(opts, sym_cache_rows) == Qfalse) {
cacheRows = 0;
}

dbTz = rb_hash_aref(opts, sym_database_timezone);
if (dbTz == sym_local) {
db_timezone = intern_local;
Expand Down Expand Up @@ -369,7 +373,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
wrapper->rows = rb_ary_new2(wrapper->numberOfRows);
}

if (wrapper->lastRowProcessed == wrapper->numberOfRows) {
if (cacheRows && wrapper->lastRowProcessed == wrapper->numberOfRows) {
// we've already read the entire dataset from the C result into our
// internal array. Lets hand that over to the user since it's ready to go
for (i = 0; i < wrapper->numberOfRows; i++) {
Expand All @@ -380,11 +384,13 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
rowsProcessed = RARRAY_LEN(wrapper->rows);
for (i = 0; i < wrapper->numberOfRows; i++) {
VALUE row;
if (i < rowsProcessed) {
if (cacheRows && i < rowsProcessed) {
row = rb_ary_entry(wrapper->rows, i);
} else {
row = rb_mysql_result_fetch_row(self, db_timezone, app_timezone, symbolizeKeys, asArray, castBool);
rb_ary_store(wrapper->rows, i, row);
if (cacheRows) {
rb_ary_store(wrapper->rows, i, row);
}
wrapper->lastRowProcessed++;
}

Expand Down Expand Up @@ -453,6 +459,7 @@ void init_mysql2_result() {
sym_cast_booleans = ID2SYM(rb_intern("cast_booleans"));
sym_database_timezone = ID2SYM(rb_intern("database_timezone"));
sym_application_timezone = ID2SYM(rb_intern("application_timezone"));
sym_cache_rows = ID2SYM(rb_intern("cache_rows"));

rb_global_variable(&opt_decimal_zero); //never GC
opt_decimal_zero = rb_str_new2("0.0");
Expand Down
13 changes: 7 additions & 6 deletions lib/mysql2/client.rb
Expand Up @@ -2,12 +2,13 @@ module Mysql2
class Client
attr_reader :query_options
@@default_query_options = {
:as => :hash,
:async => false,
:cast_booleans => false,
:symbolize_keys => false,
:database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in
:application_timezone => nil # timezone Mysql2 will convert to before handing the object back to the caller
:as => :hash, # the type of object you want each row back as; also supports :array (an array of values)
:async => false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result
:cast_booleans => false, # cast tinyint(1) fields as true/false in ruby
:symbolize_keys => false, # return field names as symbols instead of strings
:database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in
:application_timezone => nil, # timezone Mysql2 will convert to before handing the object back to the caller
:cache_rows => true # tells Mysql2 to use it's internal row cache for results
}

def initialize(opts = {})
Expand Down
9 changes: 7 additions & 2 deletions spec/mysql2/result_spec.rb
Expand Up @@ -47,8 +47,13 @@
end
end

it "should cache previously yielded results" do
@result.first.should eql(@result.first)
it "should cache previously yielded results by default" do
@result.first.object_id.should eql(@result.first.object_id)
end

it "should not cache previously yielded results if cache_rows is disabled" do
result = @client.query "SELECT 1", :cache_rows => false
result.first.object_id.should_not eql(result.first.object_id)
end
end

Expand Down

0 comments on commit ae6c33a

Please sign in to comment.