Skip to content

Commit 15f8e6e

Browse files
authored
Improve prepared statement ergonomics (#1385)
While working on Mysql2 prepared statement support in Rails, I found them very hard to use. Most notably we sometimes need to close them eagerly, but it's complicated by the fact that you can only call `close` once, any subsequent call raises an error. There was also no way to check if a statement was closed or not. This change noops on repeat calls to `close` and adds `closed?`
1 parent 03ac203 commit 15f8e6e

File tree

3 files changed

+38
-8
lines changed

3 files changed

+38
-8
lines changed

Gemfile

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ group :test do
1717
gem 'rspec', '~> 3.2'
1818

1919
# https://github.com/bbatsov/rubocop/pull/4789
20-
gem 'rubocop', '~> 1.30', '>= 1.30.1' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6')
20+
# 1.51 is the last version supporting Ruby 2.6
21+
gem 'rubocop', '>= 1.30.1', '< 1.51' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6')
2122
end
2223

2324
group :benchmarks, optional: true do

ext/mysql2/statement.c

+23-4
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ static VALUE intern_sec_fraction, intern_usec, intern_sec, intern_min, intern_ho
1010
#define TypedData_Get_Struct(obj, type, ignore, sval) Data_Get_Struct(obj, type, sval)
1111
#endif
1212

13-
#define GET_STATEMENT(self) \
13+
#define RAW_GET_STATEMENT(self) \
1414
mysql_stmt_wrapper *stmt_wrapper; \
1515
TypedData_Get_Struct(self, mysql_stmt_wrapper, &rb_mysql_statement_type, stmt_wrapper); \
16+
17+
#define GET_STATEMENT(self) \
18+
RAW_GET_STATEMENT(self) \
1619
if (!stmt_wrapper->stmt) { rb_raise(cMysql2Error, "Invalid statement handle"); } \
1720
if (stmt_wrapper->closed) { rb_raise(cMysql2Error, "Statement handle already closed"); }
1821

@@ -603,12 +606,27 @@ static VALUE rb_mysql_stmt_affected_rows(VALUE self) {
603606
* own prepared statement cache.
604607
*/
605608
static VALUE rb_mysql_stmt_close(VALUE self) {
606-
GET_STATEMENT(self);
607-
stmt_wrapper->closed = 1;
608-
rb_thread_call_without_gvl(nogvl_stmt_close, stmt_wrapper, RUBY_UBF_IO, 0);
609+
RAW_GET_STATEMENT(self);
610+
611+
if (!stmt_wrapper->closed) {
612+
stmt_wrapper->closed = 1;
613+
rb_thread_call_without_gvl(nogvl_stmt_close, stmt_wrapper, RUBY_UBF_IO, 0);
614+
}
615+
609616
return Qnil;
610617
}
611618

619+
/* call-seq:
620+
* stmt.closed?
621+
*
622+
* Returns wheter or not the statement have been closed.
623+
*/
624+
static VALUE rb_mysql_stmt_closed_p(VALUE self) {
625+
RAW_GET_STATEMENT(self);
626+
627+
return stmt_wrapper->closed ? Qtrue : Qfalse;
628+
}
629+
612630
void init_mysql2_statement() {
613631
cDate = rb_const_get(rb_cObject, rb_intern("Date"));
614632
rb_global_variable(&cDate);
@@ -630,6 +648,7 @@ void init_mysql2_statement() {
630648
rb_define_method(cMysql2Statement, "last_id", rb_mysql_stmt_last_id, 0);
631649
rb_define_method(cMysql2Statement, "affected_rows", rb_mysql_stmt_affected_rows, 0);
632650
rb_define_method(cMysql2Statement, "close", rb_mysql_stmt_close, 0);
651+
rb_define_method(cMysql2Statement, "closed?", rb_mysql_stmt_closed_p, 0);
633652

634653
sym_stream = ID2SYM(rb_intern("stream"));
635654

spec/mysql2/statement_spec.rb

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
require './spec/spec_helper'
22

3-
RSpec.describe Mysql2::Statement do
3+
RSpec.describe Mysql2::Statement do # rubocop:disable Metrics/BlockLength
44
before(:example) do
55
@client = new_client(encoding: "utf8")
66
end
@@ -319,8 +319,8 @@ def stmt_count
319319
it "should throw an exception if we try to iterate twice when streaming is enabled" do
320320
result = @client.prepare("SELECT 1 UNION SELECT 2").execute(stream: true, cache_rows: false)
321321
expect do
322-
result.each {}
323-
result.each {}
322+
result.to_a
323+
result.to_a
324324
end.to raise_exception(Mysql2::Error)
325325
end
326326
end
@@ -720,5 +720,15 @@ def stmt_count
720720
stmt.close
721721
expect { stmt.execute }.to raise_error(Mysql2::Error, /Invalid statement handle/)
722722
end
723+
724+
it 'should not raise if called multiple times' do
725+
stmt = @client.prepare 'SELECT 1'
726+
expect(stmt).to_not be_closed
727+
728+
3.times do
729+
expect { stmt.close }.to_not raise_error
730+
expect(stmt).to be_closed
731+
end
732+
end
723733
end
724734
end

0 commit comments

Comments
 (0)