Add cascading options hash at Mysql2::Client.default_query_options, which can be overridden by passing options to Mysql2::Client#query and/or Mysql2::Result#each

Tune up specs, benchmarks and AR adapter to conform
This commit is contained in:
Brian Lopez 2010-08-01 20:20:48 -07:00
parent c6230c3cec
commit 36c243be71
10 changed files with 165 additions and 59 deletions

View File

@ -45,8 +45,8 @@ Benchmark.bmbm do |x|
x.report do x.report do
puts "Mysql2" puts "Mysql2"
number_of.times do number_of.times do
mysql2_result = mysql2.query sql mysql2_result = mysql2.query sql, :symbolize_keys => true
mysql2_result.each(:symbolize_keys => true) do |res| mysql2_result.each do |res|
# puts res.inspect # puts res.inspect
end end
end end

View File

@ -17,8 +17,8 @@ Benchmark.bmbm do |x|
x.report do x.report do
puts "Mysql2" puts "Mysql2"
number_of.times do number_of.times do
mysql2_result = mysql2.query sql mysql2_result = mysql2.query sql, :symbolize_keys => true
mysql2_result.each(:symbolize_keys => true) do |res| mysql2_result.each do |res|
# puts res.inspect # puts res.inspect
end end
end end

View File

@ -3,7 +3,8 @@
VALUE cMysql2Client; VALUE cMysql2Client;
extern VALUE mMysql2, cMysql2Error, intern_encoding_from_charset; extern VALUE mMysql2, cMysql2Error, intern_encoding_from_charset;
extern ID sym_id, sym_version, sym_async; extern ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array;
extern ID intern_merge;
#define REQUIRE_OPEN_DB(_ctxt) \ #define REQUIRE_OPEN_DB(_ctxt) \
if(!_ctxt->net.vio) { \ if(!_ctxt->net.vio) { \
@ -225,7 +226,12 @@ static VALUE rb_mysql_client_async_result(VALUE self) {
} }
VALUE resultObj = rb_mysql_result_to_obj(result); VALUE resultObj = rb_mysql_result_to_obj(result);
// pass-through query options for result construction later
rb_iv_set(resultObj, "@query_options", rb_obj_dup(rb_iv_get(self, "@query_options")));
#ifdef HAVE_RUBY_ENCODING_H
rb_iv_set(resultObj, "@encoding", rb_iv_get(self, "@encoding")); rb_iv_set(resultObj, "@encoding", rb_iv_get(self, "@encoding"));
#endif
return resultObj; return resultObj;
} }
@ -234,29 +240,31 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) {
fd_set fdset; fd_set fdset;
int fd, retval; int fd, retval;
int async = 0; int async = 0;
VALUE opts; VALUE opts, defaults;
VALUE rb_async;
MYSQL *client; MYSQL *client;
Data_Get_Struct(self, MYSQL, client);
REQUIRE_OPEN_DB(client);
args.mysql = client;
defaults = rb_iv_get(self, "@query_options");
if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) { if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) {
if ((rb_async = rb_hash_aref(opts, sym_async)) != Qnil) { opts = rb_funcall(defaults, intern_merge, 1, opts);
async = rb_async == Qtrue ? 1 : 0; rb_iv_set(self, "@query_options", opts);
if (rb_hash_aref(opts, sym_async) == Qtrue) {
async = 1;
} }
} else {
opts = defaults;
} }
Check_Type(args.sql, T_STRING);
#ifdef HAVE_RUBY_ENCODING_H #ifdef HAVE_RUBY_ENCODING_H
rb_encoding *conn_enc = rb_to_encoding(rb_iv_get(self, "@encoding")); rb_encoding *conn_enc = rb_to_encoding(rb_iv_get(self, "@encoding"));
// ensure the string is in the encoding the connection is expecting // ensure the string is in the encoding the connection is expecting
args.sql = rb_str_export_to_enc(args.sql, conn_enc); args.sql = rb_str_export_to_enc(args.sql, conn_enc);
#endif #endif
Data_Get_Struct(self, MYSQL, client);
REQUIRE_OPEN_DB(client);
args.mysql = client;
if (rb_thread_blocking_region(nogvl_send_query, &args, RUBY_UBF_IO, 0) == Qfalse) { if (rb_thread_blocking_region(nogvl_send_query, &args, RUBY_UBF_IO, 0) == Qfalse) {
return rb_raise_mysql2_error(client); return rb_raise_mysql2_error(client);
} }
@ -280,7 +288,12 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) {
} }
} }
return rb_mysql_client_async_result(self); VALUE result = rb_mysql_client_async_result(self);
// pass-through query options for result construction later
rb_iv_set(result, "@query_options", rb_obj_dup(opts));
return result;
} else { } else {
return Qnil; return Qnil;
} }

View File

@ -1,16 +1,22 @@
#include <mysql2_ext.h> #include <mysql2_ext.h>
VALUE mMysql2, cMysql2Error, intern_encoding_from_charset; VALUE mMysql2, cMysql2Error, intern_encoding_from_charset;
ID sym_id, sym_version, sym_async; ID sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array;
ID intern_merge;
/* Ruby Extension initializer */ /* Ruby Extension initializer */
void Init_mysql2() { void Init_mysql2() {
mMysql2 = rb_define_module("Mysql2"); mMysql2 = rb_define_module("Mysql2");
cMysql2Error = rb_const_get(mMysql2, rb_intern("Error")); cMysql2Error = rb_const_get(mMysql2, rb_intern("Error"));
intern_merge = rb_intern("merge");
sym_array = ID2SYM(rb_intern("array"));
sym_as = ID2SYM(rb_intern("as"));
sym_id = ID2SYM(rb_intern("id")); sym_id = ID2SYM(rb_intern("id"));
sym_version = ID2SYM(rb_intern("version")); sym_version = ID2SYM(rb_intern("version"));
sym_async = ID2SYM(rb_intern("async")); sym_async = ID2SYM(rb_intern("async"));
sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys"));
intern_encoding_from_charset = rb_intern("encoding_from_charset"); intern_encoding_from_charset = rb_intern("encoding_from_charset");

View File

@ -4,12 +4,13 @@
rb_encoding *binaryEncoding; rb_encoding *binaryEncoding;
#endif #endif
ID sym_symbolize_keys;
ID intern_new, intern_utc, intern_encoding_from_charset_code; ID intern_new, intern_utc, intern_encoding_from_charset_code;
VALUE cMysql2Result; VALUE cMysql2Result;
VALUE cBigDecimal, cDate, cDateTime; VALUE cBigDecimal, cDate, cDateTime;
extern VALUE mMysql2, cMysql2Client, cMysql2Error, intern_encoding_from_charset; extern VALUE mMysql2, cMysql2Client, cMysql2Error, intern_encoding_from_charset;
extern ID sym_symbolize_keys, sym_as, sym_array;
extern ID intern_merge;
static void rb_mysql_result_mark(void * wrapper) { static void rb_mysql_result_mark(void * wrapper) {
mysql2_result_wrapper * w = wrapper; mysql2_result_wrapper * w = wrapper;
@ -85,12 +86,13 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int
return rb_field; return rb_field;
} }
static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) { static VALUE rb_mysql_result_fetch_row(VALUE self, VALUE opts) {
VALUE rowHash, opts, block; VALUE rowVal;
mysql2_result_wrapper * wrapper; mysql2_result_wrapper * wrapper;
MYSQL_ROW row; MYSQL_ROW row;
MYSQL_FIELD * fields = NULL; MYSQL_FIELD * fields = NULL;
unsigned int i = 0, symbolizeKeys = 0; unsigned int i = 0;
int symbolizeKeys = 0, asArray = 0;
unsigned long * fieldLengths; unsigned long * fieldLengths;
void * ptr; void * ptr;
#ifdef HAVE_RUBY_ENCODING_H #ifdef HAVE_RUBY_ENCODING_H
@ -100,11 +102,12 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
GetMysql2Result(self, wrapper); GetMysql2Result(self, wrapper);
if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) {
Check_Type(opts, T_HASH);
if (rb_hash_aref(opts, sym_symbolize_keys) == Qtrue) { if (rb_hash_aref(opts, sym_symbolize_keys) == Qtrue) {
symbolizeKeys = 1; symbolizeKeys = 1;
} }
if (rb_hash_aref(opts, sym_as) == sym_array) {
asArray = 1;
} }
ptr = wrapper->result; ptr = wrapper->result;
@ -113,7 +116,11 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
return Qnil; return Qnil;
} }
rowHash = rb_hash_new(); if (asArray) {
rowVal = rb_ary_new2(wrapper->numberOfFields);
} else {
rowVal = rb_hash_new();
}
fields = mysql_fetch_fields(wrapper->result); fields = mysql_fetch_fields(wrapper->result);
fieldLengths = mysql_fetch_lengths(wrapper->result); fieldLengths = mysql_fetch_lengths(wrapper->result);
if (wrapper->fields == Qnil) { if (wrapper->fields == Qnil) {
@ -220,12 +227,20 @@ static VALUE rb_mysql_result_fetch_row(int argc, VALUE * argv, VALUE self) {
#endif #endif
break; break;
} }
rb_hash_aset(rowHash, field, val); if (asArray) {
rb_ary_push(rowVal, val);
} else { } else {
rb_hash_aset(rowHash, field, Qnil); rb_hash_aset(rowVal, field, val);
}
} else {
if (asArray) {
rb_ary_push(rowVal, Qnil);
} else {
rb_hash_aset(rowVal, field, Qnil);
} }
} }
return rowHash; }
return rowVal;
} }
static VALUE rb_mysql_result_fetch_fields(VALUE self) { static VALUE rb_mysql_result_fetch_fields(VALUE self) {
@ -249,18 +264,24 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) {
} }
static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
VALUE opts, block; VALUE defaults, opts, block;
mysql2_result_wrapper * wrapper; mysql2_result_wrapper * wrapper;
unsigned long i; unsigned long i;
GetMysql2Result(self, wrapper); GetMysql2Result(self, wrapper);
rb_scan_args(argc, argv, "01&", &opts, &block); defaults = rb_iv_get(self, "@query_options");
if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) {
opts = rb_funcall(defaults, intern_merge, 1, opts);
} else {
opts = defaults;
}
if (wrapper->lastRowProcessed == 0) { if (wrapper->lastRowProcessed == 0) {
wrapper->numberOfRows = mysql_num_rows(wrapper->result); wrapper->numberOfRows = mysql_num_rows(wrapper->result);
if (wrapper->numberOfRows == 0) { if (wrapper->numberOfRows == 0) {
return Qnil; wrapper->rows = rb_ary_new();
return wrapper->rows;
} }
wrapper->rows = rb_ary_new2(wrapper->numberOfRows); wrapper->rows = rb_ary_new2(wrapper->numberOfRows);
} }
@ -279,7 +300,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
if (i < rowsProcessed) { if (i < rowsProcessed) {
row = rb_ary_entry(wrapper->rows, i); row = rb_ary_entry(wrapper->rows, i);
} else { } else {
row = rb_mysql_result_fetch_row(argc, argv, self); row = rb_mysql_result_fetch_row(self, opts);
rb_ary_store(wrapper->rows, i, row); rb_ary_store(wrapper->rows, i, row);
wrapper->lastRowProcessed++; wrapper->lastRowProcessed++;
} }
@ -319,8 +340,7 @@ VALUE rb_mysql_result_to_obj(MYSQL_RES * r) {
return obj; return obj;
} }
void init_mysql2_result() void init_mysql2_result() {
{
cBigDecimal = rb_const_get(rb_cObject, rb_intern("BigDecimal")); cBigDecimal = rb_const_get(rb_cObject, rb_intern("BigDecimal"));
cDate = rb_const_get(rb_cObject, rb_intern("Date")); cDate = rb_const_get(rb_cObject, rb_intern("Date"));
cDateTime = rb_const_get(rb_cObject, rb_intern("DateTime")); cDateTime = rb_const_get(rb_cObject, rb_intern("DateTime"));
@ -329,7 +349,6 @@ void init_mysql2_result()
rb_define_method(cMysql2Result, "each", rb_mysql_result_each, -1); rb_define_method(cMysql2Result, "each", rb_mysql_result_each, -1);
rb_define_method(cMysql2Result, "fields", rb_mysql_result_fetch_fields, 0); rb_define_method(cMysql2Result, "fields", rb_mysql_result_fetch_fields, 0);
sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys"));
intern_new = rb_intern("new"); intern_new = rb_intern("new");
intern_utc = rb_intern("utc"); intern_utc = rb_intern("utc");
intern_encoding_from_charset_code = rb_intern("encoding_from_charset_code"); intern_encoding_from_charset_code = rb_intern("encoding_from_charset_code");

View File

@ -161,6 +161,7 @@ module ActiveRecord
super(connection, logger) super(connection, logger)
@connection_options, @config = connection_options, config @connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {} @quoted_column_names, @quoted_table_names = {}, {}
configure_connection
end end
def adapter_name def adapter_name
@ -263,14 +264,36 @@ module ActiveRecord
# DATABASE STATEMENTS ====================================== # DATABASE STATEMENTS ======================================
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(sql, name = nil)
result = execute(sql, name)
result.each(:as => :hash) do |r|
return r
end
end
# Returns a single value from a record
def select_value(sql, name = nil)
result = execute(sql, name)
if first = result.first
first.first
end
end
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(sql, name = nil) def select_values(sql, name = nil)
select(sql, name).map { |row| row.values.first } execute(sql, name).map { |row| row.first }
end end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
def select_rows(sql, name = nil) def select_rows(sql, name = nil)
select(sql, name).map { |row| row.values } execute(sql, name).to_a
end end
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil) def execute(sql, name = nil)
if name == :skip_logging if name == :skip_logging
@connection.query(sql) @connection.query(sql)
@ -393,8 +416,8 @@ module ActiveRecord
def tables(name = nil) def tables(name = nil)
tables = [] tables = []
execute("SHOW TABLES", name).each(:symbolize_keys => true) do |field| execute("SHOW TABLES", name).each do |field|
tables << field.values.first tables << field.first
end end
tables tables
end end
@ -407,7 +430,7 @@ module ActiveRecord
indexes = [] indexes = []
current_index = nil current_index = nil
result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
result.each(:symbolize_keys => true) do |row| result.each(:symbolize_keys => true, :as => :hash) do |row|
if current_index != row[:Key_name] if current_index != row[:Key_name]
next if row[:Key_name] == PRIMARY # skip the primary key next if row[:Key_name] == PRIMARY # skip the primary key
current_index = row[:Key_name] current_index = row[:Key_name]
@ -423,7 +446,7 @@ module ActiveRecord
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
columns = [] columns = []
result = execute(sql, :skip_logging) result = execute(sql, :skip_logging)
result.each(:symbolize_keys => true) { |field| result.each(:symbolize_keys => true, :as => :hash) { |field|
columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
} }
columns columns
@ -520,7 +543,7 @@ module ActiveRecord
def pk_and_sequence_for(table) def pk_and_sequence_for(table)
keys = [] keys = []
result = execute("describe #{quote_table_name(table)}") result = execute("describe #{quote_table_name(table)}")
result.each(:symbolize_keys => true) do |row| result.each(:symbolize_keys => true, :as => :hash) do |row|
keys << row[:Field] if row[:Key] == "PRI" keys << row[:Field] if row[:Key] == "PRI"
end end
keys.length == 1 ? [keys.first, nil] : nil keys.length == 1 ? [keys.first, nil] : nil
@ -574,6 +597,7 @@ module ActiveRecord
end end
def configure_connection def configure_connection
@connection.query_options.merge!(:as => :array)
encoding = @config[:encoding] encoding = @config[:encoding]
execute("SET NAMES '#{encoding}'", :skip_logging) if encoding execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
@ -582,8 +606,10 @@ module ActiveRecord
execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
end end
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select(sql, name = nil) def select(sql, name = nil)
execute(sql, name).to_a result = execute(sql, name).each(:as => :hash)
end end
def supports_views? def supports_views?

View File

@ -1,6 +1,15 @@
module Mysql2 module Mysql2
class Client class Client
def initialize opts = {} attr_reader :query_options
@@default_query_options = {
:symbolize_keys => false,
:async => false,
:as => :hash
}
def initialize(opts = {})
@query_options = @@default_query_options.dup
init_connection init_connection
[:reconnect, :connect_timeout].each do |key| [:reconnect, :connect_timeout].each do |key|
@ -23,6 +32,10 @@ module Mysql2
connect user, pass, host, port, database, socket connect user, pass, host, port, database, socket
end end
def self.default_query_options
@@default_query_options
end
# NOTE: from ruby-mysql # NOTE: from ruby-mysql
if defined? Encoding if defined? Encoding
CHARSET_MAP = { CHARSET_MAP = {

View File

@ -21,8 +21,8 @@ describe ActiveRecord::ConnectionAdapters::Mysql2Adapter do
end end
it "should be able to execute a raw query" do it "should be able to execute a raw query" do
@connection.execute("SELECT 1 as one").first['one'].should eql(1) @connection.execute("SELECT 1 as one").first.first.should eql(1)
@connection.execute("SELECT NOW() as n").first['n'].class.should eql(Time) @connection.execute("SELECT NOW() as n").first.first.class.should eql(Time)
end end
end end
@ -72,7 +72,7 @@ describe ActiveRecord::ConnectionAdapters::Mysql2Adapter do
end end
after(:all) do after(:all) do
Mysql2Test2.connection.execute("DELETE FROM mysql2_test WHERE id=#{@test_result['id']}") Mysql2Test2.connection.execute("DELETE FROM mysql2_test WHERE id=#{@test_result.first}")
end end
it "default value should be cast to the expected type of the field" do it "default value should be cast to the expected type of the field" do

View File

@ -14,6 +14,10 @@ describe Mysql2::Client do
end end
end end
it "should have a global default_query_options hash" do
Mysql2::Client.should respond_to(:default_query_options)
end
it "should be able to connect via SSL options" do it "should be able to connect via SSL options" do
pending("DON'T WORRY, THIS TEST PASSES :) - but is machine-specific. You need to have MySQL running with SSL configured and enabled. Then update the paths in this test to your needs and remove the pending state.") pending("DON'T WORRY, THIS TEST PASSES :) - but is machine-specific. You need to have MySQL running with SSL configured and enabled. Then update the paths in this test to your needs and remove the pending state.")
ssl_client = nil ssl_client = nil
@ -49,6 +53,26 @@ describe Mysql2::Client do
@client.should respond_to(:query) @client.should respond_to(:query)
end end
context "#query" do
it "should accept an options hash that inherits from Mysql2::Client.default_query_options" do
@client.query "SELECT 1", :something => :else
@client.query_options.should eql(@client.query_options.merge(:something => :else))
end
it "should return results as a hash by default" do
@client.query("SELECT 1").first.class.should eql(Hash)
end
it "should be able to return results as an array" do
@client.query("SELECT 1", :as => :array).first.class.should eql(Array)
@client.query("SELECT 1").each(:as => :array)
end
it "should be able to return results with symbolized keys" do
@client.query("SELECT 1", :symbolize_keys => true).first.keys[0].class.should eql(Symbol)
end
end
it "should respond to #escape" do it "should respond to #escape" do
@client.should respond_to(:escape) @client.should respond_to(:escape)
end end

View File

@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Mysql2::Result do describe Mysql2::Result do
before(:all) do before(:each) do
@client = Mysql2::Client.new :host => "localhost", :username => "root" @client = Mysql2::Client.new :host => "localhost", :username => "root"
end end
@ -37,18 +37,23 @@ describe Mysql2::Result do
it "should yield rows as hash's with symbol keys if :symbolize_keys was set to true" do it "should yield rows as hash's with symbol keys if :symbolize_keys was set to true" do
@result.each(:symbolize_keys => true) do |row| @result.each(:symbolize_keys => true) do |row|
row.class.should eql(Hash)
row.keys.first.class.should eql(Symbol) row.keys.first.class.should eql(Symbol)
end end
end end
it "should be able to return results as an array" do
@result.each(:as => :array) do |row|
row.class.should eql(Array)
end
end
it "should cache previously yielded results" do it "should cache previously yielded results" do
@result.first.should eql(@result.first) @result.first.should eql(@result.first)
end end
end end
context "#fields" do context "#fields" do
before(:all) do before(:each) do
@client.query "USE test" @client.query "USE test"
@test_result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1") @test_result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1")
end end
@ -64,7 +69,7 @@ describe Mysql2::Result do
end end
context "row data type mapping" do context "row data type mapping" do
before(:all) do before(:each) do
@client.query "USE test" @client.query "USE test"
@client.query %[ @client.query %[
CREATE TABLE IF NOT EXISTS mysql2_test ( CREATE TABLE IF NOT EXISTS mysql2_test (