Skip to content

Allow query options to be set on each Statement object #640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Metrics/BlockNesting:
# Offense count: 1
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 125
Max: 128

# Offense count: 3
Metrics/CyclomaticComplexity:
Expand Down
6 changes: 3 additions & 3 deletions ext/mysql2/client.c
Original file line number Diff line number Diff line change
Expand Up @@ -1341,11 +1341,11 @@ static VALUE initialize_ext(VALUE self) {
*
* Create a new prepared statement.
*/
static VALUE rb_mysql_client_prepare_statement(VALUE self, VALUE sql) {
static VALUE rb_mysql_prepare_statement(VALUE self, VALUE sql, VALUE options) {
GET_CLIENT(self);
REQUIRE_CONNECTED(wrapper);

return rb_mysql_stmt_new(self, sql);
return rb_mysql_stmt_new(self, sql, options);
}

void init_mysql2_client() {
Expand Down Expand Up @@ -1393,7 +1393,6 @@ void init_mysql2_client() {
rb_define_method(cMysql2Client, "async_result", rb_mysql_client_async_result, 0);
rb_define_method(cMysql2Client, "last_id", rb_mysql_client_last_id, 0);
rb_define_method(cMysql2Client, "affected_rows", rb_mysql_client_affected_rows, 0);
rb_define_method(cMysql2Client, "prepare", rb_mysql_client_prepare_statement, 1);
rb_define_method(cMysql2Client, "thread_id", rb_mysql_client_thread_id, 0);
rb_define_method(cMysql2Client, "ping", rb_mysql_client_ping, 0);
rb_define_method(cMysql2Client, "select_db", rb_mysql_client_select_db, 1);
Expand Down Expand Up @@ -1423,6 +1422,7 @@ void init_mysql2_client() {
rb_define_private_method(cMysql2Client, "initialize_ext", initialize_ext, 0);
rb_define_private_method(cMysql2Client, "connect", rb_mysql_connect, 8);
rb_define_private_method(cMysql2Client, "_query", rb_mysql_query, 2);
rb_define_private_method(cMysql2Client, "_prepare", rb_mysql_prepare_statement, 2);

sym_id = ID2SYM(rb_intern("id"));
sym_version = ID2SYM(rb_intern("version"));
Expand Down
8 changes: 6 additions & 2 deletions ext/mysql2/statement.c
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,13 @@ static void *nogvl_prepare_statement(void *ptr) {
}
}

VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) {
VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql, VALUE options) {
mysql_stmt_wrapper *stmt_wrapper;
VALUE rb_stmt;
rb_encoding *conn_enc;

Check_Type(sql, T_STRING);
Check_Type(options, T_HASH);

rb_stmt = Data_Make_Struct(cMysql2Statement, mysql_stmt_wrapper, rb_mysql_stmt_mark, rb_mysql_stmt_free, stmt_wrapper);
{
Expand All @@ -106,6 +107,9 @@ VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) {
stmt_wrapper->stmt = NULL;
}

rb_obj_call_init(rb_stmt, 0, NULL);
rb_iv_set(rb_stmt, "@query_options", rb_hash_dup(options));

// instantiate stmt
{
GET_CLIENT(rb_client);
Expand Down Expand Up @@ -419,7 +423,7 @@ static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) {
return Qnil;
}

current = rb_hash_dup(rb_iv_get(stmt_wrapper->client, "@query_options"));
current = rb_iv_get(self, "@query_options");
(void)RB_GC_GUARD(current);
Check_Type(current, T_HASH);

Expand Down
2 changes: 1 addition & 1 deletion ext/mysql2/statement.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ typedef struct {
void init_mysql2_statement(void);
void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper);

VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql);
VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql, VALUE options);
void rb_raise_mysql2_stmt_error(mysql_stmt_wrapper *stmt_wrapper) RB_MYSQL_NORETURN;

#endif
4 changes: 4 additions & 0 deletions lib/mysql2/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ def query(sql, options = {})
end
end

def prepare(sql, options = {})
_prepare(sql, @query_options.merge(options))
end

def query_info
info = query_info_string
return {} unless info
Expand Down
1 change: 1 addition & 0 deletions lib/mysql2/statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

module Mysql2
class Statement
attr_reader :query_options
include Enumerable

if Thread.respond_to?(:handle_interrupt)
Expand Down
82 changes: 46 additions & 36 deletions spec/mysql2/statement_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ def stmt_count
expect(result.to_a).to eq(['max1' => int64_max1, 'max2' => int64_max2, 'max3' => int64_max3, 'min1' => int64_min1, 'min2' => int64_min2, 'min3' => int64_min3])
end

it "should keep query options per prepared statement" do
stmt1 = @client.prepare 'SELECT 1 AS a', as: :hash
stmt2 = @client.prepare 'SELECT 1 AS a', as: :array

expect(stmt1.execute.first).to eq("a" => 1)
expect(stmt2.execute.first).to eq([1])

expect(stmt1.query_options).to include(as: :hash)
expect(stmt2.query_options).to include(as: :array)
end

it "should capture query options when preparing the statement" do
@client.query_options[:as] = :hash
stmt1 = @client.prepare 'SELECT 1 AS a'

@client.query_options[:as] = :array
stmt2 = @client.prepare 'SELECT 1 AS a'

expect(stmt1.execute.first).to eq("a" => 1)
expect(stmt2.execute.first).to eq([1])

expect(stmt1.query_options).to include(as: :hash)
expect(stmt2.query_options).to include(as: :array)
end

it "should keep its result after other query" do
@client.query 'USE test'
@client.query 'CREATE TABLE IF NOT EXISTS mysql2_stmt_q(a int)'
Expand Down Expand Up @@ -239,10 +264,7 @@ def stmt_count
context "streaming result" do
it "should be able to stream query result" do
n = 1
stmt = @client.prepare("SELECT 1 UNION SELECT 2")

@client.query_options.merge!(stream: true, cache_rows: false, as: :array)

stmt = @client.prepare("SELECT 1 UNION SELECT 2", stream: true, cache_rows: false, as: :array)
stmt.execute.each do |r|
case n
when 1
Expand All @@ -261,68 +283,56 @@ def stmt_count
# note: The current impl. of prepared statement requires results to be cached on #execute except for streaming queries
# The drawback of this is that args of Result#each is ignored...

it "should yield rows as hash's" do
@result = @client.prepare("SELECT 1").execute
@result.each do |row|
it "should yield rows as hashes" do
result = @client.prepare("SELECT 1").execute
result.each do |row|
expect(row).to be_an_instance_of(Hash)
end
end

it "should yield rows as hash's with symbol keys if :symbolize_keys was set to true" do
@client.query_options[:symbolize_keys] = true
@result = @client.prepare("SELECT 1").execute
@result.each do |row|
it "should yield rows as hashes with symbol keys if :symbolize_keys was set to true" do
result = @client.prepare("SELECT 1", symbolize_keys: true).execute
result.each do |row|
expect(row.keys.first).to be_an_instance_of(Symbol)
end
@client.query_options[:symbolize_keys] = false
end

it "should be able to return results as an array" do
@client.query_options[:as] = :array
it "should be able to return results as a hash" do
result = @client.prepare("SELECT 1", as: :hash).execute
result.each do |row|
expect(row).to be_an_instance_of(Hash)
end
end

@result = @client.prepare("SELECT 1").execute
@result.each do |row|
it "should be able to return results as an array" do
result = @client.prepare("SELECT 1", as: :array).execute
result.each do |row|
expect(row).to be_an_instance_of(Array)
end

@client.query_options[:as] = :hash
end

it "should cache previously yielded results by default" do
@result = @client.prepare("SELECT 1").execute
expect(@result.first.object_id).to eql(@result.first.object_id)
result = @client.prepare("SELECT 1").execute
expect(result.first.object_id).to eql(result.first.object_id)
end

it "should yield different value for #first if streaming" do
@client.query_options[:stream] = true
@client.query_options[:cache_rows] = false

result = @client.prepare("SELECT 1 UNION SELECT 2").execute
result = @client.prepare("SELECT 1 UNION SELECT 2", stream: true, cache_rows: false).execute
expect(result.first).not_to eql(result.first)

@client.query_options[:stream] = false
@client.query_options[:cache_rows] = true
end

it "should yield the same value for #first if streaming is disabled" do
@client.query_options[:stream] = false
result = @client.prepare("SELECT 1 UNION SELECT 2").execute
result = @client.prepare("SELECT 1 UNION SELECT 2", stream: false).execute
expect(result.first).to eql(result.first)
end

it "should throw an exception if we try to iterate twice when streaming is enabled" do
@client.query_options[:stream] = true
@client.query_options[:cache_rows] = false

result = @client.prepare("SELECT 1 UNION SELECT 2").execute
result = @client.prepare("SELECT 1 UNION SELECT 2", stream: true, cache_rows: false).execute

expect do
result.each {}
result.each {}
end.to raise_exception(Mysql2::Error)

@client.query_options[:stream] = false
@client.query_options[:cache_rows] = true
end
end

Expand Down