Skip to content

Commit d203086

Browse files
authored
Accept query options on Statement#execute (#912)
1 parent 5fa7700 commit d203086

File tree

4 files changed

+58
-56
lines changed

4 files changed

+58
-56
lines changed

ext/mysql2/client.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ static VALUE rb_mysql_client_async_result(VALUE self) {
595595
return Qnil;
596596
}
597597

598+
// Duplicate the options hash and put the copy in the Result object
598599
current = rb_hash_dup(rb_iv_get(self, "@current_query_options"));
599600
(void)RB_GC_GUARD(current);
600601
Check_Type(current, T_HASH);
@@ -1155,6 +1156,7 @@ static VALUE rb_mysql_client_store_result(VALUE self)
11551156
return Qnil;
11561157
}
11571158

1159+
// Duplicate the options hash and put the copy in the Result object
11581160
current = rb_hash_dup(rb_iv_get(self, "@current_query_options"));
11591161
(void)RB_GC_GUARD(current);
11601162
Check_Type(current, T_HASH);

ext/mysql2/statement.c

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
VALUE cMysql2Statement;
44
extern VALUE mMysql2, cMysql2Error, cBigDecimal, cDateTime, cDate;
5-
static VALUE sym_stream, intern_new_with_args, intern_each, intern_to_s;
5+
static VALUE sym_stream, intern_new_with_args, intern_each, intern_to_s, intern_merge_bang;
66
static VALUE intern_sec_fraction, intern_usec, intern_sec, intern_min, intern_hour, intern_day, intern_month, intern_year;
77

88
#define GET_STATEMENT(self) \
@@ -184,7 +184,7 @@ static void set_buffer_for_string(MYSQL_BIND* bind_buffer, unsigned long *length
184184
* the buffer is a Ruby string pointer and not our memory to manage.
185185
*/
186186
#define FREE_BINDS \
187-
for (i = 0; i < argc; i++) { \
187+
for (i = 0; i < c; i++) { \
188188
if (bind_buffers[i].buffer && NIL_P(params_enc[i])) { \
189189
xfree(bind_buffers[i].buffer); \
190190
} \
@@ -248,8 +248,10 @@ static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) {
248248
unsigned long *length_buffers = NULL;
249249
unsigned long bind_count;
250250
long i;
251+
int c;
251252
MYSQL_STMT *stmt;
252253
MYSQL_RES *metadata;
254+
VALUE opts;
253255
VALUE current;
254256
VALUE resultObj;
255257
VALUE *params_enc;
@@ -261,22 +263,25 @@ static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) {
261263

262264
conn_enc = rb_to_encoding(wrapper->encoding);
263265

264-
/* Scratch space for string encoding exports, allocate on the stack. */
265-
params_enc = alloca(sizeof(VALUE) * argc);
266+
// Get count of ordinary arguments, and extract hash opts/keyword arguments
267+
c = rb_scan_args(argc, argv, "*:", NULL, &opts);
268+
269+
// Scratch space for string encoding exports, allocate on the stack
270+
params_enc = alloca(sizeof(VALUE) * c);
266271

267272
stmt = stmt_wrapper->stmt;
268273

269274
bind_count = mysql_stmt_param_count(stmt);
270-
if (argc != (long)bind_count) {
271-
rb_raise(cMysql2Error, "Bind parameter count (%ld) doesn't match number of arguments (%d)", bind_count, argc);
275+
if (c != (long)bind_count) {
276+
rb_raise(cMysql2Error, "Bind parameter count (%ld) doesn't match number of arguments (%d)", bind_count, c);
272277
}
273278

274279
// setup any bind variables in the query
275280
if (bind_count > 0) {
276281
bind_buffers = xcalloc(bind_count, sizeof(MYSQL_BIND));
277282
length_buffers = xcalloc(bind_count, sizeof(unsigned long));
278283

279-
for (i = 0; i < argc; i++) {
284+
for (i = 0; i < c; i++) {
280285
bind_buffers[i].buffer = NULL;
281286
params_enc[i] = Qnil;
282287

@@ -416,10 +421,16 @@ static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) {
416421
return Qnil;
417422
}
418423

424+
// Duplicate the options hash, merge! extra opts, put the copy into the Result object
419425
current = rb_hash_dup(rb_iv_get(stmt_wrapper->client, "@query_options"));
420426
(void)RB_GC_GUARD(current);
421427
Check_Type(current, T_HASH);
422428

429+
// Merge in hash opts/keyword arguments
430+
if (!NIL_P(opts)) {
431+
rb_funcall(current, intern_merge_bang, 1, opts);
432+
}
433+
423434
is_streaming = (Qtrue == rb_hash_aref(current, sym_stream));
424435
if (!is_streaming) {
425436
// recieve the whole result set from the server
@@ -562,4 +573,5 @@ void init_mysql2_statement() {
562573
intern_year = rb_intern("year");
563574

564575
intern_to_s = rb_intern("to_s");
576+
intern_merge_bang = rb_intern("merge!");
565577
}

lib/mysql2/statement.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ module Mysql2
22
class Statement
33
include Enumerable
44

5-
def execute(*args)
5+
def execute(*args, **kwargs)
66
Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_CLASS => :never) do
7-
_execute(*args)
7+
_execute(*args, **kwargs)
88
end
99
end
1010
end

spec/mysql2/statement_spec.rb

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ def stmt_count
8686
expect(result.to_a).to eq(['max1' => int64_max1, 'max2' => int64_max2, 'max3' => int64_max3, 'min1' => int64_min1, 'min2' => int64_min2, 'min3' => int64_min3])
8787
end
8888

89+
it "should accept keyword arguments on statement execute" do
90+
stmt = @client.prepare 'SELECT 1 AS a'
91+
92+
expect(stmt.execute(as: :hash).first).to eq("a" => 1)
93+
expect(stmt.execute(as: :array).first).to eq([1])
94+
end
95+
96+
it "should accept bind arguments and keyword arguments on statement execute" do
97+
stmt = @client.prepare 'SELECT ? AS a'
98+
99+
expect(stmt.execute(1, as: :hash).first).to eq("a" => 1)
100+
expect(stmt.execute(1, as: :array).first).to eq([1])
101+
end
102+
89103
it "should keep its result after other query" do
90104
@client.query 'USE test'
91105
@client.query 'CREATE TABLE IF NOT EXISTS mysql2_stmt_q(a int)'
@@ -186,10 +200,9 @@ def stmt_count
186200
end
187201

188202
it "should warn but still work if cache_rows is set to false" do
189-
@client.query_options[:cache_rows] = false
190203
statement = @client.prepare 'SELECT 1'
191204
result = nil
192-
expect { result = statement.execute.to_a }.to output(/:cache_rows is forced for prepared statements/).to_stderr
205+
expect { result = statement.execute(cache_rows: false).to_a }.to output(/:cache_rows is forced for prepared statements/).to_stderr
193206
expect(result.length).to eq(1)
194207
end
195208

@@ -238,10 +251,7 @@ def stmt_count
238251
it "should be able to stream query result" do
239252
n = 1
240253
stmt = @client.prepare("SELECT 1 UNION SELECT 2")
241-
242-
@client.query_options.merge!(stream: true, cache_rows: false, as: :array)
243-
244-
stmt.execute.each do |r|
254+
stmt.execute(stream: true, cache_rows: false, as: :array).each do |r|
245255
case n
246256
when 1
247257
expect(r).to eq([1])
@@ -267,23 +277,17 @@ def stmt_count
267277
end
268278

269279
it "should yield rows as hash's with symbol keys if :symbolize_keys was set to true" do
270-
@client.query_options[:symbolize_keys] = true
271-
@result = @client.prepare("SELECT 1").execute
280+
@result = @client.prepare("SELECT 1").execute(symbolize_keys: true)
272281
@result.each do |row|
273282
expect(row.keys.first).to be_an_instance_of(Symbol)
274283
end
275-
@client.query_options[:symbolize_keys] = false
276284
end
277285

278286
it "should be able to return results as an array" do
279-
@client.query_options[:as] = :array
280-
281-
@result = @client.prepare("SELECT 1").execute
287+
@result = @client.prepare("SELECT 1").execute(as: :array)
282288
@result.each do |row|
283289
expect(row).to be_an_instance_of(Array)
284290
end
285-
286-
@client.query_options[:as] = :hash
287291
end
288292

289293
it "should cache previously yielded results by default" do
@@ -292,35 +296,21 @@ def stmt_count
292296
end
293297

294298
it "should yield different value for #first if streaming" do
295-
@client.query_options[:stream] = true
296-
@client.query_options[:cache_rows] = false
297-
298-
result = @client.prepare("SELECT 1 UNION SELECT 2").execute
299+
result = @client.prepare("SELECT 1 UNION SELECT 2").execute(stream: true, cache_rows: true)
299300
expect(result.first).not_to eql(result.first)
300-
301-
@client.query_options[:stream] = false
302-
@client.query_options[:cache_rows] = true
303301
end
304302

305303
it "should yield the same value for #first if streaming is disabled" do
306-
@client.query_options[:stream] = false
307-
result = @client.prepare("SELECT 1 UNION SELECT 2").execute
304+
result = @client.prepare("SELECT 1 UNION SELECT 2").execute(stream: false)
308305
expect(result.first).to eql(result.first)
309306
end
310307

311308
it "should throw an exception if we try to iterate twice when streaming is enabled" do
312-
@client.query_options[:stream] = true
313-
@client.query_options[:cache_rows] = false
314-
315-
result = @client.prepare("SELECT 1 UNION SELECT 2").execute
316-
309+
result = @client.prepare("SELECT 1 UNION SELECT 2").execute(stream: true, cache_rows: false)
317310
expect do
318311
result.each {}
319312
result.each {}
320313
end.to raise_exception(Mysql2::Error)
321-
322-
@client.query_options[:stream] = false
323-
@client.query_options[:cache_rows] = true
324314
end
325315
end
326316

@@ -369,21 +359,20 @@ def stmt_count
369359

370360
context "cast booleans for TINYINT if :cast_booleans is enabled" do
371361
# rubocop:disable Style/Semicolon
372-
let(:client) { new_client(cast_booleans: true) }
373-
let(:id1) { client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES ( 1)'; client.last_id }
374-
let(:id2) { client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES ( 0)'; client.last_id }
375-
let(:id3) { client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (-1)'; client.last_id }
362+
let(:id1) { @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES ( 1)'; @client.last_id }
363+
let(:id2) { @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES ( 0)'; @client.last_id }
364+
let(:id3) { @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (-1)'; @client.last_id }
376365
# rubocop:enable Style/Semicolon
377366

378367
after do
379-
client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2},#{id3})"
368+
@client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2},#{id3})"
380369
end
381370

382371
it "should return TrueClass or FalseClass for a TINYINT value if :cast_booleans is enabled" do
383-
query = client.prepare 'SELECT bool_cast_test FROM mysql2_test WHERE id = ?'
384-
result1 = query.execute id1
385-
result2 = query.execute id2
386-
result3 = query.execute id3
372+
query = @client.prepare 'SELECT bool_cast_test FROM mysql2_test WHERE id = ?'
373+
result1 = query.execute id1, cast_booleans: true
374+
result2 = query.execute id2, cast_booleans: true
375+
result3 = query.execute id3, cast_booleans: true
387376
expect(result1.first['bool_cast_test']).to be true
388377
expect(result2.first['bool_cast_test']).to be false
389378
expect(result3.first['bool_cast_test']).to be true
@@ -392,19 +381,18 @@ def stmt_count
392381

393382
context "cast booleans for BIT(1) if :cast_booleans is enabled" do
394383
# rubocop:disable Style/Semicolon
395-
let(:client) { new_client(cast_booleans: true) }
396-
let(:id1) { client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (1)'; client.last_id }
397-
let(:id2) { client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (0)'; client.last_id }
384+
let(:id1) { @client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (1)'; @client.last_id }
385+
let(:id2) { @client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (0)'; @client.last_id }
398386
# rubocop:enable Style/Semicolon
399387

400388
after do
401-
client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2})"
389+
@client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2})"
402390
end
403391

404392
it "should return TrueClass or FalseClass for a BIT(1) value if :cast_booleans is enabled" do
405-
query = client.prepare 'SELECT single_bit_test FROM mysql2_test WHERE id = ?'
406-
result1 = query.execute id1
407-
result2 = query.execute id2
393+
query = @client.prepare 'SELECT single_bit_test FROM mysql2_test WHERE id = ?'
394+
result1 = query.execute id1, cast_booleans: true
395+
result2 = query.execute id2, cast_booleans: true
408396
expect(result1.first['single_bit_test']).to be true
409397
expect(result2.first['single_bit_test']).to be false
410398
end

0 commit comments

Comments
 (0)